Repository: airbnb/react-sketchapp
Branch: master
Commit: b238e69c6f1e
Files: 320
Total size: 477.5 KB
Directory structure:
gitextract_dx66tsc9/
├── .bookignore
├── .editorconfig
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ └── ISSUE_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__/
│ ├── jest/
│ │ ├── components/
│ │ │ ├── Artboard.tsx
│ │ │ ├── Document.tsx
│ │ │ ├── Image.tsx
│ │ │ ├── Page.tsx
│ │ │ ├── RedBox.tsx
│ │ │ ├── Svg.tsx
│ │ │ ├── Text.tsx
│ │ │ ├── View.tsx
│ │ │ ├── __snapshots__/
│ │ │ │ ├── Artboard.tsx.snap
│ │ │ │ ├── Document.tsx.snap
│ │ │ │ ├── Image.tsx.snap
│ │ │ │ ├── Page.tsx.snap
│ │ │ │ ├── RedBox.tsx.snap
│ │ │ │ ├── Svg.tsx.snap
│ │ │ │ ├── Text.tsx.snap
│ │ │ │ └── View.tsx.snap
│ │ │ └── nodeImpl/
│ │ │ ├── Svg.tsx
│ │ │ └── __snapshots__/
│ │ │ └── Svg.tsx.snap
│ │ ├── index.ts
│ │ ├── jsonUtils/
│ │ │ ├── computeTextTree.ts
│ │ │ ├── computeYogaNode.ts
│ │ │ ├── computeYogaTree.ts
│ │ │ ├── layerGroup.ts
│ │ │ ├── models.ts
│ │ │ ├── shapeLayers.ts
│ │ │ └── style.ts
│ │ ├── reactTreeToFlexTree.ts
│ │ ├── sharedStyles/
│ │ │ └── TextStyles.ts
│ │ └── utils/
│ │ ├── isDefined.ts
│ │ ├── sortObjectKeys.ts
│ │ └── zIndex.ts
│ └── skpm/
│ ├── basic.test.js
│ ├── render-context.test.js
│ └── render-in-wrapped-object.test.js
├── book.json
├── docs/
│ ├── API.md
│ ├── FAQ.md
│ ├── README.md
│ ├── examples.md
│ └── guides/
│ ├── README.md
│ ├── data-fetching.md
│ ├── getting-started.md
│ ├── rendering.md
│ ├── styling.md
│ ├── universal-rendering.md
│ └── using-skpm.md
├── examples/
│ ├── .eslintrc
│ ├── .gitignore
│ ├── basic-setup/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ ├── basic-setup-typescript/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── manifest.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── my-command.tsx
│ │ │ └── types/
│ │ │ └── sketch.d.ts
│ │ ├── tsconfig.json
│ │ └── webpack.skpm.config.js
│ ├── basic-svg/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ ├── colors/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── main.js
│ │ │ └── manifest.json
│ │ └── webpack.skpm.config.js
│ ├── emotion/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ ├── form-validation/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Button.js
│ │ │ │ ├── Register.js
│ │ │ │ ├── Space.js
│ │ │ │ ├── StrengthMeter.js
│ │ │ │ └── TextBox/
│ │ │ │ ├── index.js
│ │ │ │ ├── index.sketch.js
│ │ │ │ └── style.js
│ │ │ ├── data.js
│ │ │ ├── designSystem.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ └── web.js
│ │ └── webpack.skpm.config.js
│ ├── foursquare-maps/
│ │ ├── .eslintrc
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.js
│ │ │ ├── getVenues.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ └── web.js
│ │ └── webpack.skpm.config.js
│ ├── glamorous/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ ├── profile-cards/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Profile.js
│ │ │ │ └── Space.js
│ │ │ ├── designSystem.js
│ │ │ ├── main.js
│ │ │ └── manifest.json
│ │ └── webpack.skpm.config.js
│ ├── profile-cards-graphql/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Profile.js
│ │ │ │ └── Space.js
│ │ │ ├── designSystem.js
│ │ │ ├── main.js
│ │ │ └── manifest.json
│ │ └── webpack.skpm.config.js
│ ├── profile-cards-primitives/
│ │ ├── README.md
│ │ ├── nwb.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Profile.js
│ │ │ │ └── Space.js
│ │ │ ├── data.js
│ │ │ ├── designSystem.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ └── web.js
│ │ └── webpack.skpm.config.js
│ ├── profile-cards-react-with-styles/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ └── Profile.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ ├── theme.js
│ │ │ ├── types.js
│ │ │ └── withStyles.js
│ │ └── webpack.skpm.config.js
│ ├── react-router-prototyping/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.js
│ │ │ ├── components/
│ │ │ │ ├── AppBar.js
│ │ │ │ └── NavBar.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ └── routes/
│ │ │ ├── about.js
│ │ │ ├── home.js
│ │ │ ├── post.js
│ │ │ └── profile.js
│ │ └── webpack.skpm.config.js
│ ├── styled-components/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ ├── styleguide/
│ │ ├── .flowconfig
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── AccessibilityBadge.js
│ │ │ │ ├── Badge.js
│ │ │ │ ├── Label.js
│ │ │ │ ├── Palette.js
│ │ │ │ ├── Section.js
│ │ │ │ ├── Swatch.js
│ │ │ │ └── TypeSpecimen.js
│ │ │ ├── designSystem.js
│ │ │ ├── main.js
│ │ │ ├── manifest.json
│ │ │ └── processColor.js
│ │ └── webpack.skpm.config.js
│ ├── symbols/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── manifest.json
│ │ │ └── my-command.js
│ │ └── webpack.skpm.config.js
│ └── timeline-airtable/
│ ├── .eslintrc
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── main.js
│ │ └── manifest.json
│ └── webpack.skpm.config.js
├── jest.config.js
├── package.json
├── prettier.config.js
├── src/
│ ├── Platform.ts
│ ├── buildTree.ts
│ ├── components/
│ │ ├── Artboard.tsx
│ │ ├── Document.tsx
│ │ ├── Image.tsx
│ │ ├── ImageStylePropTypes.ts
│ │ ├── Page.tsx
│ │ ├── PageStylePropTypes.ts
│ │ ├── RedBox.tsx
│ │ ├── ResizeModePropTypes.ts
│ │ ├── ResizingConstraintPropTypes.ts
│ │ ├── ShadowsPropTypes.ts
│ │ ├── Svg/
│ │ │ ├── Circle.tsx
│ │ │ ├── ClipPath.tsx
│ │ │ ├── Defs.tsx
│ │ │ ├── Ellipse.tsx
│ │ │ ├── G.tsx
│ │ │ ├── Image.tsx
│ │ │ ├── Line.tsx
│ │ │ ├── LinearGradient.tsx
│ │ │ ├── Path.tsx
│ │ │ ├── Pattern.tsx
│ │ │ ├── Polygon.tsx
│ │ │ ├── Polyline.tsx
│ │ │ ├── RadialGradient.tsx
│ │ │ ├── Rect.tsx
│ │ │ ├── Stop.tsx
│ │ │ ├── Svg.tsx
│ │ │ ├── Symbol.tsx
│ │ │ ├── TSpan.tsx
│ │ │ ├── Text.tsx
│ │ │ ├── TextPath.tsx
│ │ │ ├── Use.tsx
│ │ │ ├── index.tsx
│ │ │ └── props.ts
│ │ ├── Text.tsx
│ │ ├── TextStylePropTypes.ts
│ │ ├── View.tsx
│ │ ├── ViewStylePropTypes.ts
│ │ └── index.ts
│ ├── context.tsx
│ ├── entrypoint.sketch.ts
│ ├── entrypoint.ts
│ ├── flexToSketchJSON.ts
│ ├── index.ts
│ ├── jsonUtils/
│ │ ├── borders.ts
│ │ ├── computeTextTree.ts
│ │ ├── computeYogaNode.ts
│ │ ├── computeYogaTree.ts
│ │ ├── hotspotLayer.ts
│ │ ├── layerGroup.ts
│ │ ├── makeSvgLayer/
│ │ │ ├── graphics/
│ │ │ │ ├── curvePoint.ts
│ │ │ │ ├── path.ts
│ │ │ │ ├── point.ts
│ │ │ │ ├── rect.ts
│ │ │ │ └── types.ts
│ │ │ ├── index.sketch.ts
│ │ │ └── index.ts
│ │ ├── models.ts
│ │ ├── resizeConstraint.ts
│ │ ├── shapeLayers.ts
│ │ ├── sketchJson/
│ │ │ ├── fromSJSON.ts
│ │ │ └── toSJSON.ts
│ │ ├── style.ts
│ │ └── textLayers.ts
│ ├── platformBridges/
│ │ ├── macos.ts
│ │ └── sketch/
│ │ ├── createStringMeasurer.ts
│ │ ├── findFontName.ts
│ │ ├── index.ts
│ │ └── makeImageDataFromUrl.ts
│ ├── render.tsx
│ ├── renderToJSON.ts
│ ├── renderers/
│ │ ├── ArtboardRenderer.ts
│ │ ├── ImageRenderer.ts
│ │ ├── SketchRenderer.ts
│ │ ├── SvgRenderer.ts
│ │ ├── SymbolInstanceRenderer.ts
│ │ ├── SymbolMasterRenderer.ts
│ │ ├── TextRenderer.ts
│ │ ├── ViewRenderer.ts
│ │ └── index.ts
│ ├── resets.ts
│ ├── sharedStyles/
│ │ └── TextStyles.ts
│ ├── stylesheet/
│ │ ├── expandStyle.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── symbol.tsx
│ ├── types/
│ │ ├── globals.d.ts
│ │ ├── index.ts
│ │ ├── intrinsic.d.ts
│ │ ├── js-sha1.d.ts
│ │ ├── murmur2js.d.ts
│ │ ├── node-sketch-bridge.d.ts
│ │ └── normalize-css-color.d.ts
│ └── utils/
│ ├── Context.ts
│ ├── constants.ts
│ ├── createStringMeasurer.ts
│ ├── getDocument.ts
│ ├── getImageDataFromURL.ts
│ ├── getSketchVersion.ts
│ ├── hasAnyDefined.ts
│ ├── hashStyle.ts
│ ├── isDefined.ts
│ ├── isNativeDocument.ts
│ ├── isNativePage.ts
│ ├── isNativeSymbolsPage.ts
│ ├── pick.ts
│ ├── processTransform/
│ │ ├── index.ts
│ │ ├── matrix2D.ts
│ │ ├── parseTransformOriginProp.ts
│ │ └── parseTransformProp.ts
│ ├── same.ts
│ ├── sharedTextStyles/
│ │ ├── index.sketch.ts
│ │ └── index.ts
│ ├── sortObjectKeys.ts
│ └── zIndex.ts
├── template/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── src/
│ ├── manifest.json
│ └── my-command.js
├── tsconfig.json
└── tsconfig.module.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .bookignore
================================================
.github/
__tests__/
examples/
lib/
scratch/
src/
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jon.gold@airbnb.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
Contributions are welcome and are greatly appreciated! Every little bit helps, and credit will always be given. By contributing, you agree to abide by the [code of conduct](https://github.com/airbnb/react-sketchapp/blob/master/.github/CODE_OF_CONDUCT.md).
## Reporting Issues and Asking Questions
**For support or usage questions like “how do I do X with react-sketchapp” and “my code doesn't work”, please search and ask on [StackOverflow with a react-sketchapp tag](http://stackoverflow.com/questions/tagged/react-sketchapp?sort=votes&pageSize=50) first.**
We ask you to do this because StackOverflow does a much better job at keeping popular questions visible. Unfortunately good answers get lost and outdated on GitHub.
Some questions take a long time to get an answer. **If your question gets closed or you don't get a reply on StackOverflow for longer than a few days,** we encourage you to post an issue linking to your question. We will close your issue but this will give people watching the repo an opportunity to see your question and reply to it on StackOverflow if they know the answer.
Please be considerate when doing this as this is not the primary purpose of the issue tracker.
### Help Us Help You
On both websites, it is a good idea to structure your code and question in a way that is easy to read to entice people to answer it. For example, we encourage you to use syntax highlighting, indentation, and split text in paragraphs.
Please keep in mind that people spend their free time trying to help you. You can make it easier for them if you provide versions of the relevant libraries and a runnable small project reproducing your issue. You can put your code on [JSBin](http://jsbin.com) or, for bigger projects, on GitHub. Make sure all the necessary dependencies are declared in `package.json` so anyone can run `npm install && npm start` and reproduce your issue.
## Development
Visit the [issue tracker](https://github.com/airbnb/react-sketchapp/issues) to find a list of open issues that need attention.
Fork, then clone the repo
```bash
git clone https://github.com/your-username/react-sketchapp.git
```
### Setting up your environment
### Testing, style & Linting
To run tests
```bash
npm run test
```
To run tests continuously
```bash
npm run test:watch
```
This style of the codebase is enforced by [Prettier](https://prettier.io/).
It is recommended that you install a Prettier plugin for your editor of choice when working on this codebase.
### Docs
We always appreciate improvements to the documentation!
#### Installing Gitbook
To install the latest version of `gitbook` and prepare to build the documentation, run the following:
```
npm run docs:prepare
```
#### Building the Docs
To build the documentation, run the following:
```
npm run docs:build
```
To watch and rebuild documentation when changes occur, run the following:
```
npm run docs:watch
```
The docs will be served at http://localhost:4000.
#### Publishing the Docs
To publish the documentation, run the following:
```
npm run docs:publish
```
### Sending a Pull Request
For non-trivial changes, please open an issue with a proposal for a new feature or refactoring before starting work — we don't want you to waste your time on a pull request that won't be accepted.
On the other hand, sometimes the best way to start a discussion _is_ to send a pull request. Use your judgement!
In general, the contribution workflow looks like this:
- Open a new issue in the [Issue tracker](https://github.com/airbnb/react-sketchapp/issues).
- Fork the repo.
- Create a new feature branch based off the `master` branch.
- Make sure all tests pass.
- Submit a pull request, referencing any issues it addresses.
Please try to keep your pull request focused in scope and avoid including unrelated commits.
After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements.
Thank you for contributing!
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
👋 Hello! Thanks for contributing. Please use the template that matches your intention
_I am..._
| -------------------------------------------------------------------------------------------------
| Requesting a new feature
| -------------------------------------------------------------------------------------------------
**Proposal/Feature-request:**
| -------------------------------------------------------------------------------------------------
| Reporting a bug or issue
| -------------------------------------------------------------------------------------------------
**Expected behavior:**
**Observed behavior:**
**How to reproduce:**
**Sketch version:**
**Please attach screenshots, a zip file of your project, and/or a link to your github project**
================================================
FILE: .gitignore
================================================
# gitignore
coverage/
.DS_Store
*.log*
node_modules
lib
react-example.sketchplugin
_book
.vscode
# only apps should have lockfiles
yarn.lock
package-lock.json
npm-shrinkwrap.json
================================================
FILE: .npmignore
================================================
.DS_Store
*.log*
node_modules
_book
examples
docs
.vscode
yarn.lock
__tests__
.github
template
book.json
prettier.config.js
.editorconfig
.bookignore
src
jest.config.js
tsconfig.json
tsconfig.module.json
.travis.yml
================================================
FILE: .travis.yml
================================================
os: osx
language: node_js
cache:
directories:
- node_modules
# - $HOME/Library/Caches/Homebrew
notifications:
email: false
node_js:
- 'lts/*'
# before_install:
# - brew update
# - brew cask install sketch # install Sketch
# - mkdir -p "~/Library/Application Support/com.bohemiancoding.sketch3/Plugins" # create plugins folder
# - echo $SKETCH_LICENSE > "~/Library/Application Support/com.bohemiancoding.sketch3/.deployment" # add the Sketch license
before_script:
- npm prune
script:
- npm run test:ci
# - npm run test:e2e -- --app=/Applications/Sketch.app
# after_script:
# - rm "~/Library/App Support/com.bohemiancoding.sketch3/.deployment" # remove the Sketch license
branches:
except:
- /^v\d+\.\d+\.\d+$/
================================================
FILE: CHANGELOG.md
================================================
# Change Log
This project adheres to [Semantic Versioning](http://semver.org/). Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/airbnb/react-sketchapp/releases) page.
## Version 3.2.6
- Fix the SVG component export
## Version 3.2.5
- Fix Skpm taking the wrong entry point when requiring react-sketchapp
## Version 3.2.4
- Fix the generated ES package
## Version 3.2.3
- Fix getting the font name (#510)
## Version 3.2.2
- Fix getting the default bridge on NodeJS
## Version 3.2.1
- `Platform.version` now reflects the Sketch version
- Fix a bug for a broken version of `@sketch-hq/sketch-file-format-ts`
## Version 3.2.0
- Add a new `useWindowDimensions` hook for Artboard viewport (#501)
## Version 3.1.3
- Add proptypes for Text
- Allow `fontWeigth` to be a number
## Version 3.1.2
- Handle passing a Sketch document more properly
## Version 3.1.1
- Fix for Sketch 64
## Version 3.1.0
- Fix acceptable text children (#474)
- Fix parsing of SVG arc shorthand parameters (#467)
- Change default font resolution, always falling back to the system font when the `fontFamily` is missing or not specified
## Version 3.0.5
- Fix missing dependency (#462)
## Version 3.0.4
- Fix rendering images (#458)
## Version 3.0.3
- Fix typo in Symbol (Thanks @antoni!)
- Fix messed up `js-sha` import (#456)
## Version 3.0.2
- Fix rotation direction (#433)
- Fix Svg renders when the shape doesn't fit the viewbox (#288)
- Add missing strokeAlignment prop (#276)
## Version 3.0.1
- Allow passing a style object when making a symbol
- Expose `getSymbolMasterByName`
## Version 3.0.0
- Export Svg components in the Svg/index.js file (Thanks @saschazar21!)
- Fix setting the overflow
- The symbol masters will try to maintain their overrides IDs so as not to reset instances that have overrides
- Improve error messages when trying to render a broken override
- Do not crash if there is no source for an Image, we will just show an placeholder for the image
- Handle specifying document in injectSymbols (#388)
- Add support for paragraph spacing (#382 - Thanks @lessthanzero!)
- `Image` and `Text` now support multiple shadows just like `View`
- Add support for `TextShadow`
- Add support for `transform`
- Add support for running `react-sketchapp` on NodeJS using `renderToJSON()`
- Port to TypeScript and publish TypeScript definitions
- `TextStyles.get(name)` now returns text styles that are part of the document (even if they haven't been defined with `react-sketchapp`) (#407)
- `getSymbolComponentByName` now returns Symbols that are part of the document (even if they haven't been defined with `react-sketchapp`) (#177)
- Switch the order of the `TextStyles.create` arguments to `TextStyles.create(styles, options)`
## Version 3.0.0-beta.9
- Fix setting the overflow
- The symbol masters will try to maintain their overrides IDs so as not to reset instances that have overrides
- Improve error messages when trying to render a broken override
- Export Svg components in the Svg/index.js file (Thanks @saschazar21!)
## Version 3.0.0-beta.8
- Flatten styles in exported Svg component (Thanks @dabbott!)
## Version 3.0.0-beta.7
- Add Node.js SVG renderer (Thanks @dabbott!)
## Version 3.0.0-beta.6
- Do not crash if there is no source for an Image, we will just show an placeholder for the image
## Version 3.0.0-beta.3 to 3.0.0-beta.5
- Fix setting overrides (#409)
- Fix images on NodeJS
- Fix Border-radius clipping incorrectly calculated (#279)
## Version 3.0.0-beta.1
- Fix ShapeGroup on nodejs (#387)
- Handle specifying document in injectSymbols (#388)
- Fix support for paragraph spacing on sketch >= 49 (#390)
## Version 3.0.0-beta.0
- Add support for paragraph spacing (#382 - Thanks @lessthanzero!)
- `Image` and `Text` now support multiple shadows just like `View`
- add support for `TextShadow`
- Experimental support for `transform`
- Experimental support for running `react-sketchapp` on NodeJS
## Version 2.1.0
- Ensure `makeSymbol` does not change currentPage (#353 - Thanks @jaridmargolin!)
- Fix Text decoration underline style (#370 - Thanks @thecalvinchan!)
- Add possibility to add multiple shadows and shadow spread (#277 - Thanks @ludwigfrank and @thierryc!)
- Support rendering into wrapped object (hence support the new Sketch API) (#379)
## Version 2.0.0
- Now throws if the "Symbols" page is explicitly passed in as the `container` on the `render` method. Previously if you explicitly passed in the "Symbols" pages as a container, it would create a new page and render onto that. (#297 - Thanks @jaridmargolin!)
- Now throws an error if you attempt to render a Document component into a node intended to be a child of `Document`. (#297 - Thanks @jaridmargolin!)
- Adds support for rendering a `Page` component into a container passed through the `render` method. This allows for rendering multiple `Artboard`s onto an existing page. (#297 - Thanks @jaridmargolin!)
- More predictable rendering of `RedBox`. (#297 - Thanks @jaridmargolin!)
- Fix Symbols overrides for Sketch >= 46 (#198 - Thanks @ianhook!)
- Fix text overrides when the name of the Text layer is not explicitly defined (#292 - Thanks @jaridmargolin!)
- update `yoga-node` to 1.9 (#314)
- Add support for Sketch 50 (#290)
- Fix shared text style matching (#290)
- Remove n^2 rendering problem with large symbol sets (#235 - Thanks @ianhook!)
- `Page` without a name explicitly set will be auto-incremented ("Page 1", "Page 2", etc.) just like how Sketch is doing by default (#296 - Thanks @jaridmargolin!)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Airbnb
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
render React components to Sketch; tailor-made for design systems
## Quick-start 🏃
First, make sure you have installed [Sketch](http://sketch.com) version 50+, & a recent [npm](https://nodejs.org/en/download/).
Open a new Sketch file, then in a terminal:
```bash
git clone https://github.com/airbnb/react-sketchapp.git
cd react-sketchapp/examples/basic-setup && npm install
npm run render
```
Next, [check out some more examples](https://github.com/airbnb/react-sketchapp/tree/master/examples)!

[](https://www.npmjs.com/package/react-sketchapp)  [](https://travis-ci.org/airbnb/react-sketchapp)
## Why?!
Managing the assets of design systems in Sketch is complex, error-prone and time consuming. Sketch is scriptable, but the API often changes. React provides the perfect wrapper to build reusable documents in a way already familiar to JavaScript developers.
## What does the code look like?
```js
import * as React from 'react';
import { render, Text, Artboard } from 'react-sketchapp';
const App = props => (
{props.message}
);
export default context => {
render( , context.document.currentPage());
};
```
## What can I do with it?
- **Manage design systems—** `react-sketchapp` was built for [Airbnb’s design system](http://airbnb.design/building-a-visual-language/); this is the easiest way to manage Sketch assets in a large design system
- **Use real components for designs—** Implement your designs in code as React components and render them into Sketch
- **Design with real data—** Designing with data is important but challenging; `react-sketchapp` makes it simple to fetch and incorporate real data into your Sketch files
- **Build new tools on top of Sketch—** the easiest way to use Sketch as a canvas for custom design tooling
Found a novel use? We'd love to hear about it!
[Read more about why we built it](http://airbnb.design/painting-with-code/)
## Documentation
- [Examples](http://airbnb.io/react-sketchapp/docs/examples.html)
- [API Reference](http://airbnb.io/react-sketchapp/docs/API.html)
- [Styling](http://airbnb.io/react-sketchapp/docs/guides/styling.html)
- [Universal Rendering](http://airbnb.io/react-sketchapp/docs/guides/universal-rendering.html)
- [Data Fetching](http://airbnb.io/react-sketchapp/docs/guides/data-fetching.html)
- [FAQ](http://airbnb.io/react-sketchapp/docs/FAQ.html)
- [Contributing](https://github.com/airbnb/react-sketchapp/blob/master/.github/CONTRIBUTING.md)
================================================
FILE: __tests__/jest/components/Artboard.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { Artboard } from '../../../src/components/Artboard';
import { StyleSheet } from '../../../src/stylesheet';
describe(' ', () => {
it('renders children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
it.todo('flattens its stylesheet');
describe('name', () => {
it('passes its name', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('defaults to Artboard', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('style', () => {
const styles = StyleSheet.create({
view: {
flex: 1,
},
});
it('accepts a plain object', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts a StyleSheet ordinal', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts an array of plain objects and/or StyleSheet ordinals', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts artboard viewport preset', () => {
const tree = renderer
.create(
,
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
});
================================================
FILE: __tests__/jest/components/Document.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { Document } from '../../../src/components/Document';
describe(' ', () => {
it('renders children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
});
================================================
FILE: __tests__/jest/components/Image.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { Image } from '../../../src/components/Image';
import { StyleSheet } from '../../../src/stylesheet';
describe(' ', () => {
it('renders children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
it.todo('flattens its stylesheet');
describe('name', () => {
it('passes its name', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('defaults to Image', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('resizeMode', () => {
it('translates contain', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('translates cover', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('translates stretch', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('translates center', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('translates repeat', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('translates none', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('falls back to cover', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('prefers prop to style', () => {
const tree = renderer
.create( )
.toJSON();
expect(tree).toMatchSnapshot();
});
it('falls back to a resizeMode from style', () => {
const tree = renderer
.create( )
.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('source', () => {
it('prefers source over defaultSource', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('falls back to defaultSource if available', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('sets height from source', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('sets width from source', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('favors style over source for height', () => {
const tree = renderer
.create( )
.toJSON();
expect(tree).toMatchSnapshot();
});
it('favors style over source for width', () => {
const tree = renderer
.create( )
.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('style', () => {
const styles = StyleSheet.create({
view: {
flex: 1,
},
});
it('accepts a plain object', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts a StyleSheet ordinal', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts an array of plain objects and/or StyleSheet ordinals', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
================================================
FILE: __tests__/jest/components/Page.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { Page } from '../../../src/components/Page';
describe(' ', () => {
it('renders children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
describe('name', () => {
it('passes its name', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('passes its name and avoids Symbol page conflict', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('passes otherProps', () => {
// @ts-ignore
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
================================================
FILE: __tests__/jest/components/RedBox.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { RedBox } from '../../../src/components/RedBox';
describe(' ', () => {
it('renders simple errors', () => {
const mockedError = new Error('THIS IS AN ERROR');
// override stack trace so that it's constant accross node versions
mockedError.stack = `Error: awdawd
at repl:1:13
at Script.runInThisContext (vm.js:65:33)
at REPLServer.defaultEval (repl.js:248:29)
at bound (domain.js:375:14)
at REPLServer.runBound [as eval] (domain.js:388:12)
at REPLServer.onLine (repl.js:501:10)
at REPLServer.emit (events.js:185:15)
at REPLServer.emit (domain.js:421:20)
at REPLServer.Interface._onLine (readline.js:285:10)
at REPLServer.Interface._line (readline.js:638:8)`;
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders string errors', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
================================================
FILE: __tests__/jest/components/Svg.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import Svg, { G, Path } from '../../../src/components/Svg';
describe(' ', () => {
it('passes its children', () => {
const tree = renderer
.create(
,
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('also works when child is directly imported', () => {
const tree = renderer.create(
,
);
expect(tree.toJSON()).toMatchSnapshot();
});
});
================================================
FILE: __tests__/jest/components/Text.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { Text } from '../../../src/components/Text';
import { StyleSheet } from '../../../src/stylesheet';
describe(' ', () => {
it('passes its children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
describe('name', () => {
it('passes its name', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('defaults to Text', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('style', () => {
const styles = StyleSheet.create({
view: {
flex: 1,
},
});
it('accepts a plain object', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts a StyleSheet ordinal', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts an array of plain objects and/or StyleSheet ordinals', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
================================================
FILE: __tests__/jest/components/View.tsx
================================================
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import { View } from '../../../src/components/View';
import { StyleSheet } from '../../../src/stylesheet';
describe(' ', () => {
it('passes its children', () => {
const tree = renderer.create(foo ).toJSON();
expect(tree).toMatchSnapshot();
});
describe('name', () => {
it('passes its name', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('defaults to View', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('style', () => {
const styles = StyleSheet.create({
view: {
flex: 1,
},
});
it('accepts a plain object', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts a StyleSheet ordinal', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('accepts an array of plain objects and/or StyleSheet ordinals', () => {
const tree = renderer.create( ).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
================================================
FILE: __tests__/jest/components/__snapshots__/Artboard.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` name defaults to Artboard 1`] = `
`;
exports[` name passes its name 1`] = `
`;
exports[` renders children 1`] = `
foo
`;
exports[` style accepts a StyleSheet ordinal 1`] = `
`;
exports[` style accepts a plain object 1`] = `
`;
exports[` style accepts an array of plain objects and/or StyleSheet ordinals 1`] = `
`;
exports[` style accepts artboard viewport preset 1`] = `
`;
================================================
FILE: __tests__/jest/components/__snapshots__/Document.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` renders children 1`] = `
foo
`;
================================================
FILE: __tests__/jest/components/__snapshots__/Image.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` name defaults to Image 1`] = `
`;
exports[` name passes its name 1`] = `
`;
exports[` renders children 1`] = `
foo
`;
exports[` resizeMode falls back to a resizeMode from style 1`] = `
`;
exports[` resizeMode falls back to cover 1`] = `
`;
exports[` resizeMode prefers prop to style 1`] = `
`;
exports[` resizeMode translates center 1`] = `
`;
exports[` resizeMode translates contain 1`] = `
`;
exports[` resizeMode translates cover 1`] = `
`;
exports[` resizeMode translates none 1`] = `
`;
exports[` resizeMode translates repeat 1`] = `
`;
exports[` resizeMode translates stretch 1`] = `
`;
exports[` source falls back to defaultSource if available 1`] = `
`;
exports[` source favors style over source for height 1`] = `
`;
exports[` source favors style over source for width 1`] = `
`;
exports[` source prefers source over defaultSource 1`] = `
`;
exports[` source sets height from source 1`] = `
`;
exports[` source sets width from source 1`] = `
`;
exports[` style accepts a StyleSheet ordinal 1`] = `
`;
exports[` style accepts a plain object 1`] = `
`;
exports[` style accepts an array of plain objects and/or StyleSheet ordinals 1`] = `
`;
================================================
FILE: __tests__/jest/components/__snapshots__/Page.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` name passes its name 1`] = `
`;
exports[` name passes its name and avoids Symbol page conflict 1`] = `
`;
exports[` name passes otherProps 1`] = `
`;
exports[` renders children 1`] = `
foo
`;
================================================
FILE: __tests__/jest/components/__snapshots__/RedBox.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` renders simple errors 1`] = `
Error: THIS IS AN ERROR
Script.runInThisContext
REPLServer.defaultEval
bound
REPLServer.runBound [as eval]
REPLServer.onLine
REPLServer.emit
REPLServer.emit
REPLServer.Interface._onLine
REPLServer.Interface._line
`;
exports[` renders string errors 1`] = `
Error: String only error
`;
================================================
FILE: __tests__/jest/components/__snapshots__/Svg.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` also works when child is directly imported 1`] = `
`;
exports[` passes its children 1`] = `
`;
================================================
FILE: __tests__/jest/components/__snapshots__/Text.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` name defaults to Text 1`] = ` `;
exports[` name passes its name 1`] = `
`;
exports[` passes its children 1`] = `
foo
`;
exports[` style accepts a StyleSheet ordinal 1`] = `
`;
exports[` style accepts a plain object 1`] = `
`;
exports[` style accepts an array of plain objects and/or StyleSheet ordinals 1`] = `
`;
================================================
FILE: __tests__/jest/components/__snapshots__/View.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` name defaults to View 1`] = `
`;
exports[` name passes its name 1`] = `
`;
exports[` passes its children 1`] = `
foo
`;
exports[` style accepts a StyleSheet ordinal 1`] = `
`;
exports[` style accepts a plain object 1`] = `
`;
exports[` style accepts an array of plain objects and/or StyleSheet ordinals 1`] = `
`;
================================================
FILE: __tests__/jest/components/nodeImpl/Svg.tsx
================================================
import * as React from 'react';
import * as ReactSketch from '../../../../src';
import Svg from '../../../../src/components/Svg';
jest.mock('../../../../src/jsonUtils/models', () => ({
...require.requireActual('../../../../src/jsonUtils/models'),
generateID: jest.fn((seed) => (seed ? `${seed}mockID` : 'mockID')),
}));
describe('node ', () => {
it('generates the json for an svg', () => {
class SVGElement extends React.Component {
render() {
return (
);
}
}
expect(ReactSketch.renderToJSON( )).toMatchSnapshot();
});
});
================================================
FILE: __tests__/jest/components/nodeImpl/__snapshots__/Svg.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`node generates the json for an svg 1`] = `
Object {
"_class": "group",
"booleanOperation": -1,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 447,
"width": 494,
"x": 0,
"y": 0,
},
"hasClickThrough": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 2,
"layers": Array [
Object {
"_class": "group",
"booleanOperation": -1,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 447,
"width": 494,
"x": 0,
"y": 0,
},
"hasClickThrough": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 2,
"layers": Array [
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 447,
"width": 494,
"x": 0,
"y": 0,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 447,
"width": 494,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 1}",
"curveMode": 1,
"curveTo": "{0.5, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 0.3579418344519016}",
"curveMode": 1,
"curveTo": "{0, 0.3579418344519016}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 0.3579418344519016}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.2165991902834008, 0.03355704697986577}",
"curveMode": 1,
"curveTo": "{0.2165991902834008, 0.03355704697986577}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.2165991902834008, 0.03355704697986577}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 0}",
"curveMode": 1,
"curveTo": "{0.5, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.7834008097165992, 0.03355704697986577}",
"curveMode": 1,
"curveTo": "{0.7834008097165992, 0.03355704697986577}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.7834008097165992, 0.03355704697986577}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 0.3579418344519016}",
"curveMode": 1,
"curveTo": "{1, 0.3579418344519016}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 0.3579418344519016}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0.6823529411764706,
"red": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 287,
"width": 494,
"x": 0,
"y": 160,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 287,
"width": 494,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 1}",
"curveMode": 1,
"curveTo": "{0.5, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 0}",
"curveMode": 1,
"curveTo": "{0, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 0}",
"curveMode": 1,
"curveTo": "{1, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 0}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0.4235294117647059,
"red": 0.9254901960784314,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 287,
"width": 294,
"x": 100,
"y": 160,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 287,
"width": 294,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 1}",
"curveMode": 1,
"curveTo": "{0.5, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 0}",
"curveMode": 1,
"curveTo": "{0, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 0}",
"curveMode": 1,
"curveTo": "{1, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 0}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0.6823529411764706,
"red": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 160,
"width": 294,
"x": 100,
"y": 0,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 160,
"width": 294,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 0}",
"curveMode": 1,
"curveTo": "{0.5, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 1}",
"curveMode": 1,
"curveTo": "{0, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 1}",
"curveMode": 1,
"curveTo": "{1, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 1}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0.7058823529411765,
"green": 0.9372549019607843,
"red": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 145,
"width": 494,
"x": 0,
"y": 15,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 145,
"width": 494,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.2165991902834008, 0}",
"curveMode": 1,
"curveTo": "{0.2165991902834008, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.2165991902834008, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.10526315789473684, 0.503448275862069}",
"curveMode": 1,
"curveTo": "{0.10526315789473684, 0.503448275862069}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.10526315789473684, 0.503448275862069}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 1}",
"curveMode": 1,
"curveTo": "{0, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.20445344129554655, 1}",
"curveMode": 1,
"curveTo": "{0.20445344129554655, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.20445344129554655, 1}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 145,
"width": 494,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.7834008097165992, 0}",
"curveMode": 1,
"curveTo": "{0.7834008097165992, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.7834008097165992, 0}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.8947368421052632, 0.503448275862069}",
"curveMode": 1,
"curveTo": "{0.8947368421052632, 0.503448275862069}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.8947368421052632, 0.503448275862069}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 1}",
"curveMode": 1,
"curveTo": "{1, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.7955465587044535, 1}",
"curveMode": 1,
"curveTo": "{0.7955465587044535, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.7955465587044535, 1}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0.6823529411764706,
"red": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
Object {
"_class": "shapeGroup",
"booleanOperation": -1,
"clippingMaskMode": 0,
"do_objectID": "mockID",
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 160,
"width": 294,
"x": 100,
"y": 0,
},
"hasClickThrough": false,
"hasClippingMask": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"layers": Array [
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 160,
"width": 294,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.023809523809523808, 0.09375}",
"curveMode": 1,
"curveTo": "{0.023809523809523808, 0.09375}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.023809523809523808, 0.09375}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0, 1}",
"curveMode": 1,
"curveTo": "{0, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 0}",
"curveMode": 1,
"curveTo": "{0.5, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 0}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
Object {
"_class": "shapePath",
"booleanOperation": -1,
"do_objectID": "mockID",
"edited": false,
"exportOptions": Object {
"_class": "exportOptions",
"exportFormats": Array [],
"includedLayerIds": Array [],
"layerOptions": 0,
"shouldTrim": false,
},
"frame": Object {
"_class": "rect",
"constrainProportions": false,
"height": 160,
"width": 294,
"x": 0,
"y": 0,
},
"isClosed": false,
"isFixedToViewport": false,
"isFlippedHorizontal": false,
"isFlippedVertical": false,
"isLocked": false,
"isVisible": true,
"layerListExpandedType": 0,
"name": "Path",
"nameIsFixed": false,
"pointRadiusBehaviour": 1,
"points": Array [
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.9761904761904762, 0.09375}",
"curveMode": 1,
"curveTo": "{0.9761904761904762, 0.09375}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.9761904761904762, 0.09375}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{1, 1}",
"curveMode": 1,
"curveTo": "{1, 1}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{1, 1}",
},
Object {
"_class": "curvePoint",
"cornerRadius": 0,
"curveFrom": "{0.5, 0}",
"curveMode": 1,
"curveTo": "{0.5, 0}",
"hasCurveFrom": false,
"hasCurveTo": false,
"point": "{0.5, 0}",
},
],
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
},
],
"name": "ShapeGroup",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"endMarkerType": 0,
"fills": Array [
Object {
"_class": "fill",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0.0196078431372549,
"green": 0.8274509803921568,
"red": 0.996078431372549,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"fillType": 0,
"gradient": Object {
"_class": "gradient",
"elipseLength": 0,
"from": "{0.5, 0}",
"gradientType": 0,
"stops": Array [
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 1,
"red": 1,
},
"position": 0,
},
Object {
"_class": "gradientStop",
"color": Object {
"_class": "color",
"alpha": 1,
"blue": 0,
"green": 0,
"red": 0,
},
"position": 1,
},
],
"to": "{0.5, 1}",
},
"isEnabled": true,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1,
},
],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
"windingRule": 1,
},
],
"name": "Shape",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"endMarkerType": 0,
"fills": Array [],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
},
],
"name": "Svg",
"nameIsFixed": false,
"resizingConstraint": 63,
"resizingType": 0,
"rotation": 0,
"shouldBreakMaskChain": false,
"style": Object {
"_class": "style",
"borderOptions": Object {
"_class": "borderOptions",
"dashPattern": Array [],
"isEnabled": false,
"lineCapStyle": 0,
"lineJoinStyle": 0,
},
"colorControls": Object {
"_class": "colorControls",
"brightness": 1,
"contrast": 1,
"hue": 1,
"isEnabled": false,
"saturation": 1,
},
"contextSettings": Object {
"_class": "graphicsContextSettings",
"blendMode": 0,
"opacity": 1,
},
"endMarkerType": 0,
"fills": Array [],
"innerShadows": Array [],
"miterLimit": 10,
"shadows": Array [],
"startMarkerType": 0,
"windingRule": 1,
},
}
`;
================================================
FILE: __tests__/jest/index.ts
================================================
import * as ReactSketch from '../../src';
describe('public API', () => {
it('exports render', () => {
expect(ReactSketch.render).toBeDefined();
});
it('exports renderToJSON', () => {
expect(ReactSketch.renderToJSON).toBeDefined();
});
it('exports StyleSheet', () => {
expect(ReactSketch.StyleSheet).toBeDefined();
});
it('exports Document', () => {
expect(ReactSketch.Document).toBeDefined();
});
it('exports Page', () => {
expect(ReactSketch.Page).toBeDefined();
});
it('exports Artboard', () => {
expect(ReactSketch.Artboard).toBeDefined();
});
it('exports Image', () => {
expect(ReactSketch.Image).toBeDefined();
});
it('exports RedBox', () => {
expect(ReactSketch.RedBox).toBeDefined();
});
it('exports Text', () => {
expect(ReactSketch.Text).toBeDefined();
});
it('exports TextStyles', () => {
expect(ReactSketch.TextStyles).toBeDefined();
});
it('exports View', () => {
expect(ReactSketch.View).toBeDefined();
});
it('exports Platform', () => {
expect(ReactSketch.Platform).toBeDefined();
});
});
================================================
FILE: __tests__/jest/jsonUtils/computeTextTree.ts
================================================
import { computeTextTree } from '../../../src/jsonUtils/computeTextTree';
import { Context } from '../../../src/utils/Context';
// Example Text component tree
const treeStub = {
type: 'text',
props: {
name: 'Swatch Hex',
style: {
color: '#636464',
zIndex: 1,
},
},
children: [
'#F3F4F4',
' ',
{
type: 'text',
props: {
name: 'Text',
style: {
color: 'blue',
},
},
children: ['Hello World'],
},
],
};
// Correct Output
const treeFixture = [
{ content: '#F3F4F4', textStyles: {} },
{ content: ' ', textStyles: {} },
{ content: 'Hello World', textStyles: { color: 'blue' } },
];
describe('Compute Text Tree', () => {
it('correctly handle Text nodes', () => {
const tree = computeTextTree(treeStub, new Context());
expect(tree).toEqual(treeFixture);
});
});
================================================
FILE: __tests__/jest/jsonUtils/computeYogaNode.ts
================================================
import yoga from 'yoga-layout-prebuilt';
import { computeYogaNode } from '../../../src/jsonUtils/computeYogaNode';
import { Context } from '../../../src/utils/Context';
import bridge from '../../../src/platformBridges/macos';
const widthAndHeightStylesStub = {
width: 10,
height: 10,
};
const widthAndHeightStylesStubFixture = {
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 10,
height: 10,
};
const createTreeNode = (style: { [key: string]: number | string }) => ({
type: 'foo',
props: {
style,
},
children: [],
});
const createYogaNodes = (
styles: Array<{ [key: string]: number | string }>,
containerWidth?: number,
containerHeight?: number,
) => {
const yogaNodes = [];
styles.forEach((style) => {
const treeNode = createTreeNode(style);
const ctx = new Context();
const { node } = computeYogaNode(bridge)(treeNode, ctx);
node.calculateLayout(
containerWidth || undefined,
containerHeight || undefined,
yoga.DIRECTION_LTR,
);
yogaNodes.push(node.getComputedLayout());
});
return yogaNodes;
};
describe('Compute Yoga Node', () => {
it('correctly handles width: 0, auto, number', () => {
const stylesToTest = [{ width: 100 }, { width: 0 }, { width: 'auto' }];
const [numberNode, noneNode, autoNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(100);
expect(noneNode.width).toEqual(0);
expect(autoNode.width).toEqual(0);
});
it('correctly handles height: 0, auto, number', () => {
const stylesToTest = [{ height: 100 }, { height: 0 }, { height: 'auto' }];
const [numberNode, noneNode, autoNode] = createYogaNodes(stylesToTest);
expect(numberNode.height).toEqual(100);
expect(noneNode.height).toEqual(0);
expect(autoNode.height).toEqual(0);
});
it('correctly handles min-height: 0 & number', () => {
const stylesToTest = [{ minHeight: 100 }, { minHeight: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.height).toEqual(100);
expect(noneNode.height).toEqual(0);
});
it('correctly handles max-height: 0 & number', () => {
const stylesToTest = [{ height: '100%', maxHeight: 100 }, { maxHeight: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest, 500, 500);
expect(numberNode.height).toEqual(100);
expect(noneNode.height).toEqual(0);
});
it('correctly handles min-width: 0 & number', () => {
const stylesToTest = [{ minWidth: 100 }, { minWidth: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(100);
expect(noneNode.width).toEqual(0);
});
it('correctly handles max-width: 0 & number', () => {
const stylesToTest = [{ width: '100%', maxWidth: 100 }, { maxWidth: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest, 500, 500);
expect(numberNode.width).toEqual(100);
expect(noneNode.width).toEqual(0);
});
it('correctly handles margin', () => {
const stylesToTest = [{ margin: 100 }, { margin: 0 }, { margin: 'auto' }];
const [numberNode, noneNode, autoNode] = createYogaNodes(stylesToTest);
expect(numberNode).toEqual({
left: 100,
right: 100,
top: 100,
bottom: 100,
width: 0,
height: 0,
});
expect(noneNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 0,
height: 0,
});
expect(autoNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 0,
height: 0,
});
});
it('correctly handles padding', () => {
const stylesToTest = [{ padding: 100 }, { padding: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 200,
height: 200,
});
expect(noneNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 0,
height: 0,
});
});
it('correctly handles border', () => {
const stylesToTest = [{ borderWidth: 10 }, { borderWidth: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 20,
height: 20,
});
expect(noneNode).toEqual({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 0,
height: 0,
});
});
it('correctly handles flex: 0, number', () => {
const stylesToTest = [{ flex: 1 }, { flex: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(0);
expect(noneNode.width).toEqual(0);
});
it('correctly handles flexGrow: 0, number', () => {
const stylesToTest = [{ flexGrow: 1 }, { flexGrow: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(0);
expect(noneNode.width).toEqual(0);
});
it('correctly handles flexShrink: 0, number', () => {
const stylesToTest = [{ flexShrink: 1 }, { flexShrink: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(0);
expect(noneNode.width).toEqual(0);
});
it('correctly handles flexBasis: 0, number', () => {
const stylesToTest = [{ flexBasis: 1 }, { flexBasis: 0 }];
const [numberNode, noneNode] = createYogaNodes(stylesToTest);
expect(numberNode.width).toEqual(0);
expect(noneNode.width).toEqual(0);
});
it('correctly handles position: relative & absolute', () => {
const stylesToTest = [
{ position: 'relative', left: 10 },
{ position: 'absolute', top: 10 },
];
const [relativeNode, absoluteNode] = createYogaNodes(stylesToTest);
expect(relativeNode).toEqual({
left: 10,
right: 10,
top: 0,
bottom: 0,
width: 0,
height: 0,
});
expect(absoluteNode).toEqual({
left: 0,
right: 0,
top: 10,
bottom: 10,
width: 0,
height: 0,
});
});
it('correctly handles display: flex & none', () => {
const stylesToTest = [
{ display: 'flex', ...widthAndHeightStylesStub },
{ display: 'none', width: 10, height: 10 },
];
const [relativeNode, absoluteNode] = createYogaNodes(stylesToTest);
expect(relativeNode).toEqual(widthAndHeightStylesStubFixture);
expect(absoluteNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles overflow: visible, scroll, hidden', () => {
const stylesToTest = [
{ overflow: 'visible', ...widthAndHeightStylesStub },
{ overflow: 'scroll', ...widthAndHeightStylesStub },
{ overflow: 'hidden', ...widthAndHeightStylesStub },
];
const [visibleNode, scrollNode, hiddenNode] = createYogaNodes(stylesToTest);
expect(visibleNode).toEqual(widthAndHeightStylesStubFixture);
expect(scrollNode).toEqual(widthAndHeightStylesStubFixture);
expect(hiddenNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles flexDirection', () => {
const stylesToTest = [
{ flexDirection: 'row', ...widthAndHeightStylesStub },
{ flexDirection: 'column', ...widthAndHeightStylesStub },
{ flexDirection: 'row-reverse', ...widthAndHeightStylesStub },
{ flexDirection: 'column-reverse', ...widthAndHeightStylesStub },
];
const [rowNode, colNode, rowReverseNode, colReverseNode] = createYogaNodes(stylesToTest);
expect(rowNode).toEqual(widthAndHeightStylesStubFixture);
expect(colNode).toEqual(widthAndHeightStylesStubFixture);
expect(rowReverseNode).toEqual(widthAndHeightStylesStubFixture);
expect(colReverseNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles justifyContent', () => {
const stylesToTest = [
{ justifyContent: 'flex-start', ...widthAndHeightStylesStub },
{ justifyContent: 'flex-end', ...widthAndHeightStylesStub },
{ justifyContent: 'center', ...widthAndHeightStylesStub },
{ justifyContent: 'space-between', ...widthAndHeightStylesStub },
{ justifyContent: 'space-around', ...widthAndHeightStylesStub },
];
const [startNode, endNode, centerNode, spaceBetweenNode, spaceAroundNode] = createYogaNodes(
stylesToTest,
);
expect(startNode).toEqual(widthAndHeightStylesStubFixture);
expect(endNode).toEqual(widthAndHeightStylesStubFixture);
expect(centerNode).toEqual(widthAndHeightStylesStubFixture);
expect(spaceBetweenNode).toEqual(widthAndHeightStylesStubFixture);
expect(spaceAroundNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles alignContent', () => {
const stylesToTest = [
{ alignContent: 'flex-start', ...widthAndHeightStylesStub },
{ alignContent: 'flex-end', ...widthAndHeightStylesStub },
{ alignContent: 'center', ...widthAndHeightStylesStub },
{ alignContent: 'stretch', ...widthAndHeightStylesStub },
{ alignContent: 'baseline', ...widthAndHeightStylesStub },
{ alignContent: 'space-between', ...widthAndHeightStylesStub },
{ alignContent: 'space-around', ...widthAndHeightStylesStub },
{ alignContent: 'auto', ...widthAndHeightStylesStub },
];
const [
startNode,
endNode,
centerNode,
stretchNode,
baselineNode,
spaceBetweenNode,
spaceAroundNode,
autoNode,
] = createYogaNodes(stylesToTest);
expect(startNode).toEqual(widthAndHeightStylesStubFixture);
expect(endNode).toEqual(widthAndHeightStylesStubFixture);
expect(centerNode).toEqual(widthAndHeightStylesStubFixture);
expect(stretchNode).toEqual(widthAndHeightStylesStubFixture);
expect(baselineNode).toEqual(widthAndHeightStylesStubFixture);
expect(spaceBetweenNode).toEqual(widthAndHeightStylesStubFixture);
expect(spaceAroundNode).toEqual(widthAndHeightStylesStubFixture);
expect(autoNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles alignItems', () => {
const stylesToTest = [
{ alignItems: 'flex-start', ...widthAndHeightStylesStub },
{ alignItems: 'flex-end', ...widthAndHeightStylesStub },
{ alignItems: 'center', ...widthAndHeightStylesStub },
{ alignItems: 'stretch', ...widthAndHeightStylesStub },
{ alignItems: 'baseline', ...widthAndHeightStylesStub },
];
const [startNode, endNode, centerNode, stretchNode, baselineNode] = createYogaNodes(
stylesToTest,
);
expect(startNode).toEqual(widthAndHeightStylesStubFixture);
expect(endNode).toEqual(widthAndHeightStylesStubFixture);
expect(centerNode).toEqual(widthAndHeightStylesStubFixture);
expect(stretchNode).toEqual(widthAndHeightStylesStubFixture);
expect(baselineNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles alignSelf', () => {
const stylesToTest = [
{ alignSelf: 'flex-start', ...widthAndHeightStylesStub },
{ alignSelf: 'flex-end', ...widthAndHeightStylesStub },
{ alignSelf: 'center', ...widthAndHeightStylesStub },
{ alignSelf: 'stretch', ...widthAndHeightStylesStub },
{ alignSelf: 'baseline', ...widthAndHeightStylesStub },
];
const [startNode, endNode, centerNode, stretchNode, baselineNode] = createYogaNodes(
stylesToTest,
);
expect(startNode).toEqual(widthAndHeightStylesStubFixture);
expect(endNode).toEqual(widthAndHeightStylesStubFixture);
expect(centerNode).toEqual(widthAndHeightStylesStubFixture);
expect(stretchNode).toEqual(widthAndHeightStylesStubFixture);
expect(baselineNode).toEqual(widthAndHeightStylesStubFixture);
});
it('correctly handles flexWrap', () => {
const stylesToTest = [
{ flexWrap: 'no-wrap', ...widthAndHeightStylesStub },
{ flexWrap: 'wrap', ...widthAndHeightStylesStub },
{ flexWrap: 'wrap-reverse', ...widthAndHeightStylesStub },
];
const [noWrapNode, wrapNode, wrapReverseNode] = createYogaNodes(stylesToTest);
expect(noWrapNode).toEqual(widthAndHeightStylesStubFixture);
expect(wrapNode).toEqual(widthAndHeightStylesStubFixture);
expect(wrapReverseNode).toEqual(widthAndHeightStylesStubFixture);
});
});
================================================
FILE: __tests__/jest/jsonUtils/computeYogaTree.ts
================================================
import yoga from 'yoga-layout-prebuilt';
import { computeYogaTree } from '../../../src/jsonUtils/computeYogaTree';
import { Context } from '../../../src/utils/Context';
import bridge from '../../../src/platformBridges/macos';
const treeRootStub = {
type: 'artboard',
props: {
style: {
flexDirection: 'row',
flexWrap: 'wrap',
width: 416,
},
name: 'Swatches',
},
children: [
{
type: 'view',
props: {
name: 'Swatch Haus',
style: {
backgroundColor: '#F3F4F4',
height: 96,
marginTop: 4,
marginRight: 4,
marginBottom: 4,
marginLeft: 4,
paddingTop: 8,
paddingRight: 8,
paddingBottom: 8,
paddingLeft: 8,
width: 96,
},
},
children: [],
},
],
};
computeYogaTree(bridge)(treeRootStub, new Context());
describe('Compute Yoga Tree', () => {
it('correctly create yoga nodes into layout tree', () => {
const yogaTree = computeYogaTree(bridge)(treeRootStub, new Context());
yogaTree.calculateLayout(undefined, undefined, yoga.DIRECTION_LTR);
expect(yogaTree.getComputedLayout()).toEqual({
bottom: 0,
height: 104,
left: 0,
right: 0,
top: 0,
width: 416,
});
expect(yogaTree.getChild(0).getComputedLayout()).toEqual({
bottom: 4,
height: 96,
left: 4,
right: 4,
top: 4,
width: 96,
});
});
});
================================================
FILE: __tests__/jest/jsonUtils/layerGroup.ts
================================================
import { layerGroup } from '../../../src/jsonUtils/layerGroup';
describe('layer group', () => {
it('is correctly constructed', () => {
const group = layerGroup(100, 200, 300, 400, 0.5);
expect(group).toHaveProperty('frame.x', 100);
expect(group).toHaveProperty('frame.y', 200);
expect(group).toHaveProperty('frame.width', 300);
expect(group).toHaveProperty('frame.height', 400);
expect(group).toHaveProperty('style.contextSettings.opacity', 0.5);
});
});
================================================
FILE: __tests__/jest/jsonUtils/models.ts
================================================
import {
generateID,
makeColorFromCSS,
makeColorFill,
makeRect,
makeSymbolInstance,
makeSymbolMaster,
} from '../../../src/jsonUtils/models';
describe('generateID', () => {
it('is unique', () => {
expect(generateID()).not.toBe(generateID());
});
it('seed generates different ID', () => {
expect(generateID('test')).not.toBe(generateID('test'));
});
it('hardcoded seed generates same ID', () => {
expect(generateID('test', true)).toBe(generateID('test', true));
});
});
const BLACK = {
_class: 'color',
red: 0,
green: 0,
blue: 0,
alpha: 1,
};
const WHITE = {
_class: 'color',
red: 1,
green: 1,
blue: 1,
alpha: 1,
};
const GOLD = {
_class: 'color',
red: 0.8745098039215686,
green: 0.7294117647058823,
blue: 0.4117647058823529,
alpha: 1,
};
const PURPLE = {
_class: 'color',
red: 0.4,
green: 0.2,
blue: 0.6,
alpha: 1,
};
describe('makeColorFromCSS', () => {
it('works with hex colors', () => {
expect(makeColorFromCSS('#000')).toEqual(BLACK);
expect(makeColorFromCSS('#000000')).toEqual(BLACK);
expect(makeColorFromCSS('#FFF')).toEqual(WHITE);
expect(makeColorFromCSS('#FFFFFF')).toEqual(WHITE);
expect(makeColorFromCSS('#DFBA69')).toEqual(GOLD);
});
it('works with named colors', () => {
expect(makeColorFromCSS('black')).toEqual(BLACK);
expect(makeColorFromCSS('white')).toEqual(WHITE);
expect(makeColorFromCSS('rebeccapurple')).toEqual(PURPLE);
});
it('is case-insensitive', () => {
expect(makeColorFromCSS('BLACK')).toEqual(BLACK);
expect(makeColorFromCSS('wHIte')).toEqual(WHITE);
expect(makeColorFromCSS('rebeccaPurple')).toEqual(PURPLE);
});
it('works with rgb colors', () => {
expect(makeColorFromCSS('rgb(0, 0, 0)')).toEqual(BLACK);
expect(makeColorFromCSS('rgb(255, 255, 255)')).toEqual(WHITE);
expect(makeColorFromCSS('rgb(102, 51, 153)')).toEqual(PURPLE);
});
it('works with rgba colors', () => {
expect(makeColorFromCSS('rgba(0, 0, 0, 1)')).toEqual(BLACK);
expect(makeColorFromCSS('rgba(255, 255, 255, 1)')).toEqual(WHITE);
expect(makeColorFromCSS('rgba(102, 51, 153, 1)')).toEqual(PURPLE);
});
it('multiplies rgba components with an alpha', () => {
expect(makeColorFromCSS('rgba(0, 0, 0, 0.5)').alpha).toBeCloseTo(0.5);
expect(makeColorFromCSS('rgba(0, 0, 0, 1)', 0.5).alpha).toBeCloseTo(0.5);
expect(makeColorFromCSS('rgba(0, 0, 0, 0.5)', 0.5).alpha).toBeCloseTo(0.25);
});
it('works with hsl colors', () => {
expect(makeColorFromCSS('hsl(0, 0%, 0%)')).toEqual(BLACK);
expect(makeColorFromCSS('hsl(0, 0%, 100%)')).toEqual(WHITE);
});
it('works with hsla colors', () => {
expect(makeColorFromCSS('hsla(0, 0%, 0%, 1)')).toEqual(BLACK);
expect(makeColorFromCSS('hsla(0, 0%, 100%, 1)')).toEqual(WHITE);
});
});
describe('makeColorFill', () => {
it('sets the correct color', () => {
expect(makeColorFill('#000')).toHaveProperty('color', BLACK);
expect(makeColorFill('#fff')).toHaveProperty('color', WHITE);
expect(makeColorFill('rebeccapurple')).toHaveProperty('color', PURPLE);
expect(makeColorFill('#DFBA69')).toHaveProperty('color', GOLD);
});
});
describe('makeRect', () => {
it('is correctly constructed', () => {
const group = makeRect(100, 200, 300, 400);
expect(group).toHaveProperty('x', 100);
expect(group).toHaveProperty('y', 200);
expect(group).toHaveProperty('width', 300);
expect(group).toHaveProperty('height', 400);
});
});
describe('makeSymbolInstance', () => {
it('is correctly constructed', () => {
const instance = makeSymbolInstance(
makeRect(0, 0, 100, 100),
'this is the symbol id',
'this is the name',
);
expect(instance).toHaveProperty('symbolID', 'this is the symbol id');
expect(instance).toHaveProperty('name', 'this is the name');
});
});
describe('makeSymbolMaster', () => {
it('is correctly constructed', () => {
const master = makeSymbolMaster(
makeRect(0, 0, 100, 100),
'this is the symbol id',
'this is the name',
);
expect(master).toHaveProperty('symbolID', 'this is the symbol id');
expect(master).toHaveProperty('name', 'this is the name');
});
});
================================================
FILE: __tests__/jest/jsonUtils/shapeLayers.ts
================================================
import {
makeRectPath,
makeShapePath,
makeRectShapeLayer,
makeShapeGroup,
} from '../../../src/jsonUtils/shapeLayers';
describe('makeRectPath', () => {
it('is correctly constructed', () => {
const path = makeRectPath([10, 20, 30, 40]);
expect(path.points[0]).toHaveProperty('cornerRadius', 10);
expect(path.points[1]).toHaveProperty('cornerRadius', 20);
expect(path.points[2]).toHaveProperty('cornerRadius', 30);
expect(path.points[3]).toHaveProperty('cornerRadius', 40);
});
});
describe('makeShapePath', () => {
it('is correctly constructed', () => {
const frame = { foo: 'bar' };
const path = { baz: 'qux' };
// @ts-ignore
const shapePath = makeShapePath(frame, path);
expect(shapePath).toHaveProperty('frame', frame);
expect(shapePath).toHaveProperty('baz', 'qux');
});
});
describe('makeRectShapeLayer', () => {
it('is correctly constructed', () => {
const shapeLayer = makeRectShapeLayer(100, 200, 300, 400, [10, 20, 30, 40]);
expect(shapeLayer).toHaveProperty('frame.x', 100);
expect(shapeLayer).toHaveProperty('frame.y', 200);
expect(shapeLayer).toHaveProperty('frame.width', 300);
expect(shapeLayer).toHaveProperty('frame.height', 400);
expect(shapeLayer).toHaveProperty('fixedRadius', 10);
});
});
describe('makeShapeGroup', () => {
it('is correctly constructed', () => {
const frame = { foo: 'bar' };
const layers = [{ baz: 'qux' }];
const fills = ['foo', 'bar'];
// @ts-ignore
const shapeGroup = makeShapeGroup(frame, layers, undefined, undefined, fills);
expect(shapeGroup).toHaveProperty('frame', frame);
expect(shapeGroup).toHaveProperty('layers', layers);
expect(shapeGroup).toHaveProperty('style.fills', fills);
});
});
================================================
FILE: __tests__/jest/jsonUtils/style.ts
================================================
import { makeBorderOptions, makeShadow } from '../../../src/jsonUtils/style';
describe('makeBorderOptions', () => {
it('makes solid borders', () => {
expect(makeBorderOptions('solid', 1)).toHaveProperty('dashPattern', []);
});
it('makes dotted borders', () => {
expect(makeBorderOptions('dotted', 1)).toHaveProperty('dashPattern', [1, 1]);
expect(makeBorderOptions('dotted', 5)).toHaveProperty('dashPattern', [5, 5]);
});
it('makes dashed borders', () => {
expect(makeBorderOptions('dashed', 1)).toHaveProperty('dashPattern', [3, 3]);
expect(makeBorderOptions('dashed', 5)).toHaveProperty('dashPattern', [15, 15]);
});
});
describe('makeShadow', () => {
it('has sensible defaults', () => {
const result = makeShadow({});
expect(result).toHaveProperty('color.alpha', 1);
expect(result).toHaveProperty('blurRadius', 1);
expect(result).toHaveProperty('offsetX', 0);
expect(result).toHaveProperty('offsetY', 0);
});
it('passes through props', () => {
const result = makeShadow({
shadowOpacity: 0.5,
shadowColor: 'red',
shadowRadius: 10,
shadowOffset: {
width: 5,
height: 7,
},
});
expect(result).toHaveProperty('color.alpha', 0.5);
expect(result).toHaveProperty('blurRadius', 10);
expect(result).toHaveProperty('offsetX', 5);
expect(result).toHaveProperty('offsetY', 7);
});
it('combines rgba alpha & shadowOpacity', () => {
const result = makeShadow({
shadowOpacity: 0.5,
shadowColor: 'rgba(0,0,0,0.5)',
shadowRadius: 10,
shadowOffset: {
width: 5,
height: 7,
},
});
expect(result.color.alpha).toBeCloseTo(0.25);
});
});
================================================
FILE: __tests__/jest/reactTreeToFlexTree.ts
================================================
import yoga from 'yoga-layout-prebuilt';
import { computeYogaTree } from '../../src/jsonUtils/computeYogaTree';
import { Context } from '../../src/utils/Context';
import { reactTreeToFlexTree } from '../../src/buildTree';
import bridge from '../../src/platformBridges/macos';
const treeRootStub = {
type: 'artboard',
props: {
style: {
flexDirection: 'row',
flexWrap: 'wrap',
width: 416,
},
name: 'Swatches',
},
children: [
{
type: 'view',
props: {
name: 'Layer 1',
style: {
height: 100,
position: 'absolute',
width: 100,
zIndex: 1,
},
},
children: [],
},
{
type: 'view',
props: {
name: 'Layer 3',
style: {
height: 300,
position: 'absolute',
width: 300,
zIndex: 3,
},
},
children: [],
},
{
type: 'view',
props: {
name: 'Layer 2',
style: {
height: 200,
position: 'absolute',
width: 200,
zIndex: 2,
},
},
children: [],
},
],
};
describe('Compute Flex Tree', () => {
it('correctly creates flex tree', () => {
const yogaNode = computeYogaTree(bridge)(treeRootStub, new Context());
yogaNode.calculateLayout(undefined, undefined, yoga.DIRECTION_LTR);
const tree = reactTreeToFlexTree(treeRootStub, yogaNode, new Context());
expect(tree.children).toEqual([
{
type: 'view',
style: {
height: 100,
position: 'absolute',
width: 100,
zIndex: 1,
},
textStyle: {},
layout: {
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 100,
height: 100,
},
props: {
name: 'Layer 1',
style: {
height: 100,
position: 'absolute',
width: 100,
zIndex: 1,
},
textNodes: [],
},
children: [],
},
{
type: 'view',
style: {
height: 200,
position: 'absolute',
width: 200,
zIndex: 2,
},
textStyle: {},
layout: {
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 200,
height: 200,
},
props: {
name: 'Layer 2',
style: {
height: 200,
position: 'absolute',
width: 200,
zIndex: 2,
},
textNodes: [],
},
children: [],
},
{
type: 'view',
style: {
height: 300,
position: 'absolute',
width: 300,
zIndex: 3,
},
textStyle: {},
layout: {
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 300,
height: 300,
},
props: {
name: 'Layer 3',
style: {
height: 300,
position: 'absolute',
width: 300,
zIndex: 3,
},
textNodes: [],
},
children: [],
},
]);
});
});
================================================
FILE: __tests__/jest/sharedStyles/TextStyles.ts
================================================
import bridge from '../../../src/platformBridges/macos';
let TextStyles;
let doc;
let sharedTextStyles;
beforeEach(() => {
jest.resetModules();
jest.mock('../../../src/utils/getSketchVersion', () => ({
getSketchVersion: jest.fn(() => 51),
}));
TextStyles = require('../../../src/sharedStyles/TextStyles').TextStyles;
sharedTextStyles = require('../../../src/utils/sharedTextStyles');
jest.mock('../../../src/utils/sharedTextStyles');
TextStyles = TextStyles(() => bridge);
sharedTextStyles = sharedTextStyles.sharedTextStyles;
sharedTextStyles.setDocument = jest.fn((doc) => {
if (!doc) {
throw new Error('Please provide a sketch document reference');
}
});
sharedTextStyles.addStyle = jest.fn(() => 'styleId');
sharedTextStyles.setStyles = jest.fn(() => sharedTextStyles);
doc = jest.fn();
});
describe('create', () => {
describe('without a context', () => {
it('it errors', () => {
const styles = {};
expect(() => TextStyles.create({}, styles)).toThrowError(
/Please provide a sketch document reference/,
);
});
});
describe('with a context', () => {
it('clears clearExistingStyles when true', () => {
TextStyles.create(
{},
{
clearExistingStyles: true,
document: doc,
},
);
expect(sharedTextStyles.setStyles).toHaveBeenCalled();
});
it('doesn’t clearExistingStyles when false', () => {
TextStyles.create(
{},
{
clearExistingStyles: false,
document: doc,
},
);
expect(sharedTextStyles.setStyles).not.toHaveBeenCalled();
});
it('stores one style', () => {
const styles = {
foo: {
fontSize: 'bar',
},
};
const res = TextStyles.create(styles, { document: doc });
expect(Object.keys(res).length).toBe(1);
});
it('stores unique styles seperately', () => {
const styles = {
foo: {
fontSize: 'bar',
},
bar: {
fontSize: 'baz',
},
};
const res = TextStyles.create(styles, { document: doc });
expect(Object.keys(res).length).toBe(2);
expect(sharedTextStyles.addStyle).toHaveBeenCalledTimes(2);
});
it('merges duplicate styles', () => {
const styles = {
foo: {
fontSize: 'foo',
},
bar: {
fontSize: 'foo',
},
};
const res = TextStyles.create(styles, { document: doc });
expect(Object.keys(res).length).toBe(1);
expect(sharedTextStyles.addStyle).toHaveBeenCalledTimes(2);
});
it('only stores text attributes', () => {
const whitelist = [
'color',
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'textShadowOffset',
'textShadowRadius',
'textShadowColor',
'textTransform',
'letterSpacing',
'lineHeight',
'textAlign',
'writingDirection',
];
const blacklist = ['foo', 'bar', 'baz'];
const input = [...whitelist, ...blacklist].reduce(
(acc, key) => ({
...acc,
[key]: '',
}),
{},
);
const res = TextStyles.create({ foo: input }, { document: doc });
const firstStoredStyle = res[Object.keys(res)[0]].cssStyle;
whitelist.forEach((key) => {
expect(firstStoredStyle).toHaveProperty(key, '');
});
blacklist.forEach((key) => {
expect(firstStoredStyle).not.toHaveProperty(key);
});
});
});
});
describe('resolve', () => {
beforeEach(() => {
TextStyles.create({}, { document: doc });
});
it('retrieves a matching style', () => {
const key = 'foo';
const styles = {
[key]: { fontSize: 'bar' },
};
TextStyles.create(styles, { document: doc });
expect(TextStyles.resolve(styles[key])).toBeDefined();
expect(sharedTextStyles.addStyle).toHaveBeenCalledTimes(1);
});
it('returns null with no matching style', () => {
const key = 'foo';
const styles = {
[key]: {
fontSize: 'bar',
},
};
const style2 = {
fontSize: 'qux',
};
TextStyles.create(styles, { document: doc });
expect(TextStyles.resolve(style2)).not.toBeDefined();
expect(sharedTextStyles.addStyle).toHaveBeenCalledTimes(1);
});
});
describe('get', () => {
it('finds a matching registered style by name', () => {
const styles = {
foo: {
fontSize: 'bar',
},
bar: {
fontSize: 'baz',
},
};
TextStyles.create(styles, { document: doc });
expect(TextStyles.get('foo')).toEqual(styles.foo);
expect(TextStyles.get('baz')).toEqual(undefined);
});
it('returns undefined when not found', () => {
const styles = {
foo: {
fontSize: 'bar',
},
};
TextStyles.create(styles, { document: doc });
expect(TextStyles.get('baz')).toEqual(undefined);
});
});
describe('clear', () => {
it('clears previously registered styles', () => {
const styles = {
foo: {
fontSize: 'bar',
},
bar: {
fontSize: 'baz',
},
};
TextStyles.create(styles, { document: doc });
TextStyles.clear();
expect(TextStyles.styles()).toEqual({});
});
});
================================================
FILE: __tests__/jest/utils/isDefined.ts
================================================
import { isDefined } from '../../../src/utils/isDefined';
describe('isNullOrUndefined', () => {
it('correctly identify null', () => {
const shouldBeNull = isDefined(null);
expect(shouldBeNull).toEqual(false);
});
it('correctly identify undefined', () => {
const shouldBeUndefined = isDefined(undefined);
expect(shouldBeUndefined).toEqual(false);
});
it('correctly identify zero (0)', () => {
const shouldBeZero = isDefined(0);
expect(shouldBeZero).toEqual(true);
});
});
================================================
FILE: __tests__/jest/utils/sortObjectKeys.ts
================================================
import { sortObjectKeys } from '../../../src/utils/sortObjectKeys';
test('simple example', () => {
const a = {
foo: true,
bar: true,
qux: true,
baz: true,
};
const b = {
bar: true,
baz: true,
foo: true,
qux: true,
};
expect(sortObjectKeys(a)).toEqual(b);
});
================================================
FILE: __tests__/jest/utils/zIndex.ts
================================================
import { zIndex } from '../../../src/utils/zIndex';
const noZIndexNode = {
props: {
style: {
zIndex: 0,
},
},
};
const zIndexNode = {
props: {
style: {
zIndex: 1,
},
},
};
const fixureThatShouldBeSortedDifferently = [noZIndexNode, zIndexNode];
const fixureThatShouldNotBeSortedDifferently = [noZIndexNode, noZIndexNode];
describe('zIndex', () => {
it('correctly resort zIndex', () => {
// @ts-ignore
const shouldBeResorted = zIndex(fixureThatShouldBeSortedDifferently);
// @ts-ignore
expect(shouldBeResorted[0].props.style.zIndex).toEqual(0);
});
it('correctly add original index to returned objects ', () => {
// @ts-ignore
const shouldBeResorted = zIndex(fixureThatShouldBeSortedDifferently);
// @ts-ignore
expect(shouldBeResorted[0].oIndex).toEqual(0);
});
it('correctly resort zIndexes that are all the same', () => {
// @ts-ignore
const shouldNotBeResorted = zIndex(fixureThatShouldNotBeSortedDifferently);
// @ts-ignore
expect(shouldNotBeResorted[0].props.style.zIndex).toEqual(0);
});
});
================================================
FILE: __tests__/skpm/basic.test.js
================================================
import * as React from 'react';
import * as sketch from 'sketch';
import { render, View, Artboard, Text } from '../../lib';
// depending on where those tests run, we don't get the things,
// eg. the context might be empty or there is no selected document
// This make sure we always get something
function getDoc(context, document) {
return context.document || (sketch.getSelectedDocument() || document).sketchObject;
}
const colorList = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur Dark': '#24828F',
Peach: '#EFADA0',
'Peach Dark': '#E37059',
Pear: '#93DAAB',
'Pear Dark': '#2E854B',
};
test('should render a Page with a rectangle', (context, document) => {
const nativePage = getDoc(context, document).currentPage();
const Swatch = ({ name, hex }) => (
{name}
{hex}
);
render(
{Object.keys(colorList).map(color => (
))}
,
nativePage,
);
const page = sketch.Page.fromNative(nativePage);
expect(page.layers[0].name).toBe('Swatches');
});
================================================
FILE: __tests__/skpm/render-context.test.js
================================================
import * as React from 'react';
import * as sketch from 'sketch';
import { render, View, Text } from '../../lib';
// depending on where those tests run, we don't get the things,
// eg. the context might be empty or there is no selected document
// This make sure we always get something
function getDoc(document) {
return sketch.getSelectedDocument() || document;
}
test('should render a Page with context events', (context, document) => {
const { selectedPage } = getDoc(document);
const Swatch = ({ hex }) => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setCount(10);
}, [count]);
return (
Count is {count}
);
};
render( , selectedPage);
expect(selectedPage.layers[0].name).toBe('Count is 10');
});
================================================
FILE: __tests__/skpm/render-in-wrapped-object.test.js
================================================
import * as React from 'react';
import * as sketch from 'sketch';
import { render, View, Artboard, Text } from '../../lib';
// depending on where those tests run, we don't get the things,
// eg. the context might be empty or there is no selected document
// This make sure we always get something
function getDoc(document) {
return sketch.getSelectedDocument() || document;
}
const colorList = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur Dark': '#24828F',
Peach: '#EFADA0',
'Peach Dark': '#E37059',
Pear: '#93DAAB',
'Pear Dark': '#2E854B',
};
test('should render a Page with a rectangle', (context, document) => {
const { selectedPage } = getDoc(document);
const Swatch = ({ name, hex }) => (
{name}
{hex}
);
render(
{Object.keys(colorList).map(color => (
))}
,
selectedPage,
);
expect(selectedPage.layers[0].name).toBe('Swatches');
});
================================================
FILE: book.json
================================================
{
"gitbook": ">= 3.2.1",
"title": "react-sketchapp",
"plugins": [
"edit-link",
"prism",
"-highlight",
"github",
"-search",
"codeblock-disable-glossary",
"anchorjs"
],
"pluginsConfig": {
"edit-link": {
"base": "https://github.com/airbnb/react-sketchapp/tree/master",
"label": "Edit This Page"
},
"github": {
"url": "https://github.com/airbnb/react-sketchapp/"
}
}
}
================================================
FILE: docs/API.md
================================================
# API Reference
- [`render`](#renderelement-container)
- [`renderToJSON`](#rendertojsonelement)
- [Components](#components)
- [``](#document)
- [``](#page)
- [``](#artboard)
- [``](#image)
- [``](#redbox)
- [``](#svg)
- [``](#text)
- [``](#view)
- [`Hooks`](#hooks)
- [`useWindowDimensions`](#usewindowdimensions)
- [`Platform`](#platform)
- [`OS`](#os)
- [`Version`](#version)
- [`select`](#selectobj)
- [`StyleSheet`](#stylesheet)
- [`hairlineWidth`](#hairlinewidth)
- [`absoluteFill`](#absolutefill)
- [`create`](#createstyles)
- [`flatten`](#flattenstyles)
- [`resolve`](#resolvestyle)
- [`TextStyles`](#textstyles)
- [`create`](#createstyleoptionsstyles)
- [`resolve`](#resolvestyle)
- [`Symbols`](#symbols)
- [`makeSymbol`](#makesymbolnode-props-document)
### `render(element, container)`
Returns the top-level rendered Sketch object or an array of Sketch objects if you use `` components.
#### Parameters
##### `element` (required)
Top-level React component that defines your Sketch document.
Example:
```js
Hello World
```
##### `container` (optional)
The element to render into - will be replaced. Should either be a Sketch [Document](https://developer.sketchapp.com/reference/api/#document), Sketch [Group](https://developer.sketchapp.com/reference/api/#group) or Sketch [Page](https://developer.sketchapp.com/reference/api/#page) Object.
Example: `sketch.getSelectedDocument().selectedPage`.
#### Returns
The top-most rendered native Sketch layer.
#### Example
```js
import sketch from 'sketch';
import { View, Text, render } from 'react-sketchapp';
const Document = props => (
Hello world!
);
export default () => {
render( , sketch.getSelectedDocument().selectedPage);
};
```
### `renderToJSON(element)`
Returns a Sketch JSON object for further consumption - doesn't add to the page.
#### Parameters
##### `element` (required)
Top-level React component that defines your Sketch document.
#### Returns
The top-most Sketch layer as JSON.
## Components
### ``
Wrapper for Sketch's Documents. Must be used at the root of your application and is required if you would like to have multiple pages.
#### Props
| Prop | Type | Default | Note |
| ---------- | ------ | ------- | ---------------------------------------- |
| `children` | `Node` | | Can only be [``](#page) components |
#### Example
```js
Hello world!
Hello second world!!
```
### ``
Wrapper for Sketch's Pages. Requires a [``](#document) component as a parent if you would like to use multiple of these components.
#### Props
| Prop | Type | Default | Note |
| ---------- | -------- | ------- | ------------------------------------------------ |
| `name` | `String` | | The name to be displayed in the Sketch Page List |
| `children` | `Node` | | |
#### Example
```js
Hello world!
```
### ``
Wrapper for Sketch's Artboards. Requires a [``](#page) component as a parent if you would like to use multiple of these components.
#### Props
| Prop | Type | Default | Note |
| --- | --- | --- | --- |
| `name` | `String` | | The name to be displayed in the Sketch Layer List |
| `children` | `Node` | | |
| `style` | [`Style`](/docs/styling.md) | | |
| `viewport` | `Viewport` | | Object: { name: string, width: number, height: number, scale?: number, fontScale?: number } |
| `isHome` | `Boolean` | | Is prototype home screen if true |
The `scale` and `fontScale` attributes in the `viewport` prop are not used by Sketch, but can be used together with the [`useWindowDimensions`](#usewindowdimensions) hook for conditional styling/rendering.
#### Examples
Hello world with width of 480px.
```js
Hello world!
```
Mobile screen artboard with viewport preset (supports scrolling in prototypes).
```js
Hello world!
```
### ``
#### Props
| Prop | Type | Default | Note |
| ------------ | --------------------------- | --------- | ---- |
| `children` | `Node` | | |
| `source` | `ImageSource` | | |
| `style` | [`Style`](/docs/styling.md) | | |
| `resizeMode` | `ResizeMode` | `contain` | |
```js
type ImageSource = string | { src: string };
type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center' | 'repeat' | 'none';
```
#### Example
```js
```
### ``
A red box / 'red screen of death' error handler. Thanks to [commissure/redbox-react](https://github.com/commissure/redbox-react).
#### Props
| Prop | Type | Default | Note |
| --- | --- | --- | --- |
| `error` | [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) | **required** | A JavaScript Error object |
#### Example
```js
import sketch from 'sketch';
import { RedBox, render } from 'react-sketchapp';
export default () => {
const { selectedPage } = sketch.getSelectedDocument();
try {
render( , selectedPage);
} catch (err) {
render( , selectedPage);
}
};
```
### ``
SVG Interface to Sketch
The API is based on [`react-native-svg`](https://github.com/react-native-community/react-native-svg). See more information on [its README](https://github.com/react-native-community/react-native-svg#Usage).
#### Example
```js
import sketch from 'sketch';
import { Svg, render } from 'react-sketchapp';
export default () => {
render(
,
sketch.getSelectedDocument().selectedPage,
);
};
```
#### Direct imports
Additionally, to have a somewhat more compliant mode to the `react-native-svg` API, the SVG components might as well be imported directly:
```js
import sketch from 'sketch';
import { render } from 'react-sketchapp';
import Svg, { G, Path } from 'react-sketchapp/lib/components/Svg';
export default () => {
render(
,
sketch.getSelectedDocument().selectedPage,
);
};
```
### ``
Text primitives
#### Props
| Prop | Type | Default | Note |
| --- | --- | --- | --- |
| `name` | `String` | | The name to be displayed in the Sketch Layer List |
| `children` | `String` | | |
| `style` | [`Style`](/docs/styling.md) | | |
#### Example
```js
Hello World!
```
### ``
View primitives
#### Props
| Prop | Type | Default | Note |
| --- | --- | --- | --- |
| `name` | `String` | | The name to be displayed in the Sketch Layer List |
| `children` | `Node` | | |
| `style` | [`Style`](/docs/styling.md) | | |
| `flow` | `Flow` | | Object: { target: string, targetId: string, animationType: string } |
#### Examples
##### Example with children
```js
Hello World!
Hello World!
Hello World!
```
##### Example using `flow` prop for prototyping destination
```js
Open menu!
id */>
Go back!
```
## Hooks
### `useWindowDimensions()`
Returns the window dimensions of the parent ``. Returns `{ width: number, height: number, fontScale: number, scale: number }`.
#### Example
```js
import { Page, Artboard, View, Text, useWindowDimensions } from 'react-sketchapp';
const HomePage = () => {
const { height, width } = useWindowDimensions();
return (
Hello World
{(width >= 768) && (
You can only see this text on tablet/desktop
)}
);
};
const devices = [{
name: 'Mobile',
width: 360,
height: 640,
}, {
name: 'Tablet',
width: 768
height: 1024,
}, {
name: 'Desktop',
width: 1024
height: 1280,
}];
render(
{devices.map(viewport => (
))}
,
);
```
## Platform
### `OS`
`sketch`
### `Version`
`1`
### `select(obj)`
#### Parameters
##### `obj`
## StyleSheet
Compared to single-use `style` objects, `StyleSheets` enable creation of re-usable, optimized style references.
### `hairlineWidth`
The platform's global 'hairline width'.
### `absoluteFill`
A constant 'absolute fill' style.
### `create(styles)`
Create an optimized `StyleSheet` reference from a style object.
#### Parameters
##### `styles`
#### Example
```js
const styles = StyleSheet.create({
foo: {
fontSize: 24,
color: 'red',
},
bar: {
fontSize: 36,
color: 'blue',
},
});
// { foo: 1, bar: 2 }
;
```
### `flatten(styles)`
Flatten an array of style objects into one aggregated object, **or** look up the definition for a registered stylesheet.
#### Parameters
##### `styles`
#### Example
```js
const styles = StyleSheet.create({
foo: {
fontSize: 24,
color: 'red',
},
bar: {
backgroundColor: 'blue',
lineHeight: 36,
},
});
StyleSheet.flatten([styles.foo, styles.bar]);
// { fontSize: 24, color: 'red', backgroundColor: 'blue', lineHeight: 36 }
// alternatively:
StyleSheet.flatten(styles.foo);
// { fontSize: 24, color: 'red' }
```
### `resolve(style)`
Resolve one style.
#### Parameters
##### `style`
#### Example
```js
const styles = StyleSheet.create({
foo: {
fontSize: 24,
color: 'red',
},
});
StyleSheet.resolve(styles.foo);
// { fontSize: 24, color: 'red' }
```
## TextStyles
An interface to Sketch's shared text styles. Create styles with or without rendering them to the document canvas.
### `create(styles, options)`
The primary interface to TextStyles. **Call this before rendering**.
#### Parameters
##### `styles` **(required)**
An object of JavaScript styles. The keys will be used as Sketch's Text Style names.
##### `options: { document, clearExistingStyles }`
###### `document`
The Sketch Document currently being rendered into.
###### `clearExistingStyles`
Clear any styles already registered in the document.
#### Example
```js
import sketch from 'sketch';
import { TextStyles, View, Text, render } from 'react-sketchapp';
export default () => {
const typeStyles = {
Headline: {
fontSize: 36,
fontFamily: 'Apercu',
lineHeight: 38,
},
Body: {
fontSize: 16,
fontFamily: 'Helvetica',
lineHeight: 22,
},
};
TextStyles.create(
{
context: context,
clearExistingStyles: true,
},
typeStyles,
);
const Document = () => (
Headline text
Body text
);
render( , sketch.getSelectedDocument().selectedPage);
};
```
### `resolve(style)`
Find a stored native Sketch style object for a given JavaScript style object. You probably don't need to use this.
#### Parameters
##### `style`
A JavaScript style
### `styles`
Find all of the registered styles. You probably don't need to use this.
### `get(name)`
Find a text style by _name_.
#### Parameters
##### `name`
The style name
#### Example
```js
import sketch from 'sketch';
import { TextStyles, View, Text, render } from 'react-sketchapp';
export default () => {
const typeStyles = {
Headline: {
fontSize: 36,
fontFamily: 'Apercu',
lineHeight: 38,
},
Body: {
fontSize: 16,
fontFamily: 'Helvetica',
lineHeight: 22,
},
};
TextStyles.create(
{
context: context,
clearExistingStyles: true,
},
typeStyles,
);
const Document = () => (
Headline text
Body text
);
render( , sketch.getSelectedDocument().selectedPage);
};
```
### `clear`
Reset the registered styles.
## Symbols
An interface to Sketch's symbols. Create symbols and optionally inject them into the symbols page.
### `makeSymbol(node, props, document)`
Creates a new symbol and injects it into the `Symbols` page. The name of the symbol can be optionally provided and will default to the display name of the component.
Returns a react component which is an can be used to render instances of the symbol.
#### Parameters
| Parameter | Type | Default | Note |
| --- | --- | --- | --- |
| `node` | `Node` | | The node object that will be rendered as a symbol |
| `props` | `Object` | The node name | Optional name for the symbol, string can include backslashes to organize these symbols with Sketch. For example `squares/blue` |
| `props.name` | `String` | The node name | Optional name for the symbol, string can include backslashes to organize these symbols with Sketch. For example `squares/blue` |
| `props.style` | [`Style`](/docs/styling.md) | | |
| `document` | `Object` | The current document | The Sketch document to make the symbol in |
### `getSymbolComponentByName(name)`
Returns a react component which can be used to render the symbol instance that is associated with that name.
### `getSymbolMasterByName(name)`
Returns the JSON representation of the symbol master that is associated with that name.
### Symbol example
```js
import sketch from 'sketch';
import { View, makeSymbol, Artboard, render } from 'react-sketchapp';
const BlueSquare = () => (
);
const BlueSquareSymbol = makeSymbol(BlueSquare);
const Document = () => (
);
export default () => {
render( , sketch.getSelectedDocument().selectedPage);
};
```
### Text override example
Text overrides use the name parameter to target a specific Text primitive. When no name is given the value within the Text primitive can be used to override the value.
```js
import sketch from 'sketch';
import { View, Text, makeSymbol, Artboard, render } from 'react-sketchapp';
const BlueSquare = () => (
Blue Square Text
);
const BlueSquareSymbol = makeSymbol(BlueSquare, 'squares/blue');
const Document = () => (
);
export default () => {
render( , sketch.getSelectedDocument().selectedPage);
};
```
### Image override example
Image overrides use the name parameter to target a specific Image primitive.
```js
import sketch from 'sketch';
import { View, Image, Artboard, makeSymbol, render } from 'react-sketchapp';
const BlueSquare = () => (
);
const BlueSquareSymbol = makeSymbol(BlueSquare, 'squares/blue');
const Document = () => (
);
export default () => {
render( , sketch.getSelectedDocument().selectedPage);
};
```
#### Nested symbol + override example
```js
import sketch from 'sketch';
import { View, Text, makeSymbol, Image, Artboard, render } from 'react-sketchapp';
const RedSquare = () => (
Red Square
);
const RedSquareSymbol = makeSymbol(RedSquare, 'squares/red');
const BlueSquare = () => (
Blue Square
);
const BlueSquareSymbol = makeSymbol(BlueSquare, 'squares/blue');
const Photo = () => (
);
const PhotoSymbol = makeSymbol(Photo);
const Nested = () => (
);
const NestedSymbol = makeSymbol(Nested);
const Document = () => (
);
export default () => {
render( , sketch.getSelectedDocument().selectedPage);
};
```
================================================
FILE: docs/FAQ.md
================================================
# Frequently Asked Questions
#### Why?!??!
`react-sketchapp` evolved out of our need to generate **high-quality, consistent Sketch assets** for our design system at Airbnb. Wrapping Sketch’s imperative API is a pragmatic solution for a great developer experience and predictable rendering.
#### How do I `console.log`?
You have multiple options to view the logs:
- Using the [sketch-dev-tools](https://github.com/skpm/sketch-dev-tools)
- `Console.app -> ~/Library/Logs -> com.bohemiancoding.sketch -> Plugin Output.log`
- in the terminal
```bash
skpm log -f
```
- in the terminal
```bash
tail -F ~/Library/Logs/com.bohemiancoding.sketch3/Plugin\ Output.log
```
Occasionally this file disappears — in that case, run this and then try `tail`ing again.
```bash
touch ~/Library/Logs/com.bohemiancoding.sketch3/Plugin\ Output.log
```
For more information, check out the [Sketch developer documentation](https://developer.sketch.com/plugins/debugging).
#### I'm running a project as a plugin & Sketch isn't showing my changes
Sketch has a [developer mode](https://developer.sketch.com/plugins/debugging#reload-scripts) which refreshes plugins before running. If you're using `skpm` this should be set up automatically, but just in case try running
```bash
defaults write com.bohemiancoding.sketch3.plist AlwaysReloadScript -bool YES
```
#### `` & ``? Where are the shapes? Talk to me about your API decisions!
Early versions of `react-sketchapp` mirrored Sketch's layers — ``, ``, `` etc. This was adequate for rendering simplistic designs such as grids of color palettes, but our focus is on production design systems.
At some point, we had to translate from our component codebase's primitives to Sketch's shapes. We tried translating trees of React Native elements into ``s etc, but it felt clumsy. Not every Sketch property has an analog in react-native, but **most react-native properties are translatable to Sketch**.
By aligning with react-native's API we:
- think in the same primitives as we actually use in production
- use the same layout algorithm in design & code
- [render real components](http://airbnb.io/react-sketchapp/docs/guides/universal-rendering.html) into Sketch with `react-primitives` (a platform independent set of primitives)
Where it makes sense we're open to creating Sketch-specific components —there's no analog for `` on web or mobile—but the goal of `react-sketchapp` is to bring design & engineering closer together.
#### So I can't draw arbitrary shapes?
You can use the [SVG API](/docs/API.md#svg) to draw arbitrary shapes.
#### Any plans to support Sketch's constraints for layout?
Not currently. FlexBox is the closest we have to a predictable, cross-platform layout specification — by using it, we can use the same styles on every platform we build for.
We currently use [`yoga`](https://github.com/facebook/yoga).
#### Is there two-way binding? Can I generate React components from Sketch? 🔁
Nope.
Isomorphisms are compelling but our focus is on tools that we can use day-to-day to improve the productivity of designers and engineers working on large-scale production applications.
Getting production-ready semantics out of Sketch is more difficult than generating production-ready Sketch templates from React components 💀
Our solution is to keep our [our design system](http://airbnb.design/building-a-visual-language/)’s source of truth in code, and use `react-sketchapp` to compose & consume it.
To _edit_ our design system, we are free to leverage any technology that can create React components, or be compiled to JSX, such as:
- [React-centric IDEs](https://www.decosoftware.com/)
- in-house design tools that are tailored to our workflow (whilst being backed by data, version control & semantic versioning) 🔜 👀
- writing React components in text editors with our fingers
#### Does this tie your workflow to Sketch? What about other design tools?
Treating Sketch primarily as a _rendering target_ for cross-platform components pushes you to store components & style in code — you're then free to build translation layers for any other design tool that exposes an API.
Given equivalent API support it would be possible to simultaneously render to `react-sketchapp`, `react-figma`, `react-xd` & `react-quark`.
Rather than tying us into one design tools, reasoning about design in cross-platform primitives _frees us_ to use the tooling we want.
#### Can I use [TypeScript](https://www.typescriptlang.org/)?
Of course!
TypeScript definitions are published with the npm package.
================================================
FILE: docs/README.md
================================================
## Table of Contents
- [Introduction](/README.md)
- [Guides](/docs/guides/README.md)
- [Getting Started](/docs/guides/getting-started.md)
- [Using `skpm` as a build system](/docs/guides/using-skpm.md)
- [Rendering](/docs/guides/rendering.md)
- [Data Fetching](/docs/guides/data-fetching.md)
- [Universal Rendering](/docs/guides/universal-rendering.md)
- [Styling](/docs/guides/styling.md)
- [API Reference](/docs/API.md)
- [render](/docs/API.md#renderelement-container)
- [renderToJSON](/docs/API.md#rendertojsonelement)
- [Document](/docs/API.md#document)
- [Page](/docs/API.md#page)
- [Artboard](/docs/API.md#artboard)
- [Image](/docs/API.md#image)
- [RedBox](/docs/API.md#redbox)
- [Svg](/docs/API.md#svg)
- [Text](/docs/API.md#text)
- [View](/docs/API.md#view)
- [Platform](/docs/API.md#platform)
- [StyleSheet](/docs/API.md#stylesheet)
- [TextStyles](/docs/API.md#textstyles)
- [Symbols](/docs/API.md#symbols)
- [Examples](/docs/examples.md)
- [Change Log](/CHANGELOG.md)
- [FAQ](/docs/FAQ.md)
- [Contributing](https://github.com/airbnb/react-sketchapp/blob/master/.github/CONTRIBUTING.md)
================================================
FILE: docs/examples.md
================================================
# Examples
`react-sketchapp` is bundled with lots of examples!
### [Basic setup](https://github.com/airbnb/react-sketchapp/tree/master/examples/basic-setup)

### [Style guide](https://github.com/airbnb/react-sketchapp/tree/master/examples/styleguide)

### [Profile Cards](https://github.com/airbnb/react-sketchapp/tree/master/examples/profile-cards)

### [Profile Cards on Web + Sketch w/ `react-primitives`](https://github.com/airbnb/react-sketchapp/tree/master/examples/profile-cards-primitives)

### [Profile Cards w/ `react-with-styles`](https://github.com/airbnb/react-sketchapp/tree/master/examples/profile-cards-react-with-styles)

### [Profile Cards w/ GraphQL](https://github.com/airbnb/react-sketchapp/tree/master/examples/profile-cards-graphql)

### [Venue Search on Web + Sketch w/ `react-primitives`, Foursquare & Google Maps](https://github.com/airbnb/react-sketchapp/tree/master/examples/foursquare-maps)

### [Generative Colors w/ Chroma-JS](https://github.com/airbnb/react-sketchapp/tree/master/examples/colors)

### [Timeline w/ AirTable](https://github.com/airbnb/react-sketchapp/tree/master/examples/timeline-airtable)

### [Basic setup w/ Typescript](https://github.com/airbnb/react-sketchapp/tree/master/examples/basic-setup-typescript)
 
================================================
FILE: docs/guides/README.md
================================================
# Guides
How to use `react-sketchapp` for fun and profit.
- [Getting Started](getting-started.md)
- [Using `skpm` as a build system](using-skpm.md)
- [Rendering](rendering.md)
- [Data Fetching](data-fetching.md)
- [Universal Rendering](universal-rendering.md)
- [Styling](styling.md)
================================================
FILE: docs/guides/data-fetching.md
================================================
# Data Fetching
Pull real data from an API with `fetch` or GraphQL.
## Fetch
[Full example](https://github.com/airbnb/react-sketchapp/tree/master/examples/foursquare-maps)
`skpm` automatically provides the [Sketch `fetch` polyfill](https://github.com/skpm/sketch-polyfill-fetch) — just use `fetch` as usual.
```js
import fetch from 'sketch-module-fetch-polyfill';
import { render } from 'react-sketchapp';
import MyApp from './MyApp';
export default context => {
fetch('https://reqres.in/api/users')
.then(res => res.json())
.then(data => {
render( , context.document.currentPage());
});
};
```
## GraphQL
[Full example](https://github.com/airbnb/react-sketchapp/tree/master/examples/profile-cards-graphql)
[`gql-sketch`](https://github.com/jongold/gql-sketch) provides a convenient interface for interacting with GraphQL APIs.
```bash
npm install gql-sketch --save
```
```js
import Client from 'gql-sketch';
import { render } from 'react-sketchapp';
import MyApp from './MyApp';
export default context => {
Client('http://example.com/my-graphql-endpoint')
.query(
`
{
allFilms {
films {
title,
actor,
catchphrase
}
}
}
`,
)
.then(({ allFilms }) => {
render( , context.document.currentPage());
});
};
```
================================================
FILE: docs/guides/getting-started.md
================================================
# Getting Started
You can create a `react-sketchapp` project with `skpm`, by cloning a ready-made [example](../examples.md), or by manually setting up the `package.json` and `manifest.json` scripts (advanced usage).
## Environment Setup
You will need npm, Node and Sketch.
- Terminal (if you’re new to the command line, this [guide](https://medium.com/32pixels/the-designers-guide-to-the-osx-command-prompt-71b0016cac31) may help)
- You need to make sure `git` is installed – type `git --version` in your Terminal to check if it's installed, if it isn’t, you should be prompted to install via “command line developer tools”.
- Code editor e.g. [VSCode](https://code.visualstudio.com/), [Atom](https://atom.io/)
- Node.js & `npm` – [install with Homebrew](https://nodejs.org/en/download/package-manager/#macos) (or install with [Node Version Manager](https://nodejs.org/en/download/package-manager/#nvm))
- [Sketch](https://www.sketch.com/)
- requires macOS
## Creating a Project With Skpm
**Replace** `my-app` with your desired project name:
### Installation
```bash
npm install --global skpm
skpm create my-app --template=airbnb/react-sketchapp # template is a GitHub repo
cd my-app
```
### Setup
You can now open `my-app` in your code editor of choice. You will see a `src` folder with a `manifest.json` file and Sketch entrypoint (e.g. `my-command.js`). If you wish to rename `my-command.js`, you can do so and update the file name in `script` in `manifest.json`
Example modifications (assuming we want to rename the entrypoint file to `main.js` and don't want to have sub-commands):
`src/manifest.json`
```diff
"commands": [
{
- "name": "my-command",
+ "name": "My App Name: Sketch Components"
- "identifier": "my-command-identifier",
+ "identifier": "main",
- "script": "./my-command.js"
+ "script": "./main.js"
}
],
"menu": {
- "title": "my-app",
- "items": [
- "my-command-identifier"
- ]
+ "isRoot": true,
+ "items": [
+ "main"
+ ]
+ }
}
```
### Rendering to Sketch
To render your app to Sketch, open the Sketch application, create a new blank document, then go to your Terminal and run:
```bash
# Make sure you've already done `cd my-app`
npm run render
```
You can pass the target Sketch container layer (i.e. document, group or page object) to the `render` function in your Sketch plugin entrypoint file, using the Sketch API: `render( , sketch.getSelectedDocument()`.
For more info on rendering to Sketch, see the [rendering](./rendering.md) page.
================================================
FILE: docs/guides/rendering.md
================================================
# Rendering Guide
You can use the Sketch API to select Sketch containers such as documents, pages or groups, to pass through to the `render` function.
### Rendering to Multiple Pages or New Documents
`src/my-command.js` (or whatever file your Sketch plugin entrypoint is).
```js
import React from 'react';
import { render, Document, Page } from 'react-sketchapp';
// wrapper is required if you want to use multiple pages
const App = () => (
Hello World!
Hello World, again!
);
export default () => {
const documents = sketch.getDocuments();
const document =
sketch.getSelectedDocument() || new sketch.Document(); // get the current document // or create a new document
};
```
## Rendering to Selected Document
This will render to the last active document. If there is no document open, document will be undefined and you will get an error, so you can add `|| new sketch.Document()` as a fallback to handle this.
```js
import sketch from 'sketch';
import { render } from 'react-sketchapp';
// const App = () => ... or import App from './App';
export default () => {
const document = sketch.getSelectedDocument();
render( , document);
};
```
## Rendering to Document by Name
We can select a document by name, by looping through `sketch.getDocuments()` and checking `doc.path` inside the loop.
```js
import path from 'path';
import sketch from 'sketch';
import { render } from 'react-sketchapp';
// const App = () => ... or import App from './App';
const getDocumentByName = name => {
return (sketch.getDocuments() || []).find(doc => {
return doc.path && path.basename(doc.path, '.sketch') === name;
});
};
export default () => {
const document = getDocumentByName('My App Design') || new sketch.Document(); // Fallback to new document if document not found
render( , document);
};
```
================================================
FILE: docs/guides/styling.md
================================================
# Styling
Components use CSS styles + FlexBox layout.
## Layout Styles
| property | type | supported? |
| --- | --- | --- |
| `width` | `number` | `percentage` | ✅ |
| `height` | `number` | `percentage` | ✅ |
| `top` | `number` | `percentage` | ✅ |
| `left` | `number` | `percentage` | ✅ |
| `right` | `number` | `percentage` | ✅ |
| `bottom` | `number` | `percentage` | ✅ |
| `minWidth` | `number` | `percentage` | ✅ |
| `maxWidth` | `number` | `percentage` | ✅ |
| `minHeight` | `number` | `percentage` | ✅ |
| `maxHeight` | `number` | `percentage` | ✅ |
| `margin` | `number` | `percentage` | ✅ |
| `marginVertical` | `number` | `percentage` | ✅ |
| `marginHorizontal` | `number` | `percentage` | ✅ |
| `marginTop` | `number` | `percentage` | ✅ |
| `marginBottom` | `number` | `percentage` | ✅ |
| `marginLeft` | `number` | `percentage` | ✅ |
| `marginRight` | `number` | `percentage` | ✅ |
| `padding` | `number` | `percentage` | ✅ |
| `paddingVertical` | `number` | `percentage` | ✅ |
| `paddingHorizontal` | `number` | `percentage` | ✅ |
| `paddingTop` | `number` | `percentage` | ✅ |
| `paddingBottom` | `number` | `percentage` | ✅ |
| `paddingLeft` | `number` | `percentage` | ✅ |
| `paddingRight` | `number` | `percentage` | ✅ |
| `borderWidth` | `number` | `percentage` | ✅ |
| `borderTopWidth` | `number` | `percentage` | ✅ |
| `borderRightWidth` | `number` | `percentage` | ✅ |
| `borderBottomWidth` | `number` | `percentage` | ✅ |
| `borderLeftWidth` | `number` | `percentage` | ✅ |
| `position` | `absolute` | `relative` | ✅ |
| `flexDirection` | `row` | `row-reverse` | `column` | `column-reverse` | ✅ |
| `flexWrap` | `wrap` | `nowrap` | ✅ |
| `justifyContent` | `flex-start` | `flex-end` | `center` | `space-between` | `space-around` | ✅ |
| `alignItems` | `flex-start` | `flex-end` | `center` | `stretch` | ✅ |
| `alignSelf` | `auto` | `flex-start` | `flex-end` | `center` | `stretch` | ✅ |
| `overflow` | `visible` | `hidden` | `scroll` | ✅ |
| `flex` | `number` | ✅ |
| `flexGrow` | `number` | ✅ |
| `flexShrink` | `number` | ✅ |
| `flexBasis` | `number` | ✅ |
| `aspectRatio` | `number` | ⛔️ |
| `zIndex` | `number` | ✅ |
| `backfaceVisibility` | `visible` | `hidden` | ⛔️ |
| `backgroundColor` | `Color` | ✅ |
| `borderColor` | `Color` | ✅ |
| `borderTopColor` | `Color` | ✅ |
| `borderRightColor` | `Color` | ✅ |
| `borderBottomColor` | `Color` | ✅ |
| `borderLeftColor` | `Color` | ✅ |
| `borderRadius` | `number` | `percentage` | ✅ |
| `borderTopLeftRadius` | `number` | `percentage` | ✅ |
| `borderTopRightRadius` | `number` | `percentage` | ✅ |
| `borderBottomLeftRadius` | `number` | `percentage` | ✅ |
| `borderBottomRightRadius` | `number` | `percentage` | ✅ |
| `borderStyle` | `solid` | `dotted` | `dashed` | ✅ |
| `borderWidth` | `number` | `percentage` | ✅ |
| `borderTopWidth` | `number` | `percentage` | ✅ |
| `borderRightWidth` | `number` | `percentage` | ✅ |
| `borderBottomWidth` | `number` | `percentage` | ✅ |
| `borderLeftWidth` | `number` | `percentage` | ✅ |
| `opacity` | `number` | ✅ |
## Shadow Styles
| property | type | supported? |
| --- | --- | --- |
| `shadowColor` | `Color` | ✅ |
| `shadowOffset` | `{ width: number, height: number }` | ✅ |
| `shadowOpacity` | `number` | ✅ |
| `shadowRadius` | `number` | `percentage` | ✅ |
## Type Styles
| property | type | supported? |
| --- | --- | --- |
| `color` | `Color` | ✅ |
| `fontFamily` | `string` | ✅ |
| `fontSize` | `number` | ✅ |
| `fontStyle` | `normal` | `italic` | ✅ |
| `fontWeight` | `string` | `number` | ✅ |
| `textDecorationLine` | `none` | `underline` | `double` | `line-through` | ✅ |
| `textShadowOffset` | `{ width: number, height: number }` | ✅ |
| `textShadowRadius` | `number` | ✅ |
| `textShadowColor` | `Color` | ✅ |
| `textTransform` | `none` | `uppercase` | `lowercase` | ✅ |
| `letterSpacing` | `number` | ✅ |
| `lineHeight` | `number` | ✅ |
| `textAlign` | `auto` | `left` | `right` | `center` | `justify` | ✅ |
| `writingDirection` | `auto` | `ltr` | `rtl` | ⛔️ |
| `opacity` | `number` | ✅ |
| `percentage` | `points` | `percentages` | ✅ |
## Styles Specific To `react-sketchapp`
Some properties are Sketch specific and won't work cross-platform but give you a better control over your components.
| property | type | supported? |
| --- | --- | --- |
| `shadowSpread` | `number` | ✅ |
| `shadowInner` | `boolean` | ✅ |
## Examples
Styles can be passed to components as plain objects, or via [`StyleSheet`](/docs/API.md).
```js
import { View, StyleSheet } from 'react-sketchapp';
// inline props
// plain JS object
const style = {
backgroundColor: 'hotPink',
width: 300,
}
// StyleSheet
const styles = StyleSheet.create({
foo: {
backgroundColor: 'hotPink',
width: 300,
}
})
```
You can use variables in your styles just like a standard React application:
```javascript
const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
Peach: '#EFADA0',
Pear: '#93DAAB',
};
{Object.keys(colors).map(name => (
))}
;
```
================================================
FILE: docs/guides/universal-rendering.md
================================================
# Universal Rendering
The `react-sketchapp` components have been architected to provide the same metaphors, layout system & interfaces as `react-native`, so there is less switching cost between platforms. However, it is also possible to render the _same code_ across multiple platforms. We call this _Universal Rendering_.
The [`react-primitives`](https://github.com/lelandrichardson/react-primitives) project provides consistent primitive interfaces across platforms, and is the simplest way to achieve Universal Rendering.
## Setup
React Primitives works out-of-the-box with `react-dom` & `react-native`, and `react-sketchapp` (when using `skpm`).
Install `react-primitives` and its peer dependencies
```bash
npm install --save react-primitives react react-dom react-native react-sketchapp
```
## Creating your components
Import base primitives from `react-primitives` rather than `react-sketchapp` / `react-native` — e.g.
```diff
/**
* components/Row.js
* Define your component using platform-independent primitives
*/
import React from 'react';
- import { View, Text, StyleSheet } from 'react-sketchapp';
+ import { View, Text, StyleSheet } from 'react-primitives';
const Row = props =>
{ props.title }
{ props.subtitle }
export default Row;
```
## Importing existing components
If you have a large existing React Native component library, you might enjoy using a `codemod` to automatically convert `react-native` imports to `react-primitives` — [a proof-of-concept `codemod` is provided on ASTExplorer](https://astexplorer.net/#/gist/68d1b3ae3ec7b0a088452a7d38643dc4/latest).
## Rendering
Each platform will require an entry point with its respective `render` / registration call - e.g:
```js
/**
* dom-entry.js
* Standard ReactDOM setup for the browser
*/
import React from 'react';
import { render } from 'react-dom';
import Row from './components/Row';
render(
, document.getElementById('root'));
```
```js
/**
* native-entry.js
* Standard ReactNative setup
*/
import React from 'react';
import { AppRegistry } from 'react-native';
import Row from './components/Row';
AppRegistry.registerComponent('Row', () => Row);
```
```js
/**
* sketch-entry.js
* same setup as other examples
*/
import React from 'react';
import { render } from 'react-sketchapp';
import Row from './components/Row';
export default context => {
render(
, context.document.currentPage());
};
```
React Primitives only provides components that make sense on every platform, so Sketch-specific concepts like `TextStyles` and ` ` should be imported from the main `react-sketchapp` package. You can mix-and-match them as necessary - e.g.
```js
/**
* sketch-entry.js
* same setup as other examples
*/
import React from 'react';
import { Artboard, render } from 'react-sketchapp';
import Row from './components/Row'; // built with react-primitives
export default context => {
render(
,
context.document.currentPage(),
);
};
```
================================================
FILE: docs/guides/using-skpm.md
================================================
# Using `skpm` as a build system
Sketch allows arbitrary plugins written in [CocoaScript](http://developer.sketchapp.com/guides/cocoascript) to run. [`skpm`](https://github.com/skpm/skpm) is a utility to create, build and manage Sketch plugins. It takes care of transforming your JavaScript into CocoaScript and makes sure the context it is running in is as close as possible to what you are used to when writing JavaScript.
## Installation
> Important: Node.JS > V6.x is a minimum requirement.
```bash
npm install -g skpm
```
## Usage
### Creating a new plugin
```bash
skpm create my-plugin --template=airbnb/react-sketchapp
```
> A note on templates
>
> The purpose of skpm templates are to provide opinionated development tooling setups so that users can get started with actual plugin code as fast as possible.
>
> - [`airbnb/react-sketchapp`](https://github.com/airbnb/react-sketchapp) is a simple template to get started with `react-sketchapp`
>
> 💁 Tip: Any Github repo with a 'template' folder can be used as a custom template: `skpm create --template=/`
### Build the plugin
Once the installation is done, you can run some commands inside the project folder:
```bash
npm run build
```
To watch for changes:
```bash
npm run watch
```
Additionally, if you wish to run the plugin every time it is built:
```bash
npm run render
```
### View the plugin's log
To view the output of your `console.log`, you have a few different options:
- Using the [sketch-dev-tools](https://github.com/skpm/sketch-dev-tools)
- Open Console.app and look for the sketch logs
- Look at the `~/Library/Logs/com.bohemiancoding.sketch3/Plugin Output.log` file
Skpm provides a convenient way to do the latter:
```bash
skpm log
-f, -F The `-f` option causes tail to not stop when end of file is
reached, but rather to wait for additional data to be appended
to the input. [boolean] [default: "false"]
--number, -n Shows `number` lines of the logs. [number]
```
## Custom Configuration
### Babel
To customize Babel, you have two options:
- You may create a [`.babelrc`](https://babeljs.io/docs/usage/babelrc) file in your project's root directory. Any settings you define here will overwrite matching config-keys within skpm preset. For example, if you pass a "presets" object, it will replace & reset all Babel presets that skpm defaults to.
- If you'd like to modify or add to the existing Babel config, you must use a `webpack.skpm.config.js` file. Visit the [`webpack`](#webpack) section for more info.
### `webpack`
To customize `webpack` create `webpack.skpm.config.js` file which exports function that will change `webpack`'s config.
```js
/**
* Function that mutates original webpack config.
* Supports asynchronous changes when promise is returned.
*
* @param {object} config - original webpack config.
* @param {boolean} isPluginCommand - wether the config is for a plugin command or a resource
**/
module.exports = function(config, isPluginCommand) {
/** you can change config here **/
};
```
================================================
FILE: examples/.eslintrc
================================================
{
"rules": {
"import/no-unresolved": 0,
"import/extensions": 0
}
}
================================================
FILE: examples/.gitignore
================================================
**/*.sketchplugin
================================================
FILE: examples/basic-setup/README.md
================================================
# Basic setup
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/basic-setup
cd basic-setup
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Basic skpm Example`
Run with live reloading in Sketch, need a new sketch doc open
```bash
npm run render
```
## The idea behind the example
[`skpm`](https://github.com/skpm/skpm) is the easiest way to build `react-sketchapp` projects - this is a minimal example of it in use.

================================================
FILE: examples/basic-setup/package.json
================================================
{
"name": "basic-setup",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "basic-setup.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.7.5"
},
"dependencies": {
"chroma-js": "^1.2.2",
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/basic-setup/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Basic Setup",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/basic-setup/src/my-command.js
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { render, Artboard, Text, View } from 'react-sketchapp';
import chroma from 'chroma-js';
// take a hex and give us a nice text color to put over it
const textColor = (hex) => {
const vsWhite = chroma.contrast(hex, 'white');
if (vsWhite > 4) {
return '#FFF';
}
return chroma(hex).darken(3).hex();
};
const Swatch = ({ name, hex }) => (
{name}
{hex}
);
const Color = {
hex: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
Swatch.propTypes = Color;
const Document = ({ colors }) => (
{Object.keys(colors).map((color) => (
))}
);
Document.propTypes = {
colors: PropTypes.objectOf(PropTypes.string).isRequired,
};
export default () => {
const colorList = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur Dark': '#24828F',
Peach: '#EFADA0',
'Peach Dark': '#E37059',
Pear: '#93DAAB',
'Pear Dark': '#2E854B',
};
render( , context.document.currentPage());
};
================================================
FILE: examples/basic-setup/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/basic-setup-typescript/.gitignore
================================================
node_modules
.ts-compiled
================================================
FILE: examples/basic-setup-typescript/README.md
================================================
# Basic setup with Typescript
This example was adapted from the [basic-setup example](../basic-setup).
> **NOTE:** you may also use the typings _without_ using typescript if you editor supports it. [See here](../../docs/guides/community-provided-tooling.md).
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/basic-setup-typescript
cd basic-setup-typescript
```
Install the dependencies
```bash
npm install
```
Run with live reloading in Sketch, need a new sketch doc open. This will put both skpm and the Typescript compiler in watch mode:
```bash
npm run render
```
To clean the `.ts-compiled` directory, you can run:
```bash
npm run typescript:clean
```
## How the typescript works
This example compiles the typescript into javascript that can be used by `skpm`. The compiled typescript files get output into the `.ts-compiled` directory. The `manifest.json` of `skpm` then simply points to the compiled javascript. To get live re-loading working, use the typescript compiler in watch mode. Whenever you save a typescript file, the typescript compiler will output javascript to the `.ts-compiled` directory. Once `skpm` notices the javascript file in `.ts-compiled` changes, it will re-build and re-render.
Here is a reference `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"jsx": "react-native",
"allowJs": true,
"strict": true,
"outDir": "./.ts-compiled",
"rootDir": "./src",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": [
"./src/**/*"
]
}
```
================================================
FILE: examples/basic-setup-typescript/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Basic Setup with Typescript",
"identifier": "main",
"script": "./.ts-compiled/my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/basic-setup-typescript/package.json
================================================
{
"name": "basic-setup-typescript",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "basic-setup.sketchplugin",
"manifest": "./manifest.json"
},
"scripts": {
"build": "npm run typescript:once && skpm-build",
"watch": "skpm-build --watch & npm run typescript",
"render": "skpm-build --watch --run & npm run typescript",
"render:once": "npm run typescript:once && skpm-build --run",
"postinstall": "npm run build && skpm-link",
"typescript": "tsc --watch",
"typescript:once": "tsc",
"typescript:clean": "rm -rf ./.ts-compiled"
},
"author": "Jon Gold ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.0",
"@types/chroma-js": "^1.3.3",
"typescript": "^3.7.2"
},
"dependencies": {
"chroma-js": "^1.2.2",
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/basic-setup-typescript/src/my-command.tsx
================================================
import * as React from 'react';
import sketch from 'sketch';
import { render, Artboard, Text, View } from 'react-sketchapp';
import chroma from 'chroma-js';
// take a hex and give us a nice text color to put over it
const textColor = (hex: string) => {
const vsWhite = chroma.contrast(hex, 'white');
if (vsWhite > 4) {
return '#FFF';
}
return chroma(hex).darken(3).hex();
};
interface SwatchProps {
name: string;
hex: string;
}
const Swatch = ({ name, hex }: SwatchProps) => (
{name}
{hex}
);
interface DocumentProps {
colors: { [key: string]: string };
}
const Document = ({ colors }: DocumentProps) => (
{Object.keys(colors).map((color) => (
))}
);
export default () => {
const colorList = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur Dark': '#24828F',
Peach: '#EFADA0',
'Peach Dark': '#E37059',
Pear: '#93DAAB',
'Pear Dark': '#2E854B',
'TypeScript Blue': '#007ACC',
};
render( , sketch.getSelectedDocument().selectedPage);
};
================================================
FILE: examples/basic-setup-typescript/src/types/sketch.d.ts
================================================
declare module 'sketch';
================================================
FILE: examples/basic-setup-typescript/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"jsx": "react-native",
"allowJs": true,
"strict": true,
"outDir": "./.ts-compiled",
"rootDir": "./src",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": [
"./src/**/*"
]
}
================================================
FILE: examples/basic-setup-typescript/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/basic-svg/README.md
================================================
# Basic SVG example
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/basic-svg
cd basic-svg
```
Install the dependencies
```bash
npm install
```
Run with live reloading in Sketch, need a new sketch doc open
```bash
npm run render
```
Or, to install as a Sketch plugin:
```bash
npm run build
npm run link-plugin
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Basic SVG Example`
## The idea behind the example
[`skpm`](https://github.com/sketch-pm/skpm) is the easiest way to build `react-sketchapp` projects - this is a minimal example of it in use.

================================================
FILE: examples/basic-svg/package.json
================================================
{
"name": "basic-svg",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "basic-svg.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.7.5"
},
"dependencies": {
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/basic-svg/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Basic SVG",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/basic-svg/src/my-command.js
================================================
import * as React from 'react';
import { render, Artboard, Svg } from 'react-sketchapp';
const Document = () => (
);
export default () => {
render( , context.document.currentPage());
};
================================================
FILE: examples/basic-svg/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/colors/README.md
================================================
# Generative Colors w/ chroma-js
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/colors
cd colors
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Generative Colors`
Run with live reloading in Sketch, need a new sketch doc open
```bash
npm run render
```
## The idea behind the example
Calculating color scales is pretty tricky in Sketch - have fun with it!

================================================
FILE: examples/colors/package.json
================================================
{
"name": "colors",
"version": "1.0.0",
"private": true,
"skpm": {
"main": "colors.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"chroma-js": "^1.2.2",
"prop-types": "^15.5.8",
"ramda": "^0.23.0",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2",
"webpack-shell-plugin": "^0.5.0"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/colors/src/main.js
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { render, StyleSheet, View } from 'react-sketchapp';
import chroma from 'chroma-js';
import { times } from 'ramda';
const styles = StyleSheet.create({
container: {
width: 480,
height: 480,
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
},
});
const Document = ({ colors, steps }) => {
const color = chroma.scale(colors);
return (
{times((i) => color(i / steps).hex(), steps).map((val, i) => (
))}
);
};
Document.propTypes = {
colors: PropTypes.arrayOf(PropTypes.string),
steps: PropTypes.number,
};
export default () => {
render(
,
context.document.currentPage(),
);
};
================================================
FILE: examples/colors/src/manifest.json
================================================
{
"name": "colors",
"author": "Jon Gold",
"version": 0.1,
"compatibleVersion": 1,
"bundleVersion": 1,
"disableCocoaScriptPreprocessor": true,
"commands": [
{
"name": "react-sketchapp: Generative Colors",
"identifier": "main",
"script": "main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/colors/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/emotion/package.json
================================================
{
"name": "emotion-example",
"version": "1.0.0",
"skpm": {
"main": "emotion.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Nitin Tulswani ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.3"
},
"dependencies": {
"emotion-primitives": "^1.0.0-beta.6",
"react": "^16.4.1",
"react-primitives": "^0.6.0",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/emotion/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: emotion",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": ["main"]
}
}
================================================
FILE: examples/emotion/src/my-command.js
================================================
import React from 'react';
import emotion from 'emotion-primitives';
import { render } from 'react-sketchapp';
const Container = emotion.View`
display: flex;
justify-content: center;
align-items: center;
margin: 50px;
border: 5px solid red;
background-color: ${(props) => props.theme.backgroundColor}
`;
const Description = emotion.Text`
color: hotpink;
`;
const Image = emotion.Image`
padding: 40px;
`;
const emotionLogo = 'https://avatars3.githubusercontent.com/u/31557565?s=400&v=4';
class App extends React.Component {
render() {
return (
Emotion Primitives
);
}
}
export default () => {
render( , context.document.currentPage());
};
================================================
FILE: examples/emotion/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/form-validation/README.md
================================================
# Form Validation
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/form-validation
cd form-validation
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Form Validation`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
`react-sketchapp` makes it simple to render all potential states of a web component to sketch.

================================================
FILE: examples/form-validation/package.json
================================================
{
"name": "form-validation",
"version": "1.0.0",
"private": true,
"skpm": {
"main": "form-validation.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link",
"web": "react run src/web.js"
},
"author": "Lloyd Wheeler ",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-native": "^0.42.3",
"react-primitives": "^0.6.0",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"extract-text-webpack-plugin": "^2.1.0",
"nwb": "^0.15.6",
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/form-validation/src/components/Button.js
================================================
import * as React from 'react';
import { Text, View } from 'react-primitives';
import { spacing, colors, fontFamily } from '../designSystem';
const buttonStyle = {
borderRadius: 3,
boxSizing: 'border-box',
color: colors.White,
cursor: 'pointer',
padding: spacing.Medium,
width: 300,
};
const textStyle = {
color: colors.White,
fontFamily,
fontWeight: 'bold',
textAlign: 'center',
};
const Button = ({ label, backgroundColor }) => (
{label}
);
export default Button;
================================================
FILE: examples/form-validation/src/components/Register.js
================================================
import * as React from 'react';
import { View, Text, StyleSheet } from 'react-primitives';
import { spacing, colors, typeRamp, fontFamily } from '../designSystem';
import TextBox from './TextBox';
import StrengthMeter from './StrengthMeter';
import Button from './Button';
const styles = StyleSheet.create({
register: {
backgroundColor: colors.LightGrey,
padding: spacing.Large,
boxSizing: 'border-box',
},
heading: {
color: colors.Purple,
fontSize: typeRamp.Medium,
fontFamily,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: spacing.Medium,
width: 300,
},
});
const Register = ({ session }) => (
Register an Account
);
Register.defaultProps = {
session: {
email: '',
password: '',
},
};
export default Register;
================================================
FILE: examples/form-validation/src/components/Space.js
================================================
import * as React from 'react';
import { View } from 'react-primitives';
const Space = ({ h, v, children }) => (
{children}
);
export default Space;
================================================
FILE: examples/form-validation/src/components/StrengthMeter.js
================================================
import * as React from 'react';
import { View, Text } from 'react-primitives';
import { colors, fontFamily, spacing, typeRamp } from '../designSystem';
const strengths = {
short: {
width: 75,
label: 'Too short',
backgroundColor: colors.Rose,
},
fair: {
width: 150,
label: 'Fair',
backgroundColor: colors.Yellow,
},
good: {
width: 225,
label: 'Good',
backgroundColor: colors.Yellow,
},
strong: {
width: 300,
label: 'Strong',
backgroundColor: colors.Green,
},
};
const styles = {
meter: {
boxSizing: 'border-box',
height: 5,
width: 300,
backgroundColor: '#ddd',
marginTop: spacing.Medium,
marginBottom: spacing.Large,
borderRadius: 5,
},
innerMeter: {
boxSizing: 'border-box',
height: 5,
borderRadius: 5,
},
meterLabel: {
fontFamily,
textAlign: 'right',
width: 300,
fontSize: typeRamp.Small,
marginTop: 5,
},
};
const passwordStrength = (password) => {
// Faux password checking
if (password.length <= 6) {
return 'short';
}
if (password.length <= 9) {
return 'fair';
}
if (password.length <= 12) {
return 'good';
}
return 'strong';
};
const StrengthMeter = ({ password }) => (
{password.length > 0 && (
{strengths[passwordStrength(password)].label}
)}
);
export default StrengthMeter;
================================================
FILE: examples/form-validation/src/components/TextBox/index.js
================================================
import React, { Component } from 'react';
import styles from './style';
class TextBox extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
value: this.props.value,
};
}
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
return (
{this.props.label}
{this.props.children &&
React.cloneElement(this.props.children, {
password: this.state.value,
})}
);
}
}
export default TextBox;
================================================
FILE: examples/form-validation/src/components/TextBox/index.sketch.js
================================================
import * as React from 'react';
import { View, Text } from 'react-primitives';
import styles from './style';
type Props = {
label: string,
value: string,
children?: React$Element,
};
const TextBox = ({ label, value, children }: Props) => (
{label}
{value}
{children}
);
export default TextBox;
================================================
FILE: examples/form-validation/src/components/TextBox/style.js
================================================
import { colors, spacing, fontFamily, typeRamp } from '../../designSystem';
export default {
formElement: {
marginBottom: spacing.Medium,
},
label: {
display: 'block',
fontFamily,
marginBottom: spacing.Small,
fontSize: typeRamp.Medium - 2,
},
textbox: {
boxSizing: 'border-box',
borderWidth: 1,
borderStyle: 'solid',
borderColor: colors.Grey,
backgroundColor: colors.White,
fontFamily,
fontSize: typeRamp.Medium,
lineHeight: typeRamp.Medium,
padding: spacing.Medium,
width: 300,
},
};
================================================
FILE: examples/form-validation/src/data.js
================================================
export default [
{
email: 'john.hornsby@example.com',
password: '',
},
{
email: 'john.hornsby@example.com',
password: 'hello',
},
{
email: 'john.hornsby@example.com',
password: '!H3ll0!',
},
{
email: 'john.hornsby@example.com',
password: 'IL0v3ToasT!',
},
{
email: 'john.hornsby@example.com',
password: 'IRea11yL0v3ToasT!',
},
];
================================================
FILE: examples/form-validation/src/designSystem.js
================================================
export const colors = {
Purple: '#5700A2',
Yellow: '#BB9A05',
Orange: '#fd6134',
Rose: '#ff4289',
Green: '#005b4c',
Black: '#222223',
LightGrey: '#eeeeee',
Grey: '#cccccc',
White: '#ffffff',
};
export const spacing = {
xSmall: 4,
Small: 8,
Medium: 16,
Large: 32,
xLarge: 64,
};
export const typeRamp = {
xSmall: 7,
Small: 12,
Medium: 16,
Large: 24,
xLarge: 36,
};
export const typography = {
Heading: {
fontSize: typeRamp.Large,
textAlign: 'center',
marginBottom: spacing.Large,
},
};
export const fontFamily = 'Helvetica';
export default {
colors,
spacing,
typeRamp,
typography,
fontFamily,
};
================================================
FILE: examples/form-validation/src/main.js
================================================
import * as React from 'react';
import { render, View } from 'react-sketchapp';
import { Text } from 'react-primitives';
import { typography, spacing } from './designSystem';
import DATA from './data';
import Register from './components/Register';
import Space from './components/Space';
const Page = ({ sessions }) => (
Form Validation w/ DOM elements and React Primitives
{sessions.map((session) => (
))}
);
export default () => {
render( , context.document.currentPage());
};
================================================
FILE: examples/form-validation/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Form Validation",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/form-validation/src/web.js
================================================
import * as React from 'react';
import { typography, fontFamily } from './designSystem';
import Register from './components/Register';
const styles = {
containerStyle: {
width: 364,
marginLeft: 'auto',
marginRight: 'auto',
},
};
export default () => (
Form Validation w/ DOM elements and React Primitives. Type a password!
👀
);
================================================
FILE: examples/form-validation/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/foursquare-maps/.eslintrc
================================================
{
"env": {
"browser": true,
}
}
================================================
FILE: examples/foursquare-maps/README.md
================================================
# Foursquare + Google Maps
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/foursquare-maps
cd foursquare-maps
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Foursquare + Google Maps`
### Run it in Sketch
Run with live reloading in Sketch
```bash
npm run render
```
### Run it in your browser
```bash
npm run web
```
Open a browser to `http://localhost:3000`
## The idea behind the example
Creating maps with live data into Sketch is notoriously difficult — until now ;)
This example is created with `react-primitives` and renders simultaneously to Sketch & Web — maps are provided by [react-primitives-google-static-map](https://www.npmjs.com/package/react-primitives-google-static-map).

================================================
FILE: examples/foursquare-maps/package.json
================================================
{
"name": "foursquare-maps",
"version": "1.0.0",
"private": true,
"skpm": {
"main": "foursquare-maps.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"web": "react run src/web.js",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"jquery-param": "^0.2.0",
"nwb": "^0.15.6",
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-native": "^0.42.3",
"react-primitives": "^0.6.0",
"react-primitives-google-static-map": "^1.0.1",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/foursquare-maps/src/App.js
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import Map from 'react-primitives-google-static-map';
import { StyleSheet, Text, View } from 'react-primitives';
const styles = StyleSheet.create({
container: {
width: 375,
height: 667,
backgroundColor: '#fefefe',
borderWidth: 2,
borderColor: '#dfba69',
borderRadius: 4,
overflowY: 'scroll',
},
text: {
fontFamily: 'Helvetica',
fontSize: 24,
lineHeight: 24,
color: '#dfba69',
textAlign: 'center',
},
rowWrapper: {
padding: 16,
backgroundColor: '#FFF',
borderBottomWidth: 2,
borderBottomColor: '#dfba69',
},
rowTitle: {
color: '#dfba69',
fontSize: 18,
// lineHeight: 27,
fontWeight: 'bold',
fontFamily: 'GT America',
},
rowSubtitle: {
color: '#dfba69',
fontSize: 14,
// lineHeight: 18,
fontFamily: 'GT America',
},
});
const LatLong = PropTypes.shape({
latitude: PropTypes.string,
longitude: PropTypes.string,
});
const Venue = {
name: PropTypes.string,
location: PropTypes.shape({
address: PropTypes.string,
}),
};
const Row = ({ name, location }) => (
{name}
{location.address}
);
Row.propTypes = Venue;
const App = ({ center, venues }) => {
const pins = venues.map((v) => ({
latitude: v.location.lat,
longitude: v.location.lng,
}));
return (
{venues.map((v) => (
))}
);
};
App.propTypes = {
center: LatLong,
venues: PropTypes.arrayOf(PropTypes.shape(Venue)),
};
export default App;
================================================
FILE: examples/foursquare-maps/src/getVenues.js
================================================
import param from 'jquery-param';
export default () => {
const query = 'burger';
const latitude = '37.773972';
const longitude = '-122.431297';
const params = param({
v: '20161016',
ll: [latitude, longitude].join(','),
query,
limit: 15,
intent: 'checkin',
client_id: 'BCUJZ2MSKUWJC2Q5HVIYZLHRWGFJ2OFPKPLBP1NOBNR3VW5R',
client_secret: 'Q10HUP5APBQOYNTPABSH4CSKRGEAI2CXIYULYGG0EZYUUWUZ',
});
return fetch(`https://api.foursquare.com/v2/venues/search?${params}`)
.then((res) => res.json())
.then((data) => ({
venues: data.response.venues,
latitude,
longitude,
query,
}));
};
================================================
FILE: examples/foursquare-maps/src/main.js
================================================
import * as React from 'react';
import { Artboard, render } from 'react-sketchapp';
import App from './App';
import getVenues from './getVenues';
export default () => {
getVenues().then(({ venues, latitude, longitude }) => {
render(
,
context.document.currentPage(),
);
});
};
================================================
FILE: examples/foursquare-maps/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Foursquare + Google Maps",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/foursquare-maps/src/web.js
================================================
import * as React from 'react';
import { render } from 'react-dom';
import App from './App';
import getVenues from './getVenues';
getVenues().then(({ venues, latitude, longitude }) => {
render( , document.getElementById('app'));
});
================================================
FILE: examples/foursquare-maps/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/glamorous/README.md
================================================
# glamorous 💄
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/glamorous
cd glamorous
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: glamorous`
### Run it in Sketch
Run with live reloading in Sketch
```bash
npm run render
```
================================================
FILE: examples/glamorous/package.json
================================================
{
"name": "glamorous-example",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "glamorous.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Nitin Tulswani ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.0"
},
"dependencies": {
"chroma-js": "^1.3.4",
"glamorous-primitives": "^2.1.0",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/glamorous/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: glamorous",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/glamorous/src/my-command.js
================================================
import * as React from 'react';
import glamorous from 'glamorous-primitives';
import { render } from 'react-sketchapp';
const Container = glamorous.view({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
const Image = glamorous.image({
width: 400,
height: 400,
});
const Description = glamorous.text({
fontSize: 35,
padding: 40,
color: '#a4a4c1',
});
class App extends React.Component {
render() {
return (
Maintainable CSS with React
);
}
}
export default () => {
render( , context.document.currentPage());
};
================================================
FILE: examples/glamorous/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/profile-cards/README.md
================================================
# Profile Cards
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/profile-cards
cd profile-cards
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Profile Cards`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
`react-sketchapp` makes it simple to compose components and see how they perform with real data.

================================================
FILE: examples/profile-cards/package.json
================================================
{
"name": "profile-cards",
"private": true,
"skpm": {
"main": "profile-cards.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/profile-cards/src/components/Profile.js
================================================
import * as React from 'react';
import { Image, View, Text, StyleSheet } from 'react-sketchapp';
import { colors, fonts, spacing } from '../designSystem';
const styles = StyleSheet.create({
container: {
backgroundColor: colors.Haus,
padding: 20,
width: 260,
},
avatar: {
height: 220,
resizeMode: 'contain',
marginBottom: 20,
borderRadius: 10,
},
title: fonts['Title 2'],
subtitle: fonts['Title 3'],
body: fonts.Body,
});
const Avatar = ({ url }) => ;
const Title = ({ children }) => {children} ;
const Subtitle = ({ children }) => {children} ;
const Body = ({ children }) => {children} ;
const Profile = props => (
{props.user.name}
{`@${props.user.screen_name}`}
{props.user.description}
{props.user.location}
{props.user.url}
);
export default Profile;
================================================
FILE: examples/profile-cards/src/components/Space.js
================================================
import * as React from 'react';
import { View } from 'react-sketchapp';
const Space = ({ h, v, children }) => (
{children}
);
export default Space;
================================================
FILE: examples/profile-cards/src/designSystem.js
================================================
export const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur a11y': '#24828F',
Peach: '#EFADA0',
'Peach a11y': '#E37059',
Pear: '#93DAAB',
'Pear a11y': '#2E854B',
};
const typeSizes = [80, 48, 36, 24, 20, 16];
export const spacing = 16;
const fontFamilies = {
display: 'Helvetica',
body: 'Georgia',
};
const fontWeights = {
regular: 'regular',
bold: 'bold',
};
export const fonts = {
Headline: {
color: colors.Night,
fontSize: typeSizes[0],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 80,
},
'Title 1': {
color: colors.Night,
fontSize: typeSizes[2],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 48,
},
'Title 2': {
color: colors.Night,
fontSize: typeSizes[3],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 36,
},
'Title 3': {
color: colors.Night,
fontSize: typeSizes[4],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
},
Body: {
color: colors.Night,
fontSize: typeSizes[5],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
marginBottom: 24,
},
};
export default {
colors,
fonts,
spacing,
};
================================================
FILE: examples/profile-cards/src/main.js
================================================
import * as React from 'react';
import { render, Text, View } from 'react-sketchapp';
import { fonts, spacing } from './designSystem';
import Profile from './components/Profile';
import Space from './components/Space';
const Page = ({ users }) => (
Profile Cards
{users.map((user) => (
))}
);
export default () => {
const DATA = [
{
screen_name: 'mxstbr',
name: 'Max Stoiber',
description:
'⚛️ Makes styled-components, react-boilerplate, @KeystoneJS and CarteBlanche. ✌ Open source developer @thethinkmill. ☕ Speciality coffee geek, skier, traveller.',
location: 'Vienna, Austria',
url: 'mxstbr.com',
profile_image_url:
'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
},
{
name: '- ̗̀Jackie ̖́-',
screen_name: 'jackiesaik',
description:
'Graphic designer, never won a spelling be. Toronto on weekdays. Go Home Lake on weekends. ╮ (. ● ᴗ ●.) ╭',
location: 'Toronto, ON',
url: 'cargocollective.com/jackiesaik',
profile_image_url:
'https://pbs.twimg.com/profile_images/895665264464764930/7Mb3QtEB_400x400.jpg',
},
{
screen_name: 'jongold',
name: 'kerning man',
description:
'an equal command of technology and form • functional programming (oc)cultist • design tools @airbnbdesign',
location: 'California',
url: 'weirdwideweb.jon.gold',
profile_image_url: 'https://pbs.twimg.com/profile_images/833785170285178881/loBb32g3.jpg',
},
];
render( , context.document.currentPage());
};
================================================
FILE: examples/profile-cards/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Profile Cards",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/profile-cards/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/profile-cards-graphql/README.md
================================================
# Profile Cards w/ GraphQL
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/profile-cards-graphql
cd profile-cards-graphql
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Profile Cards w/ GraphQL`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
Building on the [Profile Cards example](../profile-cards), it's simple to fetch real data for your mockups with GraphQL!

================================================
FILE: examples/profile-cards-graphql/package.json
================================================
{
"name": "profile-cards-gql",
"version": "1.0.0",
"skpm": {
"main": "profile-cards-gql.sketchplugin",
"manifest": "src/manifest.json"
},
"private": true,
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"apollo-cache-inmemory": "^1.1.11",
"apollo-client": "^2.2.7",
"apollo-link-http": "^1.5.3",
"graphql": "^0.13.2",
"graphql-tag": "^2.4.0",
"react": "^16.3.2",
"react-apollo": "^2.1.0",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/profile-cards-graphql/src/components/Profile.js
================================================
import * as React from 'react';
import { Image, View, Text, StyleSheet } from 'react-sketchapp';
import { colors, fonts, spacing } from '../designSystem';
const styles = StyleSheet.create({
container: {
backgroundColor: colors.Haus,
padding: 20,
width: 260,
},
avatar: {
height: 220,
resizeMode: 'contain',
marginBottom: 20,
borderRadius: 10,
},
title: fonts['Title 2'],
subtitle: fonts['Title 3'],
body: fonts.Body,
});
const Avatar = ({ url }) => ;
const Title = ({ children }) => {children} ;
const Subtitle = ({ children }) => {children} ;
const Body = ({ children }) => {children} ;
const Profile = props => (
{props.user.name}
@{props.user.screenname}
{props.user.description}
{props.user.location}
{props.user.url}
);
export default Profile;
================================================
FILE: examples/profile-cards-graphql/src/components/Space.js
================================================
import * as React from 'react';
import { View } from 'react-sketchapp';
const Space = ({ h, v, children }) => (
{children}
);
export default Space;
================================================
FILE: examples/profile-cards-graphql/src/designSystem.js
================================================
export const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur a11y': '#24828F',
Peach: '#EFADA0',
'Peach a11y': '#E37059',
Pear: '#93DAAB',
'Pear a11y': '#2E854B',
};
const typeSizes = [80, 48, 36, 24, 20, 16];
export const spacing = 16;
const fontFamilies = {
display: 'Helvetica',
body: 'Georgia',
};
const fontWeights = {
regular: 'regular',
bold: 'bold',
};
export const fonts = {
Headline: {
color: colors.Night,
fontSize: typeSizes[0],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 80,
},
'Title 1': {
color: colors.Night,
fontSize: typeSizes[2],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 48,
},
'Title 2': {
color: colors.Night,
fontSize: typeSizes[3],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 36,
},
'Title 3': {
color: colors.Night,
fontSize: typeSizes[4],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
},
Body: {
color: colors.Night,
fontSize: typeSizes[5],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
marginBottom: 24,
},
};
export default {
colors,
fonts,
spacing,
};
================================================
FILE: examples/profile-cards-graphql/src/main.js
================================================
import * as React from 'react';
import { render, Text, View } from 'react-sketchapp';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { graphql, ApolloProvider } from 'react-apollo';
import gql from 'graphql-tag';
import { fonts, spacing } from './designSystem';
import Profile from './components/Profile';
import Space from './components/Space';
const GRAPHQL_ENDPOINT = 'https://api.graph.cool/simple/v1/cj09zm1k4jcpc0115ecsoc1k4';
const client = new ApolloClient({
link: new HttpLink({ uri: GRAPHQL_ENDPOINT }),
ssrMode: true,
cache: new InMemoryCache(),
});
const QUERY = gql`
{
allProfiles {
screenname
name
description
location
url
profileImageUrl
}
}
`;
const props = ({ data }) => (data.loading ? { users: [] } : { users: data.allProfiles });
const withUsers = graphql(QUERY, { props });
const Page = ({ users }) => (
Profile Cards w/ GraphQL
{users && (
{users.map((user) => (
))}
)}
);
const PageWithUsers = withUsers(Page);
const App = () => (
);
export default () => {
client
.query({ query: QUERY })
.then(() => render( , context.document.currentPage()))
.catch(console.log);
};
================================================
FILE: examples/profile-cards-graphql/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Profile Cards w/ GraphQL",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/profile-cards-graphql/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/profile-cards-primitives/README.md
================================================
# Profile Cards w/ React Primitives
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/profile-cards-primitives
cd profile-cards-primitives
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Profile Cards w/ Primitives`
### Run it in Sketch
Run with live reloading in Sketch
```bash
npm run render
```
### Run it in your browser
```bash
npm run web
```
Open a browser to `http://localhost:3000`
## The idea behind the example
`react-primitives` provides a powerful way to build platform-independent components. This is a simple example of rendering to web & Sketch simultaneously.

================================================
FILE: examples/profile-cards-primitives/nwb.config.js
================================================
module.exports = {
webpack: {
resolve: {
// look for .web.js first
extensions: ['.web.js', '.js', '.json'],
},
},
};
================================================
FILE: examples/profile-cards-primitives/package.json
================================================
{
"name": "profile-cards-primitives",
"private": true,
"skpm": {
"main": "profile-cards-primitives.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link",
"web": "react run src/web.js"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"nwb": "^0.15.6",
"react": "^16.3.2",
"react-dom": "^15.4.2",
"react-native": "^0.42.3",
"react-primitives": "^0.6.0",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/profile-cards-primitives/src/components/Profile.js
================================================
import * as React from 'react';
import { Image, View, Text, StyleSheet } from 'react-primitives';
import { colors, fonts, spacing } from '../designSystem';
const styles = StyleSheet.create({
container: {
backgroundColor: colors.Haus,
padding: 20,
width: 260,
},
avatar: {
height: 220,
resizeMode: 'contain',
marginBottom: 20,
borderRadius: 10,
},
title: fonts['Title 2'],
subtitle: fonts['Title 3'],
body: fonts.Body,
});
const Avatar = ({ url }) => ;
const Title = ({ children }) => {children} ;
const Subtitle = ({ children }) => {children} ;
const Body = ({ children }) => {children} ;
const Profile = (props) => (
{props.user.name}
{`@${props.user.screen_name}`}
{props.user.description}
{props.user.location}
{props.user.url}
);
export default Profile;
================================================
FILE: examples/profile-cards-primitives/src/components/Space.js
================================================
import * as React from 'react';
import { View } from 'react-primitives';
const Space = ({ h, v, children }) => (
{children}
);
export default Space;
================================================
FILE: examples/profile-cards-primitives/src/data.js
================================================
export default [
{
screen_name: 'mxstbr',
name: 'Max Stoiber',
description:
'⚛️ Makes styled-components, react-boilerplate, @KeystoneJS and CarteBlanche. ✌ Open source developer @thethinkmill. ☕ Speciality coffee geek, skier, traveller.',
location: 'Vienna, Austria',
url: 'mxstbr.com',
profile_image_url:
'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
},
{
name: '- ̗̀Jackie ̖́-',
screen_name: 'jackiesaik',
description:
'Graphic designer, never won a spelling be. Toronto on weekdays. Go Home Lake on weekends. ╮ (. ● ᴗ ●.) ╭',
location: 'Toronto, ON',
url: 'cargocollective.com/jackiesaik',
profile_image_url:
'https://pbs.twimg.com/profile_images/895665264464764930/7Mb3QtEB_400x400.jpg',
},
{
screen_name: 'jongold',
name: 'kerning man',
description:
'an equal command of technology and form • functional programming (oc)cultist • design tools @airbnbdesign',
location: 'California',
url: 'weirdwideweb.jon.gold',
profile_image_url: 'https://pbs.twimg.com/profile_images/833785170285178881/loBb32g3.jpg',
},
];
================================================
FILE: examples/profile-cards-primitives/src/designSystem.js
================================================
export const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur a11y': '#24828F',
Peach: '#EFADA0',
'Peach a11y': '#E37059',
Pear: '#93DAAB',
'Pear a11y': '#2E854B',
};
const typeSizes = [80, 48, 36, 24, 20, 16];
export const spacing = 16;
const fontFamilies = {
display: 'Helvetica',
body: 'Georgia',
};
const fontWeights = {
regular: 'regular',
bold: 'bold',
};
export const fonts = {
Headline: {
color: colors.Night,
fontSize: typeSizes[0],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 80,
},
'Title 1': {
color: colors.Night,
fontSize: typeSizes[2],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 48,
},
'Title 2': {
color: colors.Night,
fontSize: typeSizes[3],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 36,
},
'Title 3': {
color: colors.Night,
fontSize: typeSizes[4],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
},
Body: {
color: colors.Night,
fontSize: typeSizes[5],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
marginBottom: 24,
},
};
export default {
colors,
fonts,
spacing,
};
================================================
FILE: examples/profile-cards-primitives/src/main.js
================================================
import * as React from 'react';
import { render } from 'react-sketchapp';
import { Text, View } from 'react-primitives';
import { fonts, spacing } from './designSystem';
import Profile from './components/Profile';
import Space from './components/Space';
import DATA from './data';
const Page = ({ users }) => (
Profile Cards w/ React Primitives
{users.map((user) => (
))}
);
export default () => {
render( , context.document.currentPage());
};
================================================
FILE: examples/profile-cards-primitives/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Profile Cards w/ Primitives",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/profile-cards-primitives/src/web.js
================================================
import * as React from 'react';
import Profile from './components/Profile';
import Space from './components/Space';
import { spacing } from './designSystem';
import DATA from './data';
/*
* is defined with platform-independent components
* from react-primitives. We can use it in our web UI, and
* continue to use primitives, or mix them with DOM elements
*/
export default () => (
Cross-platform components!
<Profile /> is defined with platform-independent components from react-primitives. We
can use it in our web UI, and continue to use primitives, or mix them with DOM elements
{DATA.map((user) => (
))}
);
================================================
FILE: examples/profile-cards-primitives/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/profile-cards-react-with-styles/README.md
================================================
# Profile Cards w/ react-with-styles
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/profile-cards-react-with-styles
cd profile-cards-react-with-styles
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Profile Cards`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
Use `react-sketchapp` with [`react-with-styles`](https://github.com/airbnb/react-with-styles) — a library for writing CSS-in-JS without coupling to specific implementations.

================================================
FILE: examples/profile-cards-react-with-styles/package.json
================================================
{
"name": "profile-cards-react-with-styles",
"private": true,
"skpm": {
"main": "profile-cards-react-with-styles.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2",
"react-with-styles": "^1.4.0"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/profile-cards-react-with-styles/src/components/Profile.js
================================================
import * as React from 'react';
import { Image, View, Text } from 'react-sketchapp';
import { css, withStyles } from '../withStyles';
const Profile = ({ user, styles }) => (
{user.name}
{`@${user.screen_name}`}
{user.description}
{user.location}
{user.url}
);
export default withStyles(({ colors, fonts, spacing }) => ({
container: {
backgroundColor: colors.Haus,
padding: spacing,
width: 260,
marginRight: spacing,
},
avatar: {
height: 220,
resizeMode: 'contain',
marginBottom: spacing * 2,
borderRadius: 10,
},
titleWrapper: {
marginBottom: spacing,
},
title: { ...fonts['Title 2'] },
subtitle: { ...fonts['Title 3'] },
body: { ...fonts.Body },
}))(Profile);
================================================
FILE: examples/profile-cards-react-with-styles/src/main.js
================================================
import * as React from 'react';
import { render, Text, View } from 'react-sketchapp';
import Profile from './components/Profile';
import { css, withStyles } from './withStyles';
const Title = withStyles(({ fonts }) => ({
titleText: fonts['Title 1'],
}))(({ children, styles }) => {children} );
const Page = ({ users }) => (
Profile Cards w/ react-with-styles
{users.map((user) => (
))}
);
export default () => {
const DATA = [
{
screen_name: 'jaredpalmer',
name: 'Jared Palmer',
description: 'Engineer @PalmerGroupHQ',
location: 'New York, NY',
url: 'github.com/jaredpalmer',
profile_image_url: 'https://pbs.twimg.com/profile_images/662984079638405120/Y6oncSaf.jpg',
},
{
name: '- ̗̀Jackie ̖́-',
screen_name: 'jackiesaik',
description:
'Graphic designer, never won a spelling be. Toronto on weekdays. Go Home Lake on weekends. ╮ (. ● ᴗ ●.) ╭',
location: 'Toronto, ON',
url: 'cargocollective.com/jackiesaik',
profile_image_url:
'https://pbs.twimg.com/profile_images/895665264464764930/7Mb3QtEB_400x400.jpg',
},
{
screen_name: 'jongold',
name: 'kerning man',
description:
'an equal command of technology and form • functional programming (oc)cultist • design tools @airbnbdesign',
location: 'California',
url: 'weirdwideweb.jon.gold',
profile_image_url: 'https://pbs.twimg.com/profile_images/833785170285178881/loBb32g3.jpg',
},
];
render( , context.document.currentPage());
};
================================================
FILE: examples/profile-cards-react-with-styles/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Profile Cards w/ react-with-styles",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/profile-cards-react-with-styles/src/theme.js
================================================
export const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur a11y': '#24828F',
Peach: '#EFADA0',
'Peach a11y': '#E37059',
Pear: '#93DAAB',
'Pear a11y': '#2E854B',
};
const typeSizes = [80, 48, 36, 24, 20, 16];
export const spacing = 16;
const fontFamilies = {
display: 'Helvetica',
body: 'Georgia',
};
const fontWeights = {
regular: 'regular',
bold: 'bold',
};
export const fonts = {
Headline: {
color: colors.Night,
fontSize: typeSizes[0],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 80,
},
'Title 1': {
color: colors.Night,
fontSize: typeSizes[2],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 48,
},
'Title 2': {
color: colors.Night,
fontSize: typeSizes[3],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 36,
},
'Title 3': {
color: colors.Night,
fontSize: typeSizes[4],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
},
Body: {
color: colors.Night,
fontSize: typeSizes[5],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
marginBottom: 24,
},
};
export default {
colors,
fonts,
spacing,
};
================================================
FILE: examples/profile-cards-react-with-styles/src/types.js
================================================
export type User = {
screen_name: string,
name: string,
description: string,
profile_image_url: string,
location: string,
url: string,
};
================================================
FILE: examples/profile-cards-react-with-styles/src/withStyles.js
================================================
import ThemedStyleSheet from 'react-with-styles/lib/ThemedStyleSheet';
import { css, withStyles, ThemeProvider } from 'react-with-styles';
import { StyleSheet } from 'react-sketchapp';
import theme from './theme';
const Interface = {
create(styleHash) {
return StyleSheet.create(styleHash);
},
resolve(styles) {
return { style: styles };
},
};
ThemedStyleSheet.registerDefaultTheme(theme);
ThemedStyleSheet.registerInterface(Interface);
export { css, withStyles, ThemeProvider, ThemedStyleSheet };
================================================
FILE: examples/profile-cards-react-with-styles/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/react-router-prototyping/README.md
================================================
# React Router setup
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/react-router-prototyping
cd react-router-prototyping
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: React Router prototyping`
Run with live reloading in Sketch, need a new sketch doc open
```bash
npm run render
```
## The idea behind the example
`react-sketchapp` allows you to build prototypes with navigation. This example shows how you build a Sketch prototype with `react-sketchapp-router`, while being able to share your routing code with your React web or React Native app.

================================================
FILE: examples/react-router-prototyping/package.json
================================================
{
"name": "react-router-prototyping",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "react-router-prototyping.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Macintosh Helper ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.3"
},
"dependencies": {
"prop-types": "^15.5.8",
"react": "^16.13.0",
"react-router-primitives": "^0.1.2",
"react-sketchapp": "^3.1.0",
"react-sketchapp-router": "^0.1.3",
"react-test-renderer": "^16.13.0"
}
}
================================================
FILE: examples/react-router-prototyping/src/App.js
================================================
import React from 'react';
import { SketchRouter, Switch, Route } from 'react-sketchapp-router';
import Home from './routes/home';
import About from './routes/about';
import Post from './routes/post';
import Profile from './routes/profile';
const App = () => (
{/* (Need to have menus/sidebars inside of a Route) */}
} />
} />
} />
} />
{/* } /> */}
);
export default App;
================================================
FILE: examples/react-router-prototyping/src/components/AppBar.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import { Link } from 'react-router-primitives';
const AppBar = () => (
My Blog
);
export default AppBar;
================================================
FILE: examples/react-router-prototyping/src/components/NavBar.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import { Link } from 'react-router-primitives';
const MenuItem = ({ name, href }) => (
{name}
);
const NavBar = () => (
{[{ name: 'About Us', href: '/about' }].map((props) => (
))}
);
export default NavBar;
================================================
FILE: examples/react-router-prototyping/src/main.js
================================================
/* global context */
import * as React from 'react';
import { render, Page, Document as RootDocument } from 'react-sketchapp';
import App from './App';
const pages = [
{
name: 'App',
component: App,
},
];
const Document = () => (
{pages.map(({ name, component: Component }) => (
))}
);
export default () => {
const data = context.document.documentData();
const pages = context.document.pages();
data.setCurrentPage(pages.firstObject());
render( );
};
================================================
FILE: examples/react-router-prototyping/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: React Router prototyping",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/react-router-prototyping/src/routes/about.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import AppBar from '../components/AppBar';
const About = () => (
About Us
There is not a lot of information here about us.
);
export default About;
================================================
FILE: examples/react-router-prototyping/src/routes/home.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import { Link } from 'react-router-primitives';
import AppBar from '../components/AppBar';
import NavBar from '../components/NavBar';
const PostSummary = () => (
Title of a Blog Post
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
{`> `}
Click here to read more
);
const Home = () => (
);
export default Home;
================================================
FILE: examples/react-router-prototyping/src/routes/post.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import { Link } from 'react-router-primitives';
import AppBar from '../components/AppBar';
import NavBar from '../components/NavBar';
const posts = {
'1': {
title: 'Title of a Blog Post',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
author: {
id: 'john',
name: 'John',
},
},
};
const Post = ({ id }) => {
return (
{id && (
{posts[id].title}
{posts[id].author.name}
{posts[id].content}
)}
);
};
export default Post;
================================================
FILE: examples/react-router-prototyping/src/routes/profile.js
================================================
import React from 'react';
import { View, Text } from 'react-sketchapp';
import AppBar from '../components/AppBar';
import NavBar from '../components/NavBar';
const Profile = ({ user }) => {
const name = user ? `${user.charAt(0).toUpperCase()}${user.slice(1)}` : 'User not found';
return (
{name}
);
};
export default Profile;
================================================
FILE: examples/react-router-prototyping/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/styled-components/README.md
================================================
# Styled-components
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/styled-components
cd styled-components
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Styled components`
### Run it in Sketch
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
`styled-components` allows you to write actual CSS code to style your components. It also removes the mapping between components and styles
================================================
FILE: examples/styled-components/package.json
================================================
{
"name": "styled-components-example",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "styled-components.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Mathieu Dutour ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.0"
},
"dependencies": {
"chroma-js": "^1.2.2",
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-primitives": "^0.6.0",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2",
"styled-components": "^2.1.0"
}
}
================================================
FILE: examples/styled-components/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Styled components",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/styled-components/src/my-command.js
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import styled from 'styled-components/primitives';
import { render } from 'react-sketchapp';
import chroma from 'chroma-js';
// take a hex and give us a nice text color to put over it
const textColor = (hex) => {
const vsWhite = chroma.contrast(hex, 'white');
if (vsWhite > 4) {
return '#FFF';
}
return chroma(hex).darken(3).hex();
};
const SwatchTile = styled.View`
height: 250px;
width: 250px;
border-radius: 4px;
margin: 4px;
background-color: ${(props) => props.hex};
justify-content: center;
align-items: center;
`;
const SwatchName = styled.Text`
color: ${(props) => textColor(props.hex)};
font-weight: bold;
`;
const Ampersand = styled.Text`
color: #f3f3f3;
font-size: 120px;
font-family: Himalaya;
line-height: 144px;
`;
const Title = styled.Text`
font-size: 24px;
font-family: 'GT America';
font-weight: bold;
padding: 4px;
`;
const Swatch = ({ name, hex }) => (
{name}
&
);
const Color = {
hex: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
Swatch.propTypes = Color;
const Artboard = styled.View`
flex-direction: row;
flex-wrap: wrap;
width: ${(96 + 8) * 4}px;
justify-content: center;
`;
const Document = ({ colors }) => (
Max’s Sweaters
{Object.keys(colors).map((color) => (
))}
);
Document.propTypes = {
colors: PropTypes.objectOf(PropTypes.string).isRequired,
};
export default () => {
const colorList = {
Classic: '#96324E',
Neue: '#21304E',
};
render( , context.document.currentPage());
};
================================================
FILE: examples/styled-components/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/styleguide/.flowconfig
================================================
[ignore]
[include]
[libs]
[options]
================================================
FILE: examples/styleguide/README.md
================================================
# Styleguide
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/styleguide
cd styleguide
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Styleguide`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
The reason we started `react-sketchapp` was to build dynamic styleguides! This is an example showing how to quickly render rich styleguides from JavaScript design system definition. It uses `chroma-js` to dynamically generate color contrast labels.

================================================
FILE: examples/styleguide/package.json
================================================
{
"name": "styleguide",
"private": true,
"skpm": {
"main": "styleguide.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"dependencies": {
"chroma-js": "^1.2.2",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/styleguide/src/components/AccessibilityBadge.js
================================================
import * as React from 'react';
import Badge from './Badge';
const AccessibilityBadge = ({ level }) => {
let text;
switch (true) {
case level.aaa:
text = 'AAA';
break;
case level.aa:
text = 'AA';
break;
case level.aaLarge:
text = 'AA Large';
break;
default:
text = null;
}
return text && {text} ;
};
export default AccessibilityBadge;
================================================
FILE: examples/styleguide/src/components/Badge.js
================================================
import * as React from 'react';
import { View, Text } from 'react-sketchapp';
const Badge = ({ children, filled = false }) => (
{children}
);
export default Badge;
================================================
FILE: examples/styleguide/src/components/Label.js
================================================
import * as React from 'react';
import { Text } from 'react-sketchapp';
const Label = ({ bold, children }) => (
{children}
);
export default Label;
================================================
FILE: examples/styleguide/src/components/Palette.js
================================================
import * as React from 'react';
import { View } from 'react-sketchapp';
import Swatch from './Swatch';
const SWATCH_WIDTH = 100;
const Palette = ({ colors }) => (
{Object.keys(colors).map((name) => (
))}
);
export default Palette;
================================================
FILE: examples/styleguide/src/components/Section.js
================================================
import * as React from 'react';
import { View } from 'react-sketchapp';
import Label from './Label';
const Section = ({ title, children }) => (
{title}
{children}
);
export default Section;
================================================
FILE: examples/styleguide/src/components/Swatch.js
================================================
import * as React from 'react';
import { View } from 'react-sketchapp';
import AccessibilityBadge from './AccessibilityBadge';
import Label from './Label';
const SWATCH_WIDTH = 100;
const Swatch = ({ color, name }) => (
{name}
{color.hex}
);
export default Swatch;
================================================
FILE: examples/styleguide/src/components/TypeSpecimen.js
================================================
import * as React from 'react';
import { View, Text } from 'react-sketchapp';
import Label from './Label';
const TypeSpecimen = ({ name, style }) => (
{`${style.fontSize} / ${style.lineHeight}`}
{name}
);
export default TypeSpecimen;
================================================
FILE: examples/styleguide/src/designSystem.js
================================================
import processColor from './processColor';
export const colors = {
Haus: '#F3F4F4',
Night: '#333',
Sur: '#96DBE4',
'Sur a11y': '#24828F',
Peach: '#EFADA0',
'Peach a11y': '#E37059',
Pear: '#93DAAB',
'Pear a11y': '#2E854B',
};
const typeSizes = [80, 48, 36, 24, 20, 16];
export const spacing = 16;
const fontFamilies = {
display: 'Helvetica',
body: 'Georgia',
};
const fontWeights = {
regular: 'regular',
bold: 'bold',
};
export const fonts = {
Headline: {
color: colors.Night,
fontSize: typeSizes[0],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 80,
},
'Title 1': {
color: colors.Night,
fontSize: typeSizes[2],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 48,
},
'Title 2': {
color: colors.Night,
fontSize: typeSizes[3],
fontFamily: fontFamilies.display,
fontWeight: fontWeights.bold,
lineHeight: 36,
},
'Title 3': {
color: colors.Night,
fontSize: typeSizes[4],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
},
Body: {
color: colors.Night,
fontSize: typeSizes[5],
fontFamily: fontFamilies.body,
fontWeight: fontWeights.regular,
lineHeight: 24,
marginBottom: 24,
},
};
export default {
colors: Object.keys(colors).reduce(
(acc, name) => ({
...acc,
[name]: processColor(colors[name]),
}),
{},
),
fonts,
spacing,
};
================================================
FILE: examples/styleguide/src/main.js
================================================
import * as React from 'react';
import { render, TextStyles, View } from 'react-sketchapp';
import designSystem from './designSystem';
import Label from './components/Label';
import Palette from './components/Palette';
import Section from './components/Section';
import TypeSpecimen from './components/TypeSpecimen';
const Document = ({ system }) => (
This is an example react-sketchapp document, showing how to render a styleguide from a data
representation of your design system.
{Object.keys(system.fonts).map((name) => (
))}
);
export default () => {
TextStyles.create(designSystem.fonts, {
clearExistingStyles: true,
});
render( , context.document.currentPage());
};
================================================
FILE: examples/styleguide/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Styleguide",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/styleguide/src/processColor.js
================================================
import chroma from 'chroma-js';
const minimums = {
aa: 4.5,
aaLarge: 3,
aaa: 7,
aaaLarge: 4.5,
};
export default (hex) => {
const contrast = chroma.contrast(hex, 'white');
return {
hex,
contrast,
accessibility: {
aa: contrast >= minimums.aa,
aaLarge: contrast >= minimums.aaLarge,
aaa: contrast >= minimums.aaa,
aaaLarge: contrast >= minimums.aaaLarge,
},
};
};
================================================
FILE: examples/styleguide/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/symbols/README.md
================================================
# Symbol Support
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/symbols
cd symbols
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Symbol Support`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
`react-sketchapp@^0.11.0` introduces an API for creating Sketch symbols — this example shows them in use with React components.
================================================
FILE: examples/symbols/package.json
================================================
{
"name": "symbols",
"version": "1.0.0",
"description": "",
"skpm": {
"main": "symbols.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "Jon Gold ",
"license": "MIT",
"devDependencies": {
"@skpm/builder": "^0.4.0"
},
"dependencies": {
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
}
}
================================================
FILE: examples/symbols/src/manifest.json
================================================
{
"compatibleVersion": 3,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Symbol Support",
"identifier": "main",
"script": "./my-command.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/symbols/src/my-command.js
================================================
import * as React from 'react';
import { render, Artboard, Text, View, Image, makeSymbol } from 'react-sketchapp';
const RedSquare = () => (
Red Square
);
const RedSquareSym = makeSymbol(RedSquare, 'squares/red');
const BlueSquare = () => (
Blue Square
);
const BlueSquareSym = makeSymbol(BlueSquare, 'squares/blue');
const Photo = () => (
);
const PhotoSym = makeSymbol(Photo);
const Nested = () => (
);
const NestedSym = makeSymbol(Nested);
export default () => {
const Document = () => (
);
render( , context.document.currentPage());
};
================================================
FILE: examples/symbols/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: examples/timeline-airtable/.eslintrc
================================================
{
"env": {
"browser": true
}
}
================================================
FILE: examples/timeline-airtable/README.md
================================================
# Timeline w/ Airtable
## How to use
Download the example or [clone the repo](http://github.com/airbnb/react-sketchapp):
```bash
curl https://codeload.github.com/airbnb/react-sketchapp/tar.gz/master | tar -xz --strip=2 react-sketchapp-master/examples/timeline-airtable
cd timeline-airtable
```
Install the dependencies
```bash
npm install
```
Then, open Sketch and navigate to `Plugins → react-sketchapp: Timeline w/ Airtable`
Run with live reloading in Sketch
```bash
npm run render
```
## The idea behind the example
Simple timeline feed demonstrates how to retrieve records from Airtable to design with real data

================================================
FILE: examples/timeline-airtable/package.json
================================================
{
"name": "timeline-airtable",
"private": true,
"skpm": {
"main": "timeline-airtable.sketchplugin",
"manifest": "src/manifest.json"
},
"scripts": {
"build": "skpm-build",
"watch": "skpm-build --watch",
"render": "skpm-build --watch --run",
"render:once": "skpm-build --run",
"postinstall": "npm run build && skpm-link"
},
"author": "David E. Chen ",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.8",
"react": "^16.3.2",
"react-sketchapp": "^3.0.0",
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
"@skpm/builder": "^0.4.0"
}
}
================================================
FILE: examples/timeline-airtable/src/main.js
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { render, Artboard, Text, View, StyleSheet } from 'react-sketchapp';
const API_ENDPOINT_URL =
'https://api.airtable.com/v0/appFs7J3WdgHYCDxD/Features?api_key=keyu4dudakWLI0vAh&&sort%5B0%5D%5Bfield%5D=Target+Launch+Date&sort%5B0%5D%5Bdirection%5D=asc';
const styles = StyleSheet.create({
artboard: {
backgroundColor: '#F9FDFF',
},
verticalLine: {
width: 3,
},
dot: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: '#46D2B3',
},
dotCompleted: {
backgroundColor: '#46D2B3',
},
title: {
fontSize: 48,
fontWeight: 200,
color: '#000',
},
rowContainer: {
width: 800,
flexDirection: 'row',
flex: 1,
paddingLeft: 30,
paddingRight: 30,
},
rowDescription: {
fontSize: 16,
width: 400,
},
rowLeftArea: {
width: 99, // odd number to avoid antialiasing
alignItems: 'center',
height: 150,
},
rowDate: {
fontSize: 10,
color: '#46D2B3',
},
rowTitle: {
fontSize: 20,
},
});
const VerticalLine = ({ height = 1, color = '#46D2B3' }) => (
);
VerticalLine.propTypes = {
height: PropTypes.number,
color: PropTypes.string,
};
const Header = ({ title }) => (
{title}
);
Header.propTypes = {
title: PropTypes.string,
};
const Footer = () => (
);
const Dot = ({ completed }) => (
);
Dot.propTypes = {
completed: PropTypes.bool,
};
const Row = ({ title, description, completed, date, status }) => (
{`${status} on ${date}`}
{title}
{description}
);
Row.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
completed: PropTypes.bool,
date: PropTypes.string,
status: PropTypes.string,
};
const Timeline = (props) => (
{props.data.records.map(({ id, fields }) => (
))}
);
Timeline.propTypes = {
data: PropTypes.shape({
records: PropTypes.array,
}),
};
export default () => {
fetch(API_ENDPOINT_URL)
.then((res) => res.json())
.then((data) => {
render( , context.document.currentPage());
})
.catch((e) => console.error(e));
};
================================================
FILE: examples/timeline-airtable/src/manifest.json
================================================
{
"compatibleVersion": 1,
"bundleVersion": 1,
"commands": [
{
"name": "react-sketchapp: Timeline w/ Airtable",
"identifier": "main",
"script": "./main.js"
}
],
"menu": {
"isRoot": true,
"items": [
"main"
]
}
}
================================================
FILE: examples/timeline-airtable/webpack.skpm.config.js
================================================
const path = require('path');
module.exports = (config) => {
if (process.env.LOCAL_DEV) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'react-sketchapp': path.resolve(__dirname, '../../'),
},
};
}
};
================================================
FILE: jest.config.js
================================================
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/__tests__/jest/.*\\.(j|t)sx?$',
testPathIgnorePatterns: [
'/node_modules/',
'/_book',
'/lib',
'/scratch',
'/template',
'/src',
'/docs',
],
globals: {
'ts-jest': {
isolatedModules: true,
},
},
};
================================================
FILE: package.json
================================================
{
"name": "react-sketchapp",
"version": "3.2.6",
"description": "A React renderer for Sketch.app",
"sideEffects": false,
"main": "lib/index.js",
"module": "lib/module/index.js",
"types": "lib/index.d.ts",
"license": "MIT",
"author": "Jon Gold (http://jon.gold)",
"contributors": [
"Ben Wilkins ",
"Leland Richardson ",
"Mathieu Dutour ",
"Jarid Margolin "
],
"pre-commit": [
"lint-staged"
],
"scripts": {
"build": "run-s clean && run-p build:*",
"build:main": "tsc -p tsconfig.json",
"build:module": "tsc -p tsconfig.module.json",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"clean": "trash lib",
"docs:clean": "trash _book",
"docs:prepare": "gitbook install",
"docs:build": "npm run docs:prepare && gitbook build",
"docs:watch": "npm run docs:prepare && gitbook serve",
"docs:publish": "npm run docs:clean && npm run docs:build && cd _book && git init && git commit --allow-empty -m 'update book' && git fetch https://github.com/airbnb/react-sketchapp gh-pages && git checkout -b gh-pages && git add . && git commit -am 'update book' && git push https://github.com/airbnb/react-sketchapp gh-pages --force",
"lint-staged": "lint-staged",
"prepublishOnly": "npm run clean && npm run test:ci && npm run build",
"prettier:base": "prettier --write",
"prettify": "npm run prettier:base \"src/**/*.(j|t)sx?\" \"examples/**/*.(j|t)sx?\" \"__tests__/**/*.(j|t)sx?\" \"docs/**/*.md\"",
"test": "npm run test:unit && npm run test:e2e",
"test:unit": "jest --config jest.config.js --no-watchman",
"test:ci": "npm run test:unit -- --runInBand",
"test:e2e": "skpm-test",
"test:update": "npm run test -- --updateSnapshot",
"test:e2e:watch": "npm run test:e2e -- --watch",
"watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\""
},
"dependencies": {
"@lona/svg-model": "^2.0.0",
"@sketch-hq/sketch-file-format-ts": "4.0.3",
"airbnb-prop-types": "^2.15.0",
"error-stack-parser": "^2.0.6",
"invariant": "^2.2.2",
"js-sha1": "^0.6.0",
"murmur2js": "^1.0.0",
"node-sketch-bridge": "^0.2.0",
"normalize-css-color": "^1.0.1",
"pegjs": "^0.10.0",
"prop-types": "^15.7.2",
"seedrandom": "^3.0.5",
"yoga-layout-prebuilt": "^1.9.5"
},
"peerDependencies": {
"react": "*",
"react-test-renderer": "*"
},
"keywords": [
"sketch",
"sketchapp",
"react",
"reactjs",
"renderer"
],
"repository": {
"type": "git",
"url": "https://github.com/airbnb/react-sketchapp"
},
"bugs": {
"url": "https://github.com/airbnb/react-sketchapp/issues"
},
"homepage": "https://github.com/airbnb/react-sketchapp",
"lint-staged": {
"*.{js,jsx,ts,tsx,md}": "npm run prettier:base"
},
"devDependencies": {
"@skpm/babel-preset": "^0.2.1",
"@skpm/test-runner": "^0.4.1",
"@types/airbnb-prop-types": "^2.13.1",
"@types/invariant": "^2.2.31",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.2",
"@types/pegjs": "^0.10.1",
"@types/react": "^16.9.34",
"@types/react-test-renderer": "^16.9.2",
"@types/seedrandom": "^2.4.28",
"gitbook-cli": "^2.3.0",
"gitbook-plugin-anchorjs": "^2.1.0",
"gitbook-plugin-codeblock-disable-glossary": "0.0.1",
"gitbook-plugin-edit-link": "^2.0.2",
"gitbook-plugin-github": "^2.0.0",
"gitbook-plugin-prism": "^2.3.0",
"jest": "^25.4.0",
"jest-cli": "^25.4.0",
"lint-staged": "^10.1.7",
"npm-run-all": "^4.1.5",
"pre-commit": "^1.2.2",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-test-renderer": "^16.13.1",
"sketchapp-json-flow-types": "^0.3.6",
"trash-cli": "^3.0.0",
"ts-jest": "^25.4.0",
"typescript": "^3.8.3"
},
"skpm": {
"test": {
"testRegex": "/__tests__/skpm/.*\\.jsx?$"
}
}
}
================================================
FILE: prettier.config.js
================================================
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
proseWrap: 'never',
};
================================================
FILE: src/Platform.ts
================================================
import { getSketchVersion } from './utils/getSketchVersion';
export const Platform = {
OS: 'sketch',
Version: getSketchVersion(),
select: (obj: { sketch: any }) => obj.sketch,
};
================================================
FILE: src/buildTree.ts
================================================
import * as TestRenderer from 'react-test-renderer';
import yoga from 'yoga-layout-prebuilt';
import { Context } from './utils/Context';
import { TreeNode, TextNode, PlatformBridge } from './types';
import { hasAnyDefined } from './utils/hasAnyDefined';
import { pick } from './utils/pick';
import { computeYogaTree } from './jsonUtils/computeYogaTree';
import { computeTextTree } from './jsonUtils/computeTextTree';
import { INHERITABLE_FONT_STYLES } from './utils/constants';
import { zIndex } from './utils/zIndex';
export const reactTreeToFlexTree = (
node: TestRenderer.ReactTestRendererNode,
yogaNode: yoga.YogaNode,
context: Context,
): TreeNode => {
let textNodes: TextNode[] = [];
let textStyle = context.getInheritedStyles();
let newChildren: (string | TreeNode)[] = [];
let style: any;
let type: string;
if (typeof node === 'string') {
textNodes = computeTextTree(node, context);
type = 'sketch_text';
} else {
style = node.props && node.props.style ? node.props.style : {};
type = node.type || 'sketch_text';
if (type === 'sketch_svg' && node.children) {
// @ts-ignore
newChildren = node.children;
} else if (type === 'sketch_text') {
// If current node is a Text node, add text styles to Context to pass down to
// child nodes.
if (node.props && node.props.style && hasAnyDefined(style, INHERITABLE_FONT_STYLES)) {
const inheritableStyles: any = pick(style, INHERITABLE_FONT_STYLES);
inheritableStyles.flexDirection = 'row';
context.addInheritableStyles(inheritableStyles);
textStyle = {
...context.getInheritedStyles(),
...inheritableStyles,
};
}
// Compute Text Children
textNodes = computeTextTree(node, context);
} else if (node.children && node.children.length > 0) {
// Recursion reverses the render stacking order
// but that's actually fine because Sketch renders the first on top
// Calculates zIndex order to match yoga
const children = zIndex(node.children);
for (let index = 0; index < children.length; index += 1) {
const childComponent = children[index];
const childNode = yogaNode.getChild(index);
const renderedChildComponent = reactTreeToFlexTree(
childComponent,
childNode,
context.forChildren(),
);
newChildren.push(renderedChildComponent);
}
}
}
return {
type,
style,
textStyle,
layout: {
left: yogaNode.getComputedLeft(),
right: yogaNode.getComputedRight(),
top: yogaNode.getComputedTop(),
bottom: yogaNode.getComputedBottom(),
width: yogaNode.getComputedWidth(),
height: yogaNode.getComputedHeight(),
},
props: {
...(typeof node !== 'string' ? node.props : {}),
textNodes,
},
children: newChildren,
};
};
export const buildTree = (bridge: PlatformBridge) => (element: React.ReactElement) => {
let renderer: TestRenderer.ReactTestRenderer | undefined;
if (typeof TestRenderer.act !== 'undefined') {
TestRenderer.act(() => {
// synchronous callback
renderer = TestRenderer.create(element);
});
} else {
renderer = TestRenderer.create(element);
}
if (!renderer) {
throw new Error('Cannot access react renderer');
}
const json = renderer.toJSON();
if (!json) {
throw new Error('Cannot render react element');
}
const yogaNode = computeYogaTree(bridge)(json, new Context());
yogaNode.calculateLayout(undefined, undefined, yoga.DIRECTION_LTR);
const tree = reactTreeToFlexTree(json, yogaNode, new Context());
return tree;
};
================================================
FILE: src/components/Artboard.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { or } from 'airbnb-prop-types';
import { StyleSheet } from '../stylesheet';
import { ViewStylePropTypes } from './ViewStylePropTypes';
import { ArtboardProvider } from '../context';
const ViewportPropTypes = {
name: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
scale: PropTypes.number,
fontScale: PropTypes.number,
};
export const ArtboardPropTypes = {
style: or([PropTypes.shape(ViewStylePropTypes), PropTypes.number]),
name: PropTypes.string,
isHome: PropTypes.bool,
children: PropTypes.node,
viewport: PropTypes.shape(ViewportPropTypes),
};
export type Props = PropTypes.InferProps;
export class Artboard extends React.Component {
static propTypes = ArtboardPropTypes;
static defaultProps = {
name: 'Artboard',
};
render() {
const style = StyleSheet.flatten(this.props.style);
return (
{this.props.children}
);
}
}
================================================
FILE: src/components/Document.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
export const DocumentPropTypes = {
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class Document extends React.Component {
static propTypes = {
children: PropTypes.node,
};
render() {
return {this.props.children} ;
}
}
================================================
FILE: src/components/Image.tsx
================================================
import * as React from 'react';
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import * as PropTypes from 'prop-types';
import { or } from 'airbnb-prop-types';
import { StyleSheet } from '../stylesheet';
import { ResizeModePropTypes } from './ResizeModePropTypes';
import { ImageStylePropTypes } from './ImageStylePropTypes';
import { ViewPropTypes } from './View';
const ImageURISourcePropType = PropTypes.shape({
uri: PropTypes.string.isRequired,
height: PropTypes.number,
width: PropTypes.number,
// bundle: PropTypes.string,
// method: PropTypes.string,
// headers: PropTypes.objectOf(PropTypes.string),
// body: PropTypes.string,
// cache: PropTypes.oneOf(['default', 'reload', 'force-cache', 'only-if-cached']),
// scale: PropTypes.number,
});
export const ImageSourcePropType = PropTypes.oneOfType([
ImageURISourcePropType,
// PropTypes.arrayOf(ImageURISourcePropType), // TODO: handle me
PropTypes.string,
]);
const ResizeModes: { [key: string]: FileFormat.PatternFillType } = {
contain: 3,
cover: 1,
stretch: 2,
center: 1, // TODO(gold): implement ResizeModes.center
repeat: 0,
none: 1,
};
export const ImagePropTypes = {
...ViewPropTypes,
style: or([PropTypes.shape(ImageStylePropTypes), PropTypes.number]),
defaultSource: ImageSourcePropType,
resizeMode: ResizeModePropTypes,
source: ImageSourcePropType,
};
export type Props = PropTypes.InferProps;
export class Image extends React.Component {
static propTypes = ImagePropTypes;
static defaultProps = {
name: 'Image',
};
render() {
const { children, source, defaultSource, resizeMode, name, resizingConstraint } = this.props;
let style = StyleSheet.flatten(this.props.style) || {};
const sketchResizeMode = ResizeModes[resizeMode || (style && style.resizeMode) || 'cover'];
if (source && typeof source !== 'string') {
style = {
height: source.height,
width: source.width,
...style,
};
}
return (
{children}
);
}
}
================================================
FILE: src/components/ImageStylePropTypes.ts
================================================
import { ViewStylePropTypes } from './ViewStylePropTypes';
import { ResizeModePropTypes } from './ResizeModePropTypes';
export const ImageStylePropTypes = {
...ViewStylePropTypes,
resizeMode: ResizeModePropTypes,
};
================================================
FILE: src/components/Page.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { or } from 'airbnb-prop-types';
import { StyleSheet } from '../stylesheet';
import { PageStylePropTypes } from './PageStylePropTypes';
export const PagePropTypes = {
name: PropTypes.string,
children: PropTypes.node,
style: or([PropTypes.shape(PageStylePropTypes), PropTypes.number]),
};
type Props = PropTypes.InferProps;
export class Page extends React.Component {
static propTypes = PagePropTypes;
render() {
const { name, children, style, ...otherProps } = this.props;
const _name = name === 'Symbols' ? 'Symbols (renamed to avoid conflict)' : name;
const _style = StyleSheet.flatten(style);
return (
{children}
);
}
}
================================================
FILE: src/components/PageStylePropTypes.ts
================================================
export const PageStylePropTypes = {};
================================================
FILE: src/components/RedBox.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import ErrorStackParser from 'error-stack-parser';
import { Text } from './Text';
import { View } from './View';
type StackFrame = {
isConstrutor?: boolean;
isEval?: boolean;
isNative?: boolean;
isTopLevel?: boolean;
columnNumber?: number;
lineNumber?: number;
fileName?: string;
functionName?: string;
source?: string;
args?: any[];
evalOrigin?: StackFrame;
};
const styles = {
redbox: {
padding: 10,
width: 480,
backgroundColor: 'rgb(204, 0, 0)',
},
frame: {},
message: {
fontWeight: 'bold',
fontSize: 16,
lineHeight: 16 * 1.2,
color: 'white',
},
stack: {
fontFamily: 'Monaco',
marginTop: 20,
color: 'white',
},
};
export const ErrorBoxPropTypes = {
error: PropTypes.oneOfType([PropTypes.instanceOf(Error), PropTypes.string]).isRequired,
// filename: PropTypes.string,
// editorScheme: PropTypes.string,
// useLines: PropTypes.bool,
// useColumns: PropTypes.bool,
};
type Props = PropTypes.InferProps;
export class RedBox extends React.Component {
static propTypes = ErrorBoxPropTypes;
static defaultProps = {
useLines: true,
useColumns: true,
};
renderFrames(frames: Array) {
return frames.map((f, index) => (
{f.functionName}
));
}
render() {
const { error } = this.props;
if (typeof error === 'string') {
return (
{`Error: ${error}`}
);
}
let frames: ErrorStackParser.StackFrame[] | undefined;
let parseError: Error | undefined;
let frameChildren: JSX.Element[] | JSX.Element | undefined;
try {
frames = ErrorStackParser.parse(error);
} catch (e) {
parseError = new Error('Failed to parse stack trace. Stack trace information unavailable.');
}
if (parseError) {
frameChildren = (
{parseError.message}
);
}
if (frames) {
frameChildren = this.renderFrames(frames);
}
return (
{`${error.name}: ${error.message}`}
{frameChildren}
);
}
}
================================================
FILE: src/components/ResizeModePropTypes.ts
================================================
import * as PropTypes from 'prop-types';
export const ResizeModePropTypes = PropTypes.oneOf([
'contain',
'cover',
'stretch',
'center',
'repeat',
'none',
]);
================================================
FILE: src/components/ResizingConstraintPropTypes.ts
================================================
import * as PropTypes from 'prop-types';
export const ResizingConstraintPropTypes = {
top: PropTypes.bool,
right: PropTypes.bool,
bottom: PropTypes.bool,
left: PropTypes.bool,
fixedHeight: PropTypes.bool,
fixedWidth: PropTypes.bool,
};
================================================
FILE: src/components/ShadowsPropTypes.ts
================================================
import * as PropTypes from 'prop-types';
export const ShadowsPropTypes = {
shadowColor: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
shadowOffset: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
}),
shadowOpacity: PropTypes.number,
shadowRadius: PropTypes.number,
shadowSpread: PropTypes.number,
shadowInner: PropTypes.bool,
};
================================================
FILE: src/components/Svg/Circle.tsx
================================================
import * as React from 'react';
import { pathProps, numberProp } from './props';
import * as PropTypes from 'prop-types';
const propTypes = {
...pathProps,
cx: numberProp.isRequired,
cy: numberProp.isRequired,
r: numberProp.isRequired,
};
type Props = PropTypes.InferProps;
export class Circle extends React.Component {
static propTypes = propTypes;
static defaultProps = {
cx: 0,
cy: 0,
r: 0,
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/ClipPath.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
const propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class ClipPath extends React.Component {
static propTypes = propTypes;
render() {
return {this.props.children} ;
}
}
================================================
FILE: src/components/Svg/Defs.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
const propTypes = {
children: PropTypes.node.isRequired,
};
type Props = PropTypes.InferProps;
export class Defs extends React.Component {
static propTypes = propTypes;
render() {
return {this.props.children} ;
}
}
================================================
FILE: src/components/Svg/Ellipse.tsx
================================================
import * as React from 'react';
import { pathProps, numberProp } from './props';
import * as PropTypes from 'prop-types';
const propTypes = {
...pathProps,
cx: numberProp.isRequired,
cy: numberProp.isRequired,
rx: numberProp.isRequired,
ry: numberProp.isRequired,
};
type Props = PropTypes.InferProps;
export class Ellipse extends React.Component {
static propTypes = propTypes;
static defaultProps = {
cx: 0,
cy: 0,
rx: 0,
ry: 0,
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/G.tsx
================================================
import * as React from 'react';
import { pathProps, fontProps } from './props';
import * as PropTypes from 'prop-types';
const propTypes = {
...pathProps,
...fontProps,
};
type Props = PropTypes.InferProps;
export class G extends React.Component {
static propTypes = propTypes;
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Image.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { ImageSourcePropType } from '../Image';
import { numberProp } from './props';
const propTypes = {
x: numberProp,
y: numberProp,
width: numberProp.isRequired,
height: numberProp.isRequired,
href: ImageSourcePropType,
preserveAspectRatio: PropTypes.string,
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class SVGImage extends React.Component {
static propTypes = propTypes;
static defaultProps = {
x: 0,
y: 0,
width: 0,
height: 0,
preserveAspectRatio: 'xMidYMid meet',
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Line.tsx
================================================
import * as React from 'react';
import { pathProps, numberProp } from './props';
import * as PropTypes from 'prop-types';
const propTypes = {
...pathProps,
x1: numberProp.isRequired,
x2: numberProp.isRequired,
y1: numberProp.isRequired,
y2: numberProp.isRequired,
};
type Props = PropTypes.InferProps;
export class Line extends React.Component {
static propTypes = propTypes;
static defaultProps = {
x1: 0,
y1: 0,
x2: 0,
y2: 0,
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/LinearGradient.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { numberProp } from './props';
const propTypes = {
x1: numberProp.isRequired,
x2: numberProp.isRequired,
y1: numberProp.isRequired,
y2: numberProp.isRequired,
gradientUnits: PropTypes.oneOf(['objectBoundingBox', 'userSpaceOnUse']),
id: PropTypes.string.isRequired,
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class LinearGradient extends React.Component {
static propTypes = propTypes;
static defaultProps = {
x1: '0%',
y1: '0%',
x2: '100%',
y2: '0%',
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Path.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { pathProps } from './props';
const propTypes = {
...pathProps,
d: PropTypes.string.isRequired,
};
type Props = PropTypes.InferProps;
export class Path extends React.Component {
static propTypes = propTypes;
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Pattern.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { numberProp } from './props';
const propTypes = {
x1: numberProp,
x2: numberProp,
y1: numberProp,
y2: numberProp,
patternTransform: PropTypes.string,
patternUnits: PropTypes.oneOf(['userSpaceOnUse', 'objectBoundingBox']),
patternContentUnits: PropTypes.oneOf(['userSpaceOnUse', 'objectBoundingBox']),
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class Pattern extends React.Component {
static propTypes = propTypes;
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Polygon.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { pathProps } from './props';
const propTypes = {
...pathProps,
points: PropTypes.string.isRequired,
};
type Props = PropTypes.InferProps;
export class Polygon extends React.Component {
static displayName = 'Polygon';
static propTypes = propTypes;
static defaultProps = {
points: '',
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Polyline.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { pathProps } from './props';
const propTypes = {
...pathProps,
points: PropTypes.string.isRequired,
};
type Props = PropTypes.InferProps;
export class Polyline extends React.Component {
static propTypes = propTypes;
static defaultProps = {
points: '',
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/RadialGradient.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { numberProp } from './props';
const propTypes = {
fx: numberProp.isRequired,
fy: numberProp.isRequired,
rx: numberProp,
ry: numberProp,
cx: numberProp.isRequired,
cy: numberProp.isRequired,
r: numberProp,
gradientUnits: PropTypes.oneOf(['objectBoundingBox', 'userSpaceOnUse']),
id: PropTypes.string.isRequired,
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class RadialGradient extends React.Component {
static propTypes = propTypes;
static defaultProps = {
fx: '50%',
fy: '50%',
cx: '50%',
cy: '50%',
r: '50%',
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Rect.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { pathProps, numberProp } from './props';
const propTypes = {
...pathProps,
x: numberProp.isRequired,
y: numberProp.isRequired,
width: numberProp.isRequired,
height: numberProp.isRequired,
rx: numberProp,
ry: numberProp,
};
type Props = PropTypes.InferProps;
export class Rect extends React.Component {
static propTypes = propTypes;
static defaultProps = {
x: 0,
y: 0,
width: 0,
height: 0,
rx: 0,
ry: 0,
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Stop.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { numberProp } from './props';
const propTypes = {
stopColor: PropTypes.string,
stopOpacity: numberProp,
children: PropTypes.node,
};
type Props = PropTypes.InferProps;
export class Stop extends React.Component {
static propTypes = propTypes;
static defaultProps = {
stopColor: '#000',
stopOpacity: 1,
};
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Svg.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { ViewPropTypes } from '../View';
import { StyleSheet } from '../../stylesheet';
import { Circle } from './Circle';
import { ClipPath } from './ClipPath';
import { Defs } from './Defs';
import { Ellipse } from './Ellipse';
import { G } from './G';
import { SVGImage as Image } from './Image';
import { Line } from './Line';
import { LinearGradient } from './LinearGradient';
import { Path } from './Path';
import { Pattern } from './Pattern';
import { Polygon } from './Polygon';
import { Polyline } from './Polyline';
import { RadialGradient } from './RadialGradient';
import { Rect } from './Rect';
import { Stop } from './Stop';
import { Symbol } from './Symbol';
import { Text } from './Text';
import { TextPath } from './TextPath';
import { TSpan } from './TSpan';
import { Use } from './Use';
const propTypes = {
...ViewPropTypes,
opacity: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// more detail https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute
viewBox: PropTypes.string,
preserveAspectRatio: PropTypes.string,
xmlns: PropTypes.string,
'xmlns:xlink': PropTypes.string,
};
export type Props = PropTypes.InferProps;
export class Svg extends React.Component {
static displayName = 'Svg';
static propTypes = propTypes;
static defaultProps = {
preserveAspectRatio: 'xMidYMid meet',
};
static Circle = Circle;
static ClipPath = ClipPath;
static Defs = Defs;
static Ellipse = Ellipse;
static G = G;
static Image = Image;
static Line = Line;
static LinearGradient = LinearGradient;
static Path = Path;
static Pattern = Pattern;
static Polygon = Polygon;
static Polyline = Polyline;
static RadialGradient = RadialGradient;
static Rect = Rect;
static Stop = Stop;
static Symbol = Symbol;
static Text = Text;
static TextPath = TextPath;
static TSpan = TSpan;
static Use = Use;
render() {
const { children, style, ...rest } = this.props;
return (
{children}
);
}
}
================================================
FILE: src/components/Svg/Symbol.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
const propTypes = {
id: PropTypes.string.isRequired,
viewBox: PropTypes.string,
preserveAspectRatio: PropTypes.string,
children: PropTypes.node.isRequired,
};
type Props = PropTypes.InferProps;
export class Symbol extends React.Component {
static propTypes = propTypes;
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/TSpan.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { textProps } from './props';
type Props = PropTypes.InferProps;
export class TSpan extends React.Component {
static propTypes = textProps;
static childContextTypes = {
isInAParentText: PropTypes.bool,
};
getChildContext() {
return {
isInAParentText: true,
};
}
getContextTypes() {
return {
isInAParentText: PropTypes.bool,
};
}
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Text.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { textProps } from './props';
type Props = PropTypes.InferProps;
export class Text extends React.Component {
static propTypes = textProps;
static childContextTypes = {
isInAParentText: PropTypes.bool,
};
getChildContext() {
return {
isInAParentText: true,
};
}
getContextTypes() {
return {
isInAParentText: PropTypes.bool,
};
}
render() {
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/TextPath.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { textPathProps } from './props';
type Props = PropTypes.InferProps;
const idExpReg = /^#(.+)$/;
export class TextPath extends React.Component {
static propTypes = textPathProps;
render() {
if (!this.props.href || !this.props.href.match(idExpReg)) {
console.warn(
`Invalid \`href\` prop for \`TextPath\` element, expected a href like \`"#id"\`, but got: "${this.props.href}"`,
);
}
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/Use.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { pathProps, numberProp } from './props';
const propTypes = {
href: PropTypes.string.isRequired,
width: numberProp, // Just for reusing `Symbol`
height: numberProp, // Just for reusing `Symbol`
...pathProps,
};
type Props = PropTypes.InferProps;
const idExpReg = /^#(.+)$/;
export class Use extends React.Component {
static propTypes = propTypes;
render() {
const { href } = this.props;
// match "url(#pattern)"
const matched = href.match(idExpReg);
if (!href || !matched) {
console.warn(
`Invalid \`href\` prop for \`Use\` element, expected a href like \`"#id"\`, but got: "${href}"`,
);
}
const { children, ...rest } = this.props;
return {children} ;
}
}
================================================
FILE: src/components/Svg/index.tsx
================================================
export { Circle } from './Circle';
export { ClipPath } from './ClipPath';
export { Defs } from './Defs';
export { Ellipse } from './Ellipse';
export { G } from './G';
export { SVGImage as Image } from './Image';
export { Line } from './Line';
export { LinearGradient } from './LinearGradient';
export { Path } from './Path';
export { Pattern } from './Pattern';
export { Polygon } from './Polygon';
export { Polyline } from './Polyline';
export { RadialGradient } from './RadialGradient';
export { Rect } from './Rect';
export { Stop } from './Stop';
export { Symbol } from './Symbol';
export { Text } from './Text';
export { TextPath } from './TextPath';
export { TSpan } from './TSpan';
export { Use } from './Use';
export { Svg as default } from './Svg';
================================================
FILE: src/components/Svg/props.ts
================================================
import * as PropTypes from 'prop-types';
const numberProp = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
const numberArrayProp = PropTypes.oneOfType([PropTypes.arrayOf(numberProp), numberProp]);
const fillProps = {
fill: PropTypes.string,
fillOpacity: numberProp,
fillRule: PropTypes.oneOf(['evenodd', 'nonzero']),
};
const clipProps = {
clipRule: PropTypes.oneOf(['evenodd', 'nonzero']),
clipPath: PropTypes.string,
};
const definationProps = {
name: PropTypes.string,
};
const strokeProps = {
stroke: PropTypes.string,
strokeWidth: numberProp,
strokeOpacity: numberProp,
strokeDasharray: numberArrayProp,
strokeDashoffset: numberProp,
strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round']),
strokeLinejoin: PropTypes.oneOf(['miter', 'bevel', 'round']),
strokeAlignment: PropTypes.oneOf(['center', 'inner', 'outer']),
strokeMiterlimit: numberProp,
};
const transformProps = {
scale: numberProp,
scaleX: numberProp,
scaleY: numberProp,
rotate: numberProp,
rotation: numberProp,
translate: numberProp,
translateX: numberProp,
translateY: numberProp,
x: numberProp,
y: numberProp,
origin: numberProp,
originX: numberProp,
originY: numberProp,
skew: numberProp,
skewX: numberProp,
skewY: numberProp,
transform: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
};
const pathProps = {
...fillProps,
...strokeProps,
...clipProps,
...transformProps,
...definationProps,
};
// normal | italic | oblique | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-style
const fontStyle = PropTypes.oneOf(['normal', 'italic', 'oblique']);
// normal | small-caps | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-variant
const fontVariant = PropTypes.oneOf(['normal', 'small-caps']);
// normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-weight
const fontWeight = PropTypes.oneOf([
'normal',
'bold',
'bolder',
'lighter',
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
]);
// normal | wider | narrower | ultra-condensed | extra-condensed |
// condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-stretch
const fontStretch = PropTypes.oneOf([
'normal',
'wider',
'narrower',
'ultra-condensed',
'extra-condensed',
'condensed',
'semi-condensed',
'semi-expanded',
'expanded',
'extra-expanded',
'ultra-expanded',
]);
// | | | | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-size
const fontSize = numberProp;
// [[ | ],]* [ | ] | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-family
const fontFamily = PropTypes.string;
/*
font syntax [ [ <'font-style'> || ||
<'font-weight'> || <'font-stretch'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] |
caption | icon | menu | message-box | small-caption | status-bar
where = [ normal | small-caps ]
Shorthand property for setting ‘font-style’, ‘font-variant’,
‘font-weight’, ‘font-size’, ‘line-height’ and ‘font-family’.
The ‘line-height’ property has no effect on text layout in SVG.
Note: for the purposes of processing the ‘font’ property in SVG,
'line-height' is assumed to be equal the value for property ‘font-size’
https://www.w3.org/TR/SVG11/text.html#FontProperty
https://developer.mozilla.org/en-US/docs/Web/CSS/font
https://drafts.csswg.org/css-fonts-3/#font-prop
https://www.w3.org/TR/CSS2/fonts.html#font-shorthand
https://www.w3.org/TR/CSS1/#font
*/
const font = PropTypes.object;
// start | middle | end | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
const textAnchor = PropTypes.oneOf(['start', 'middle', 'end']);
// none | underline | overline | line-through | blink | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-decoration
const textDecoration = PropTypes.oneOf(['none', 'underline', 'overline', 'line-through', 'blink']);
// normal | | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/letter-spacing
const letterSpacing = numberProp;
// normal | | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/word-spacing
const wordSpacing = numberProp;
// auto | | inherit
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kerning
const kerning = numberProp;
/*
Name: font-variant-ligatures
Value: normal | none | [ || ||
|| ]
Initial: normal
Applies to: all elements
Inherited: yes
Percentages: N/A
Media: visual
Computed value: as specified
Animatable: no
Ligatures and contextual forms are ways of combining glyphs to produce more harmonized forms.
= [ common-ligatures | no-common-ligatures ]
= [ discretionary-ligatures | no-discretionary-ligatures ]
= [ historical-ligatures | no-historical-ligatures ]
= [ contextual | no-contextual ]
https://developer.mozilla.org/en/docs/Web/CSS/font-variant-ligatures
https://www.w3.org/TR/css-fonts-3/#font-variant-ligatures-prop
*/
const fontVariantLigatures = PropTypes.oneOf(['normal', 'none']);
const fontProps = {
fontStyle,
fontVariant,
fontWeight,
fontStretch,
fontSize,
fontFamily,
textAnchor,
textDecoration,
letterSpacing,
wordSpacing,
kerning,
fontVariantLigatures,
font,
};
/*
Name Value Initial value Animatable
lengthAdjust spacing | spacingAndGlyphs spacing yes
https://svgwg.org/svg2-draft/text.html#TextElementLengthAdjustAttribute
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/lengthAdjust
*/
const lengthAdjust = PropTypes.oneOf(['spacing', 'spacingAndGlyphs']);
/*
Name Value Initial value Animatable
textLength | | See below yes
https://svgwg.org/svg2-draft/text.html#TextElementTextLengthAttribute
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/textLength
*/
const textLength = numberProp;
/*
2.2. Transverse Box Alignment: the vertical-align property
Name: vertical-align
Value: <‘baseline-shift’> || <‘alignment-baseline’>
Initial: baseline
Applies to: inline-level boxes
Inherited: no
Percentages: N/A
Media: visual
Computed value: as specified
Canonical order: per grammar
Animation type: discrete
This shorthand property specifies how an inline-level box is aligned within the line.
Values are the same as for its longhand properties, see below.
Authors should use this property (vertical-align) instead of its longhands.
https://www.w3.org/TR/css-inline-3/#transverse-alignment
https://drafts.csswg.org/css-inline/#propdef-vertical-align
*/
const verticalAlign = numberProp;
/*
Name: alignment-baseline
1.1 Value: auto | baseline | before-edge | text-before-edge | middle | central |
after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | inherit
2.0 Value: baseline | text-bottom | alphabetic | ideographic | middle | central |
mathematical | text-top | bottom | center | top
Initial: baseline
Applies to: inline-level boxes, flex items, grid items, table cells
Inherited: no
Percentages: N/A
Media: visual
Computed value: as specified
Canonical order: per grammar
Animation type: discrete
https://drafts.csswg.org/css-inline/#propdef-alignment-baseline
https://www.w3.org/TR/SVG11/text.html#AlignmentBaselineProperty
https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
*/
const alignmentBaseline = PropTypes.oneOf([
'baseline',
'text-bottom',
'alphabetic',
'ideographic',
'middle',
'central',
'mathematical',
'text-top',
'bottom',
'center',
'top',
'text-before-edge',
'text-after-edge',
'before-edge',
'after-edge',
'hanging',
]);
/*
2.2.2. Alignment Shift: baseline-shift longhand
Name: baseline-shift
Value: | | sub | super
Initial: 0
Applies to: inline-level boxes
Inherited: no
Percentages: refer to the used value of line-height
Media: visual
Computed value: absolute length, percentage, or keyword specified
Animation type: discrete
This property specifies by how much the box is shifted up from its alignment point.
It does not apply when alignment-baseline is top or bottom.
https://www.w3.org/TR/css-inline-3/#propdef-baseline-shift
*/
const baselineShift = PropTypes.oneOfType([
PropTypes.oneOf(['sub', 'super', 'baseline']),
PropTypes.arrayOf(numberProp),
numberProp,
]);
/*
6.12. Low-level font feature settings control: the font-feature-settings property
Name: font-feature-settings
Value: normal | #
Initial: normal
Applies to: all elements
Inherited: yes
Percentages: N/A
Media: visual
Computed value: as specified
Animatable: no
This property provides low-level control over OpenType font features.
It is intended as a way of providing access to font features
that are not widely used but are needed for a particular use case.
Authors should generally use ‘font-variant’ and its related subproperties
whenever possible and only use this property for special cases where its use
is the only way of accessing a particular infrequently used font feature.
enable small caps and use second swash alternate
font-feature-settings: "smcp", "swsh" 2;
A value of ‘normal’ means that no change in glyph selection or positioning
occurs due to this property.
Feature tag values have the following syntax:
= [ | on | off ]?
The is a case-sensitive OpenType feature tag. As specified in the
OpenType specification, feature tags contain four ASCII characters.
Tag strings longer or shorter than four characters,
or containing characters outside the U+20–7E codepoint range are invalid.
Feature tags need only match a feature tag defined in the font,
so they are not limited to explicitly registered OpenType features.
Fonts defining custom feature tags should follow the tag name rules
defined in the OpenType specification [OPENTYPE-FEATURES].
Feature tags not present in the font are ignored;
a user agent must not attempt to synthesize fallback behavior based on these feature tags.
The one exception is that user agents may synthetically support the kern feature with fonts
that contain kerning data in the form of a ‘kern’ table but lack kern feature
support in the ‘GPOS’ table.
In general, authors should use the ‘font-kerning’ property to explicitly
enable or disable kerning
since this property always affects fonts with either type of kerning data.
If present, a value indicates an index used for glyph selection.
An value must be 0 or greater.
A value of 0 indicates that the feature is disabled.
For boolean features, a value of 1 enables the feature.
For non-boolean features, a value of 1 or greater enables the
feature and indicates the feature selection index.
A value of ‘on’ is synonymous with 1 and ‘off’ is synonymous with 0.
If the value is omitted, a value of 1 is assumed.
font-feature-settings: "dlig" 1; /* dlig=1 enable discretionary ligatures * /
font-feature-settings: "smcp" on; /* smcp=1 enable small caps * /
font-feature-settings: 'c2sc'; /* c2sc=1 enable caps to small caps * /
font-feature-settings: "liga" off; /* liga=0 no common ligatures * /
font-feature-settings: "tnum", 'hist'; /* tnum=1, hist=1 enable tabular numbers
and historical forms * /
font-feature-settings: "tnum" "hist"; /* invalid, need a comma-delimited list * /
font-feature-settings: "silly" off; /* invalid, tag too long * /
font-feature-settings: "PKRN"; /* PKRN=1 enable custom feature * /
font-feature-settings: dlig; /* invalid, tag must be a string * /
When values greater than the range supported by the font are specified,
the behavior is explicitly undefined.
For boolean features, in general these will enable the feature.
For non-boolean features, out of range values will in general be equivalent to a 0 value.
However, in both cases the exact behavior will depend upon the way the font is designed
(specifically, which type of lookup is used to define the feature).
Although specifically defined for OpenType feature tags,
feature tags for other modern font formats that support font features
may be added in the future.
Where possible, features defined for other font formats
should attempt to follow the pattern of registered OpenType tags.
The Japanese text below will be rendered with half-width kana characters:
body { font-feature-settings: "hwid"; /* Half-width OpenType feature * / }
毎日カレー食べてるのに、飽きない
https://drafts.csswg.org/css-fonts-3/#propdef-font-feature-settings
https://developer.mozilla.org/en/docs/Web/CSS/font-feature-settings
*/
const fontFeatureSettings = PropTypes.string;
const textSpecificProps = {
...pathProps,
...fontProps,
alignmentBaseline,
baselineShift,
verticalAlign,
lengthAdjust,
textLength,
fontData: PropTypes.object,
fontFeatureSettings,
};
// https://svgwg.org/svg2-draft/text.html#TSpanAttributes
const textProps = {
...textSpecificProps,
dx: numberArrayProp,
dy: numberArrayProp,
};
/*
Name
side
Value
left | right
initial value
left
Animatable
yes
https://svgwg.org/svg2-draft/text.html#TextPathElementSideAttribute
*/
const side = PropTypes.oneOf(['left', 'right']);
/*
Name
startOffset
Value
| |
initial value
0
Animatable
yes
https://svgwg.org/svg2-draft/text.html#TextPathElementStartOffsetAttribute
https://developer.mozilla.org/en/docs/Web/SVG/Element/textPath
*/
const startOffset = numberProp;
/*
Name
method
Value
align | stretch
initial value
align
Animatable
yes
https://svgwg.org/svg2-draft/text.html#TextPathElementMethodAttribute
https://developer.mozilla.org/en/docs/Web/SVG/Element/textPath
*/
const method = PropTypes.oneOf(['align', 'stretch']);
/*
Name
spacing
Value
auto | exact
initial value
exact
Animatable
yes
https://svgwg.org/svg2-draft/text.html#TextPathElementSpacingAttribute
https://developer.mozilla.org/en/docs/Web/SVG/Element/textPath
*/
const spacing = PropTypes.oneOf(['auto', 'exact']);
/*
Name
mid-line
Value
sharp | smooth
initial value
smooth
Animatable
yes
*/
const midLine = PropTypes.oneOf(['sharp', 'smooth']);
// https://svgwg.org/svg2-draft/text.html#TextPathAttributes
// https://developer.mozilla.org/en/docs/Web/SVG/Element/textPath
const textPathProps = {
...textSpecificProps,
href: PropTypes.string.isRequired,
startOffset,
method,
spacing,
side,
midLine,
};
export {
numberProp,
fillProps,
strokeProps,
fontProps,
textProps,
textPathProps,
clipProps,
pathProps,
};
================================================
FILE: src/components/Text.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { or } from 'airbnb-prop-types';
import { StyleSheet } from '../stylesheet';
import { TextStylePropTypes } from './TextStylePropTypes';
import { ViewPropTypes } from './View';
export const TextPropTypes = {
...ViewPropTypes,
style: or([PropTypes.shape(TextStylePropTypes), PropTypes.number]),
};
export type Props = PropTypes.InferProps;
/**
* @example
*
* Hello World!
*
*/
export class Text extends React.Component {
static propTypes = TextPropTypes;
render() {
return (
{this.props.children}
);
}
}
================================================
FILE: src/components/TextStylePropTypes.ts
================================================
import * as PropTypes from 'prop-types';
import { ViewStylePropTypes, Color } from './ViewStylePropTypes';
export const TextStylePropTypes = {
...ViewStylePropTypes,
fontFamily: PropTypes.string,
fontSize: PropTypes.number,
fontStyle: PropTypes.oneOf<'normal' | 'italic'>(['normal', 'italic']),
fontWeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
textDecoration: PropTypes.oneOf<'none' | 'underline' | 'double' | 'line-through'>([
'none',
'underline',
'double',
'line-through',
]),
textShadowOpacity: PropTypes.number,
textShadowSpread: PropTypes.number,
textShadowOffset: PropTypes.shape({ width: PropTypes.number, height: PropTypes.number }),
textShadowRadius: PropTypes.number,
textShadowColor: Color,
textTransform: PropTypes.oneOf<'uppercase' | 'lowercase'>(['uppercase', 'lowercase']),
letterSpacing: PropTypes.number,
lineHeight: PropTypes.number,
textAlign: PropTypes.oneOf<'auto' | 'left' | 'right' | 'center' | 'justify'>([
'auto',
'left',
'right',
'center',
'justify',
]),
paragraphSpacing: PropTypes.number,
writingDirection: PropTypes.oneOf<'auto' | 'ltr' | 'rtl'>(['auto', 'ltr', 'rtl']),
};
================================================
FILE: src/components/View.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { or } from 'airbnb-prop-types';
import { StyleSheet } from '../stylesheet';
import { ViewStylePropTypes } from './ViewStylePropTypes';
import { ResizingConstraintPropTypes } from './ResizingConstraintPropTypes';
import { ShadowsPropTypes } from './ShadowsPropTypes';
export const ViewPropTypes = {
// TODO(lmr): do some nice warning stuff like RN does
style: or([PropTypes.shape(ViewStylePropTypes), PropTypes.number]),
name: PropTypes.string,
resizingConstraint: PropTypes.shape({
...ResizingConstraintPropTypes,
}),
shadows: PropTypes.arrayOf(
PropTypes.shape({
...ShadowsPropTypes,
}),
),
flow: PropTypes.shape({
targetId: PropTypes.string,
target: PropTypes.string,
animationType: PropTypes.string,
}),
children: PropTypes.node,
};
export type Props = PropTypes.InferProps;
export class View extends React.Component {
static propTypes = ViewPropTypes;
static defaultProps = {
name: 'View',
};
render() {
return (
{this.props.children}
);
}
}
================================================
FILE: src/components/ViewStylePropTypes.ts
================================================
import * as PropTypes from 'prop-types';
export const BorderStyle = PropTypes.oneOf<'solid' | 'dotted' | 'dashed'>([
'solid',
'dotted',
'dashed',
]);
export const Color = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
export const Overflow = PropTypes.oneOf<'visible' | 'hidden' | 'scroll'>([
'visible',
'hidden',
'scroll',
]);
export const ViewStylePropTypes = {
display: PropTypes.oneOf(['flex', 'none']),
color: Color,
shadowColor: Color,
shadowInner: PropTypes.bool,
shadowSpread: PropTypes.number,
shadowOffset: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
}),
shadowOpacity: PropTypes.number,
shadowRadius: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
top: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
minWidth: PropTypes.number,
maxWidth: PropTypes.number,
minHeight: PropTypes.number,
maxHeight: PropTypes.number,
margin: PropTypes.number,
marginVertical: PropTypes.number,
marginHorizontal: PropTypes.number,
marginTop: PropTypes.number,
marginBottom: PropTypes.number,
marginLeft: PropTypes.number,
marginRight: PropTypes.number,
padding: PropTypes.number,
paddingVertical: PropTypes.number,
paddingHorizontal: PropTypes.number,
paddingTop: PropTypes.number,
paddingBottom: PropTypes.number,
paddingLeft: PropTypes.number,
paddingRight: PropTypes.number,
position: PropTypes.oneOf<'absolute' | 'relative'>(['absolute', 'relative']),
flexDirection: PropTypes.oneOf<'row' | 'row-reverse' | 'column' | 'column-reverse'>([
'row',
'row-reverse',
'column',
'column-reverse',
]),
flexWrap: PropTypes.oneOf<'wrap' | 'nowrap' | 'wrap-reverse'>(['wrap', 'nowrap', 'wrap-reverse']),
justifyContent: PropTypes.oneOf<
'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around'
>(['flex-start', 'flex-end', 'center', 'space-between', 'space-around']),
alignContent: PropTypes.oneOf<
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'stretch'
| 'baseline'
| 'auto'
>([
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'stretch',
'baseline',
'auto',
]),
alignItems: PropTypes.oneOf<'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'>([
'flex-start',
'flex-end',
'center',
'stretch',
'baseline',
]),
alignSelf: PropTypes.oneOf<
'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'
>(['auto', 'flex-start', 'flex-end', 'center', 'stretch', 'baseline']),
overflow: Overflow,
overflowX: Overflow,
overflowY: Overflow,
flex: PropTypes.number,
flexGrow: PropTypes.number,
flexShrink: PropTypes.number,
flexBasis: PropTypes.number,
aspectRatio: PropTypes.number,
zIndex: PropTypes.number,
backfaceVisibility: PropTypes.oneOf<'visible' | 'hidden'>(['visible', 'hidden']),
backgroundColor: Color,
borderColor: Color,
borderTopColor: Color,
borderRightColor: Color,
borderBottomColor: Color,
borderLeftColor: Color,
borderRadius: PropTypes.number,
borderTopLeftRadius: PropTypes.number,
borderTopRightRadius: PropTypes.number,
borderBottomLeftRadius: PropTypes.number,
borderBottomRightRadius: PropTypes.number,
borderStyle: BorderStyle,
borderTopStyle: BorderStyle,
borderRightStyle: BorderStyle,
borderBottomStyle: BorderStyle,
borderLeftStyle: BorderStyle,
borderWidth: PropTypes.number,
borderTopWidth: PropTypes.number,
borderRightWidth: PropTypes.number,
borderBottomWidth: PropTypes.number,
borderLeftWidth: PropTypes.number,
opacity: PropTypes.number,
transform: PropTypes.string,
transformOrigin: PropTypes.string,
};
================================================
FILE: src/components/index.ts
================================================
export { Document } from './Document';
export { Page } from './Page';
export { Artboard } from './Artboard';
export { Image } from './Image';
export { RedBox } from './RedBox';
export { View } from './View';
export { Text } from './Text';
import Svg from './Svg';
export { Svg };
================================================
FILE: src/context.tsx
================================================
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { Style } from './stylesheet/types';
const initialArtboardState = {
width: 0,
height: 0,
scale: 1,
fontScale: 1,
};
export const ArtboardContext = React.createContext({
state: initialArtboardState,
});
export const useWindowDimensions = () => {
const { state } = React.useContext(ArtboardContext);
const { width, height, scale, fontScale } = state || {};
return { width, height, scale, fontScale };
};
const ArtboardPropTypes = {
viewport: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
name: PropTypes.string,
scale: PropTypes.number,
fontScale: PropTypes.number,
}),
children: PropTypes.node,
};
type ArtboardProps = PropTypes.InferProps & {
children: any;
style?: Style;
};
export const ArtboardProvider = ({ children, viewport, style }: ArtboardProps) => {
if (!viewport) {
return children;
}
const { state: oState } = React.useContext(ArtboardContext);
const state = {
...oState,
width: viewport.width || (style || {}).width || oState.width,
height: viewport.height || (style || {}).height || oState.height,
scale: viewport.scale || oState.scale,
fontScale: viewport.fontScale || oState.scale,
};
return {children} ;
};
================================================
FILE: src/entrypoint.sketch.ts
================================================
import { render as _render } from './render';
import { renderToJSON as _renderToJSON } from './renderToJSON';
import { makeSymbol as _makeSymbol, SymbolMasterProps } from './symbol';
import {
SketchLayer,
WrappedSketchLayer,
SketchDocumentData,
WrappedSketchDocument,
SketchDocument,
PlatformBridge,
} from './types';
import { TextStyles as _TextStyles } from './sharedStyles/TextStyles';
import SketchBridge from './platformBridges/sketch';
export function render(
element: React.ReactElement,
container?: SketchLayer | WrappedSketchLayer,
platformBridge: PlatformBridge = SketchBridge,
) {
return _render(platformBridge)(element, container);
}
export function renderToJSON(
element: React.ReactElement,
platformBridge: PlatformBridge = SketchBridge,
) {
return _renderToJSON(platformBridge)(element);
}
export function makeSymbol(
Component: React.ComponentType,
symbolProps: string | SymbolMasterProps,
document: SketchDocumentData | SketchDocument | WrappedSketchDocument | undefined,
bridge: PlatformBridge = SketchBridge,
) {
return _makeSymbol(bridge)(Component, symbolProps, document);
}
export const TextStyles = _TextStyles(() => SketchBridge);
================================================
FILE: src/entrypoint.ts
================================================
import { render as _render } from './render';
import { renderToJSON as _renderToJSON } from './renderToJSON';
import { makeSymbol as _makeSymbol, SymbolMasterProps } from './symbol';
import {
SketchLayer,
WrappedSketchLayer,
SketchDocumentData,
WrappedSketchDocument,
SketchDocument,
PlatformBridge,
} from './types';
import { TextStyles as _TextStyles } from './sharedStyles/TextStyles';
function getDefaultPlatformBridge() {
return require('./platformBridges/macos').default;
}
export function render(
element: React.ReactElement,
container?: SketchLayer | WrappedSketchLayer,
platformBridge: PlatformBridge = getDefaultPlatformBridge(),
) {
return _render(platformBridge)(element, container);
}
export function renderToJSON(
element: React.ReactElement,
platformBridge: PlatformBridge = getDefaultPlatformBridge(),
) {
return _renderToJSON(platformBridge)(element);
}
export function makeSymbol(
Component: React.ComponentType,
symbolProps: string | SymbolMasterProps,
document: SketchDocumentData | SketchDocument | WrappedSketchDocument | undefined,
bridge: PlatformBridge = getDefaultPlatformBridge(),
) {
return _makeSymbol(bridge)(Component, symbolProps, document);
}
export const TextStyles = _TextStyles(() => getDefaultPlatformBridge());
================================================
FILE: src/flexToSketchJSON.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import * as renderers from './renderers';
import { SketchRenderer } from './renderers/SketchRenderer';
import { TreeNode, PlatformBridge } from './types';
function missingRendererError(type: string, annotations?: string) {
return new Error(
`Could not find renderer for type '${type}'.${annotations ? `\n${annotations}` : ''}`,
);
}
export const flexToSketchJSON = (bridge: PlatformBridge) => (
node: TreeNode | string,
):
| FileFormat.SymbolMaster
| FileFormat.Artboard
| FileFormat.Group
| FileFormat.ShapeGroup
| FileFormat.SymbolInstance => {
if (typeof node === 'string') {
throw missingRendererError('string');
}
const { type, children } = node;
// Give some insight as to why there might be issues
// specific to Page and Document components or SVG components
if (type === 'sketch_document') {
throw missingRendererError(
type,
'Be sure to only have components as children of .',
);
}
// @ts-ignore
const Renderer: typeof SketchRenderer | null = renderers[type];
if (Renderer == null) {
if (type.indexOf('svg') === 0) {
// the svg renderer should stop the walk down the tree so it shouldn't happen
throw missingRendererError(
type,
'Be sure to always have components as children of .',
);
}
throw missingRendererError(type);
}
const renderer = new Renderer(bridge);
const groupLayer = renderer.renderGroupLayer(node);
if (groupLayer._class === 'symbolInstance') {
return groupLayer;
}
const backingLayers = renderer.renderBackingLayers(node);
// stopping the walk down the tree if we have an svg
const curriedFlexToSketchJSON = flexToSketchJSON(bridge);
const sublayers =
children && type !== 'sketch_svg'
? children.map((child) => curriedFlexToSketchJSON(child))
: [];
// Filter out anything null, undefined
const layers = [...backingLayers, ...sublayers].filter((l) => l);
return { ...groupLayer, layers };
};
================================================
FILE: src/index.ts
================================================
export { Platform } from './Platform';
export { StyleSheet } from './stylesheet';
export { getSymbolComponentByName, getSymbolMasterByName, injectSymbols } from './symbol';
export { useWindowDimensions } from './context';
export * from './components';
export * from './entrypoint';
================================================
FILE: src/jsonUtils/borders.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeColorFromCSS, emptyGradient } from './models';
import { ViewStyle, LayoutInfo, BorderStyle, Color } from '../types';
import { same } from '../utils/same';
import { makeVerticalBorder, makeHorizontalBorder } from './shapeLayers';
import { makeBorderOptions } from './style';
const DEFAULT_BORDER_COLOR = 'transparent';
export const DEFAULT_BORDER_STYLE = 'solid';
export const createUniformBorder = (
width: number,
color: Color,
style: BorderStyle = DEFAULT_BORDER_STYLE,
position: FileFormat.BorderPosition = FileFormat.BorderPosition.Center,
lineCapStyle: FileFormat.LineCapStyle = FileFormat.LineCapStyle.Butt,
lineJoinStyle: FileFormat.LineJoinStyle = FileFormat.LineJoinStyle.Miter,
): { borderOptions: FileFormat.BorderOptions; borders: FileFormat.Border[] } => {
const borderOptions = makeBorderOptions(style, width, lineCapStyle, lineJoinStyle);
const borders: FileFormat.Border[] = [
{
_class: 'border',
isEnabled: true,
color: makeColorFromCSS(color),
fillType: FileFormat.FillType.Color,
position,
thickness: width,
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
gradient: emptyGradient,
},
];
return { borderOptions, borders };
};
export const createBorders = (
content: FileFormat.ShapeGroup,
layout: LayoutInfo,
style?: ViewStyle,
): FileFormat.ShapeGroup[] => {
if (!style) {
return [content];
}
const {
borderTopWidth = 0,
borderRightWidth = 0,
borderBottomWidth = 0,
borderLeftWidth = 0,
borderTopColor = DEFAULT_BORDER_COLOR,
borderRightColor = DEFAULT_BORDER_COLOR,
borderBottomColor = DEFAULT_BORDER_COLOR,
borderLeftColor = DEFAULT_BORDER_COLOR,
borderTopStyle = DEFAULT_BORDER_STYLE,
borderRightStyle = DEFAULT_BORDER_STYLE,
borderBottomStyle = DEFAULT_BORDER_STYLE,
borderLeftStyle = DEFAULT_BORDER_STYLE,
} = style;
if (
same(borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth) &&
same(borderTopColor, borderRightColor, borderBottomColor, borderLeftColor) &&
same(borderTopStyle, borderRightStyle, borderBottomStyle, borderLeftStyle)
) {
// all sides have same border width
// in this case, we can do everything with just a single shape.
if (borderTopStyle !== undefined && borderTopWidth !== null) {
const borderOptions = makeBorderOptions(borderTopStyle, borderTopWidth);
if (borderOptions && content.style) {
content.style.borderOptions = borderOptions;
}
}
if (borderTopWidth && borderTopWidth > 0 && content.style) {
content.style.borders = createUniformBorder(
borderTopWidth,
borderTopColor,
'solid',
FileFormat.BorderPosition.Outside,
).borders;
const backingLayer = content.layers ? content.layers[0] : undefined;
if (backingLayer) {
backingLayer.frame.x += borderTopWidth;
backingLayer.frame.y += borderTopWidth;
backingLayer.frame.width -= borderTopWidth * 2;
backingLayer.frame.height -= borderTopWidth * 2;
}
}
return [content];
}
content.hasClippingMask = true;
const layers = [content];
if (borderTopWidth && borderTopWidth > 0) {
const topBorder = makeHorizontalBorder(0, 0, layout.width, borderTopWidth, borderTopColor);
topBorder.name = 'Border (top)';
const borderOptions = makeBorderOptions(borderTopStyle, borderTopWidth);
if (borderOptions && topBorder.style) {
topBorder.style.borderOptions = borderOptions;
}
layers.push(topBorder);
}
if (borderRightWidth && borderRightWidth > 0) {
const rightBorder = makeVerticalBorder(
layout.width - borderRightWidth,
0,
layout.height,
borderRightWidth,
borderRightColor,
);
rightBorder.name = 'Border (right)';
const borderOptions = makeBorderOptions(borderRightStyle, borderRightWidth);
if (borderOptions && rightBorder.style) {
rightBorder.style.borderOptions = borderOptions;
}
layers.push(rightBorder);
}
if (borderBottomWidth && borderBottomWidth > 0) {
const bottomBorder = makeHorizontalBorder(
0,
layout.height - borderBottomWidth,
layout.width,
borderBottomWidth,
borderBottomColor,
);
bottomBorder.name = 'Border (bottom)';
const borderOptions = makeBorderOptions(borderBottomStyle, borderBottomWidth);
if (borderOptions && bottomBorder.style) {
bottomBorder.style.borderOptions = borderOptions;
}
layers.push(bottomBorder);
}
if (borderLeftWidth && borderLeftWidth > 0) {
const leftBorder = makeVerticalBorder(0, 0, layout.height, borderLeftWidth, borderLeftColor);
leftBorder.name = 'Border (left)';
const borderOptions = makeBorderOptions(borderLeftStyle, borderLeftWidth);
if (borderOptions && leftBorder.style) {
leftBorder.style.borderOptions = borderOptions;
}
layers.push(leftBorder);
}
return layers;
};
================================================
FILE: src/jsonUtils/computeTextTree.ts
================================================
import { ReactTestRendererNode } from 'react-test-renderer';
import { TextNode } from '../types';
import { Context } from '../utils/Context';
import { VALID_TEXT_CHILDREN_TYPES } from '../utils/constants';
const walkTextTree = (textTree: ReactTestRendererNode, context: Context, textNodes: TextNode[]) => {
if (typeof textTree !== 'string' && !VALID_TEXT_CHILDREN_TYPES.includes(textTree.type)) {
throw new Error(`"${textTree.type}" is not a valid child for Text components`);
}
if (typeof textTree === 'string') {
textNodes.push({
textStyles: context.getInheritedStyles(),
content: textTree,
});
return;
}
if (textTree.children) {
if (textTree.props && textTree.props.style) {
context.addInheritableStyles(textTree.props.style);
}
for (let index = 0; index < textTree.children.length; index += 1) {
const textComponent = textTree.children[index];
walkTextTree(textComponent, context.forChildren(), textNodes);
}
}
};
export const computeTextTree = (
node: ReactTestRendererNode,
context: Context,
textNodes: TextNode[] = [],
) => {
if (typeof node === 'string') {
return [
{
textStyles: context.getInheritedStyles(),
content: node,
},
];
}
const { children } = node;
if (children) {
const childContext = context.forChildren();
for (let index = 0; index < children.length; index += 1) {
const textNode = children[index];
if (typeof textNode === 'string') {
textNodes.push({
content: textNode,
textStyles: childContext.getInheritedStyles(),
});
} else if (textNode.children && textNode.children.length > 0) {
walkTextTree(textNode, childContext, textNodes);
}
}
}
return textNodes;
};
================================================
FILE: src/jsonUtils/computeYogaNode.ts
================================================
import yoga from 'yoga-layout-prebuilt';
import { ReactTestRendererNode } from 'react-test-renderer';
import { ViewStyle, PlatformBridge } from '../types';
import { Context } from '../utils/Context';
import { createStringMeasurer } from '../utils/createStringMeasurer';
import { hasAnyDefined } from '../utils/hasAnyDefined';
import { pick } from '../utils/pick';
import { computeTextTree } from './computeTextTree';
import { INHERITABLE_FONT_STYLES } from '../utils/constants';
import { isDefined } from '../utils/isDefined';
import { getSymbolMasterById } from '../symbol';
// flatten all styles (including nested) into one object
export const getStyles = (node: ReactTestRendererNode): ViewStyle => {
if (typeof node === 'string') {
return {};
}
let { style } = node.props;
if (Array.isArray(style)) {
const flattened = Array.prototype.concat.apply([], style);
const themeFlattened = Array.prototype.concat.apply([], flattened);
const objectsOnly = themeFlattened.filter((f) => f);
style = Object.assign({}, ...objectsOnly);
}
return style;
};
export const computeYogaNode = (bridge: PlatformBridge) => (
node: ReactTestRendererNode,
context: Context,
): { node: yoga.YogaNode; stop?: boolean } => {
const yogaNode = yoga.Node.create();
const hasStyle = typeof node !== 'string' && node.props && node.props.style;
const style: ViewStyle = hasStyle ? getStyles(node) : {};
// Setup default symbol instance dimensions
if (typeof node !== 'string' && node.type === 'sketch_symbolinstance') {
const symbolProps = node.props;
const symbolMaster = getSymbolMasterById(symbolProps.symbolID);
if (!symbolMaster) {
throw new Error('Cannot find Symbol Master with id ' + symbolProps.symbolID);
}
const { frame } = symbolMaster;
yogaNode.setWidth(frame.width);
yogaNode.setHeight(frame.height);
}
if (typeof node !== 'string' && node.type === 'sketch_svg') {
const svgProps = node.props;
// Width
if (isDefined(svgProps.width)) {
yogaNode.setWidth(svgProps.width);
}
// Height
if (isDefined(svgProps.height)) {
yogaNode.setHeight(svgProps.height);
}
}
if (hasStyle) {
// http://facebook.github.io/react-native/releases/0.48/docs/layout-props.html
// Width
if (isDefined(style.width)) {
yogaNode.setWidth(style.width);
}
// Height
if (isDefined(style.height)) {
yogaNode.setHeight(style.height);
}
// Min-Height
if (isDefined(style.minHeight)) {
yogaNode.setMinHeight(style.minHeight);
}
// Min-Width
if (isDefined(style.minWidth)) {
yogaNode.setMinWidth(style.minWidth);
}
// Max-Height
if (isDefined(style.maxHeight)) {
yogaNode.setMaxHeight(style.maxHeight);
}
// Min-Width
if (isDefined(style.maxWidth)) {
yogaNode.setMaxWidth(style.maxWidth);
}
// Margin
if (isDefined(style.marginTop)) {
yogaNode.setMargin(yoga.EDGE_TOP, style.marginTop);
}
if (isDefined(style.marginBottom)) {
yogaNode.setMargin(yoga.EDGE_BOTTOM, style.marginBottom);
}
if (isDefined(style.marginLeft)) {
yogaNode.setMargin(yoga.EDGE_LEFT, style.marginLeft);
}
if (isDefined(style.marginRight)) {
yogaNode.setMargin(yoga.EDGE_RIGHT, style.marginRight);
}
if (isDefined(style.marginVertical)) {
yogaNode.setMargin(yoga.EDGE_VERTICAL, style.marginVertical);
}
if (isDefined(style.marginHorizontal)) {
yogaNode.setMargin(yoga.EDGE_HORIZONTAL, style.marginHorizontal);
}
if (isDefined(style.margin)) {
yogaNode.setMargin(yoga.EDGE_ALL, style.margin);
}
// Padding
if (isDefined(style.paddingTop)) {
yogaNode.setPadding(yoga.EDGE_TOP, style.paddingTop);
}
if (isDefined(style.paddingBottom)) {
yogaNode.setPadding(yoga.EDGE_BOTTOM, style.paddingBottom);
}
if (isDefined(style.paddingLeft)) {
yogaNode.setPadding(yoga.EDGE_LEFT, style.paddingLeft);
}
if (isDefined(style.paddingRight)) {
yogaNode.setPadding(yoga.EDGE_RIGHT, style.paddingRight);
}
if (isDefined(style.paddingVertical)) {
yogaNode.setPadding(yoga.EDGE_VERTICAL, style.paddingVertical);
}
if (isDefined(style.paddingHorizontal)) {
yogaNode.setPadding(yoga.EDGE_HORIZONTAL, style.paddingHorizontal);
}
if (isDefined(style.padding)) {
yogaNode.setPadding(yoga.EDGE_ALL, style.padding);
}
// Border
if (isDefined(style.borderTopWidth)) {
yogaNode.setBorder(yoga.EDGE_TOP, style.borderTopWidth);
}
if (isDefined(style.borderBottomWidth)) {
yogaNode.setBorder(yoga.EDGE_BOTTOM, style.borderBottomWidth);
}
if (isDefined(style.borderLeftWidth)) {
yogaNode.setBorder(yoga.EDGE_LEFT, style.borderLeftWidth);
}
if (isDefined(style.borderRightWidth)) {
yogaNode.setBorder(yoga.EDGE_RIGHT, style.borderRightWidth);
}
if (isDefined(style.borderWidth)) {
yogaNode.setBorder(yoga.EDGE_ALL, style.borderWidth);
}
// Flex
if (isDefined(style.flex)) {
yogaNode.setFlex(style.flex);
}
if (isDefined(style.flexGrow)) {
yogaNode.setFlexGrow(style.flexGrow);
}
if (isDefined(style.flexShrink)) {
yogaNode.setFlexShrink(style.flexShrink);
}
if (isDefined(style.flexBasis)) {
yogaNode.setFlexBasis(style.flexBasis);
}
// Position
if (style.position === 'absolute') {
yogaNode.setPositionType(yoga.POSITION_TYPE_ABSOLUTE);
}
if (style.position === 'relative') {
yogaNode.setPositionType(yoga.POSITION_TYPE_RELATIVE);
}
if (isDefined(style.top)) {
yogaNode.setPosition(yoga.EDGE_TOP, style.top);
}
if (isDefined(style.left)) {
yogaNode.setPosition(yoga.EDGE_LEFT, style.left);
}
if (isDefined(style.right)) {
yogaNode.setPosition(yoga.EDGE_RIGHT, style.right);
}
if (isDefined(style.bottom)) {
yogaNode.setPosition(yoga.EDGE_BOTTOM, style.bottom);
}
// Display
if (style.display) {
if (style.display === 'flex') {
yogaNode.setDisplay(yoga.DISPLAY_FLEX);
}
if (style.display === 'none') {
yogaNode.setDisplay(yoga.DISPLAY_NONE);
}
}
// Overflow
if (style.overflow) {
if (style.overflow === 'visible') {
yogaNode.setOverflow(yoga.OVERFLOW_VISIBLE);
}
if (style.overflow === 'scroll') {
yogaNode.setOverflow(yoga.OVERFLOW_SCROLL);
}
if (style.overflow === 'hidden') {
yogaNode.setOverflow(yoga.OVERFLOW_HIDDEN);
}
}
// Flex direction
if (style.flexDirection) {
if (style.flexDirection === 'row') {
yogaNode.setFlexDirection(yoga.FLEX_DIRECTION_ROW);
}
if (style.flexDirection === 'column') {
yogaNode.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN);
}
if (style.flexDirection === 'row-reverse') {
yogaNode.setFlexDirection(yoga.FLEX_DIRECTION_ROW_REVERSE);
}
if (style.flexDirection === 'column-reverse') {
yogaNode.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN_REVERSE);
}
}
// Justify Content
if (style.justifyContent) {
if (style.justifyContent === 'flex-start') {
yogaNode.setJustifyContent(yoga.JUSTIFY_FLEX_START);
}
if (style.justifyContent === 'flex-end') {
yogaNode.setJustifyContent(yoga.JUSTIFY_FLEX_END);
}
if (style.justifyContent === 'center') {
yogaNode.setJustifyContent(yoga.JUSTIFY_CENTER);
}
if (style.justifyContent === 'space-between') {
yogaNode.setJustifyContent(yoga.JUSTIFY_SPACE_BETWEEN);
}
if (style.justifyContent === 'space-around') {
yogaNode.setJustifyContent(yoga.JUSTIFY_SPACE_AROUND);
}
}
// Align Content
if (style.alignContent) {
if (style.alignContent === 'flex-start') {
yogaNode.setAlignContent(yoga.ALIGN_FLEX_START);
}
if (style.alignContent === 'flex-end') {
yogaNode.setAlignContent(yoga.ALIGN_FLEX_END);
}
if (style.alignContent === 'center') {
yogaNode.setAlignContent(yoga.ALIGN_CENTER);
}
if (style.alignContent === 'stretch') {
yogaNode.setAlignContent(yoga.ALIGN_STRETCH);
}
if (style.alignContent === 'baseline') {
yogaNode.setAlignContent(yoga.ALIGN_BASELINE);
}
if (style.alignContent === 'space-between') {
yogaNode.setAlignContent(yoga.ALIGN_SPACE_BETWEEN);
}
if (style.alignContent === 'space-around') {
yogaNode.setAlignContent(yoga.ALIGN_SPACE_AROUND);
}
if (style.alignContent === 'auto') {
yogaNode.setAlignContent(yoga.ALIGN_AUTO);
}
}
// Align Items
if (style.alignItems) {
if (style.alignItems === 'flex-start') {
yogaNode.setAlignItems(yoga.ALIGN_FLEX_START);
}
if (style.alignItems === 'flex-end') {
yogaNode.setAlignItems(yoga.ALIGN_FLEX_END);
}
if (style.alignItems === 'center') {
yogaNode.setAlignItems(yoga.ALIGN_CENTER);
}
if (style.alignItems === 'stretch') {
yogaNode.setAlignItems(yoga.ALIGN_STRETCH);
}
if (style.alignItems === 'baseline') {
yogaNode.setAlignItems(yoga.ALIGN_BASELINE);
}
}
// Align Self
if (style.alignSelf) {
if (style.alignSelf === 'flex-start') {
yogaNode.setAlignSelf(yoga.ALIGN_FLEX_START);
}
if (style.alignSelf === 'flex-end') {
yogaNode.setAlignSelf(yoga.ALIGN_FLEX_END);
}
if (style.alignSelf === 'center') {
yogaNode.setAlignSelf(yoga.ALIGN_CENTER);
}
if (style.alignSelf === 'stretch') {
yogaNode.setAlignSelf(yoga.ALIGN_STRETCH);
}
if (style.alignSelf === 'baseline') {
yogaNode.setAlignSelf(yoga.ALIGN_BASELINE);
}
}
// Flex Wrap
if (style.flexWrap) {
if (style.flexWrap === 'nowrap') {
yogaNode.setFlexWrap(yoga.WRAP_NO_WRAP);
}
if (style.flexWrap === 'wrap') {
yogaNode.setFlexWrap(yoga.WRAP_WRAP);
}
if (style.flexWrap === 'wrap-reverse') {
yogaNode.setFlexWrap(yoga.WRAP_WRAP_REVERSE);
}
}
}
if (typeof node === 'string' || node.type === 'sketch_text') {
// If current node is a Text node, add text styles to Context to pass down to
// child nodes.
if (
typeof node !== 'string' &&
node.props &&
node.props.style &&
hasAnyDefined(style, INHERITABLE_FONT_STYLES)
) {
// @ts-ignore
const inheritableStyles = pick(style, INHERITABLE_FONT_STYLES);
context.addInheritableStyles(inheritableStyles);
}
// Handle Text Children
const textNodes = computeTextTree(node, context);
yogaNode.setMeasureFunc(createStringMeasurer(bridge)(textNodes));
return { node: yogaNode, stop: true };
}
return { node: yogaNode };
};
================================================
FILE: src/jsonUtils/computeYogaTree.ts
================================================
import yoga from 'yoga-layout-prebuilt';
import { ReactTestRendererNode } from 'react-test-renderer';
import { PlatformBridge } from '../types';
import { computeYogaNode } from './computeYogaNode';
import { Context } from '../utils/Context';
import { zIndex } from '../utils/zIndex';
const walkTree = (bridge: PlatformBridge) => (tree: ReactTestRendererNode, context: Context) => {
const { node, stop } = computeYogaNode(bridge)(tree, context);
if (typeof tree === 'string' || tree.type === 'sketch_svg') {
// handle svg node, eg: stop here, we will handle the children in the renderer
return node;
}
if (tree.children && tree.children.length > 0) {
// Calculates zIndex order
const children = zIndex(tree.children);
for (let index = 0; index < children.length; index += 1) {
const childComponent = children[index];
// Avoid going into node's children
if (!stop) {
const childNode = walkTree(bridge)(childComponent, context.forChildren());
node.insertChild(childNode, index);
}
}
}
return node;
};
export const computeYogaTree = (bridge: PlatformBridge) => (
root: ReactTestRendererNode,
context: Context,
): yoga.YogaNode => walkTree(bridge)(root, context);
================================================
FILE: src/jsonUtils/hotspotLayer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { generateID } from './models';
const animationTypes = {
none: -1,
slideFromRight: 0,
slideFromLeft: 1,
slideFromBottom: 2,
slideFromTop: 3,
};
const BackTarget = 'back';
const getArtboard = (target: string) => {
if (target === BackTarget) {
return BackTarget;
}
return generateID(`artboard:${target}`, true);
};
export const hotspotLayer = ({
targetId,
target,
animationType,
}: {
targetId?: string;
target?: string;
animationType?: 'none' | 'slideFromRight' | 'slideFromLeft' | 'slideFromBottom' | 'slideFromTop';
}): { flow: FileFormat.FlowConnection } => ({
flow: {
_class: 'MSImmutableFlowConnection',
animationType: (animationType && animationTypes[animationType]) || -1,
destinationArtboardID: target ? getArtboard(target) : targetId || 'broken',
},
});
================================================
FILE: src/jsonUtils/layerGroup.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeResizeConstraint } from './resizeConstraint';
import { generateID, makeRect } from './models';
import { ResizeConstraints } from '../types';
import { makeStyle } from './style';
export const layerGroup = (
x: number,
y: number,
width: number,
height: number,
opacity: number,
resizingConstraint?: ResizeConstraints,
): FileFormat.Group => ({
_class: 'group',
do_objectID: generateID(),
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
frame: makeRect(x, y, width, height),
isFlippedHorizontal: false,
isFlippedVertical: false,
isLocked: false,
isVisible: true,
layerListExpandedType: FileFormat.LayerListExpanded.Expanded,
name: 'Group',
nameIsFixed: false,
resizingConstraint: makeResizeConstraint(resizingConstraint),
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
style: makeStyle({ opacity }),
hasClickThrough: false,
layers: [],
booleanOperation: FileFormat.BooleanOperation.NA,
isFixedToViewport: false,
});
================================================
FILE: src/jsonUtils/makeSvgLayer/graphics/curvePoint.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { Point } from './types';
export function describePoint(point: Point): string {
const { x, y } = point;
return `{${x}, ${y}}`;
}
export function makeCurvePoint(
point: Point,
curveFrom?: Point,
curveTo?: Point,
curveMode?: FileFormat.CurveMode,
): FileFormat.CurvePoint {
return {
_class: 'curvePoint',
cornerRadius: 0,
curveFrom: describePoint(curveFrom || point),
curveMode: curveMode || 0,
curveTo: describePoint(curveTo || point),
hasCurveFrom: !!curveFrom,
hasCurveTo: !!curveTo,
point: describePoint(point),
};
}
================================================
FILE: src/jsonUtils/makeSvgLayer/graphics/path.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { normalizePointInRect } from './point';
import { makeCurvePoint, describePoint } from './curvePoint';
type Path = Pick;
function makePath(curvePoints: FileFormat.CurvePoint[], isClosed: boolean): Path {
return {
isClosed,
points: curvePoints,
};
}
// Points are normalized between 0 and 1, relative to the frame.
// We use the original frame here and can scale it later.
//
// This is a rough port of Lona's PDF to Sketch path conversion
// https://github.com/airbnb/Lona/blob/94fd0b26de3e3f4b4496cdaa4ab31c6d258dc4ac/studio/LonaStudio/Utils/Sketch.swift#L285
export function makePathsFromCommands(
commands: { type: string; data: any }[],
frame: FileFormat.Rect,
): Path[] {
const paths: Path[] = [];
let curvePoints: FileFormat.CurvePoint[] = [];
function finishPath(isClosed: boolean) {
if (curvePoints.length === 0) return;
const path = makePath(curvePoints, isClosed);
paths.push(path);
curvePoints = [];
}
commands.forEach((command: any) => {
const { type, data } = command;
switch (type) {
case 'move': {
finishPath(false);
const { to } = data;
const curvePoint = makeCurvePoint(normalizePointInRect(to, frame), undefined, undefined, 1);
curvePoints.push(curvePoint);
break;
}
case 'line': {
const { to } = data;
const curvePoint = makeCurvePoint(normalizePointInRect(to, frame), undefined, undefined, 1);
curvePoints.push(curvePoint);
break;
}
case 'cubicCurve': {
const { to, controlPoint1, controlPoint2 } = data;
if (curvePoints.length > 0) {
const last = curvePoints[curvePoints.length - 1];
last.curveFrom = describePoint(normalizePointInRect(controlPoint1, frame));
last.curveMode = 2;
last.hasCurveFrom = true;
}
const curvePoint = makeCurvePoint(
normalizePointInRect(to, frame),
undefined,
normalizePointInRect(controlPoint2, frame),
2,
);
curvePoints.push(curvePoint);
break;
}
case 'close': {
// If first and last points are equal, combine them
if (curvePoints.length > 0) {
const first = curvePoints[0];
const last = curvePoints[curvePoints.length - 1];
if (first.point == last.point && last.hasCurveTo) {
first.curveTo = last.curveTo;
first.hasCurveTo = last.hasCurveTo;
first.curveMode = 2;
curvePoints.pop();
}
}
finishPath(true);
break;
}
default:
throw new Error(`Invalid SVG path command: ${type}`);
}
});
finishPath(false);
return paths;
}
export function makeLineCapStyle(strokeLineCap: 'butt' | 'round' | 'square'): 0 | 1 | 2 {
switch (strokeLineCap) {
case 'butt':
return 0;
case 'round':
return 1;
case 'square':
return 2;
default:
throw new Error(`Invalid SVG stroke line cap: ${strokeLineCap}`);
}
}
================================================
FILE: src/jsonUtils/makeSvgLayer/graphics/point.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { Point } from './types';
export function normalizePointInRect(point: Point, rect: FileFormat.Rect): Point {
const x = (point.x - rect.x) / rect.width;
const y = (point.y - rect.y) / rect.height;
return { x, y };
}
================================================
FILE: src/jsonUtils/makeSvgLayer/graphics/rect.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeRect } from '../../models';
import { Point, Size } from './types';
export function makeBoundingRectFromPoints(points: Point[]): FileFormat.Rect {
const x = Math.min(...points.map((point) => point.x));
const y = Math.min(...points.map((point) => point.y));
const width = Math.max(...points.map((point) => point.x)) - x;
const height = Math.max(...points.map((point) => point.y)) - y;
return makeRect(x, y, width, height);
}
export function makeBoundingRectFromCommands(commands: any): FileFormat.Rect {
const points: Point[] = commands.reduce((acc: Point[], command: any) => {
const { type, data } = command;
switch (type) {
case 'line':
case 'move': {
const { to } = data;
return [...acc, to];
}
case 'cubicCurve': {
const { to, controlPoint1, controlPoint2 } = data;
return [...acc, to, controlPoint1, controlPoint2];
}
case 'close':
return acc;
default:
throw new Error(`Invalid SVG path command: ${type}`);
}
}, []);
return makeBoundingRectFromPoints(points);
}
export function unionRects(...rects: FileFormat.Rect[]): FileFormat.Rect {
function union(a: FileFormat.Rect, b: FileFormat.Rect) {
const minX = Math.min(a.x, b.x);
const minY = Math.min(a.y, b.y);
const maxX = Math.max(a.x + a.width, b.x + b.width);
const maxY = Math.max(a.y + a.height, b.y + b.height);
return makeRect(minX, minY, maxX - minX, maxY - minY);
}
if (rects.length === 0) {
throw new Error('No rects to union');
}
return rects.reduce((acc, rect) => union(acc, rect), rects[0]);
}
export function scaleRect(rect: FileFormat.Rect, scale: number) {
return makeRect(rect.x * scale, rect.y * scale, rect.width * scale, rect.height * scale);
}
// Port of Lona's resizing algorithm
// https://github.com/airbnb/Lona/blob/94fd0b26de3e3f4b4496cdaa4ab31c6d258dc4ac/examples/generated/test/swift/CGSize%2BResizing.swift
export function resize(
source: Size,
destination: Size,
resizingMode: 'cover' | 'contain' | 'stretch',
) {
const newSize = { ...destination };
const sourceAspectRatio = source.height / source.width;
const destinationAspectRatio = destination.height / destination.width;
const sourceIsWiderThanDestination = sourceAspectRatio < destinationAspectRatio;
switch (resizingMode) {
case 'contain':
if (sourceIsWiderThanDestination) {
newSize.height = destination.width * sourceAspectRatio;
} else {
newSize.width = destination.height / sourceAspectRatio;
}
break;
case 'cover':
if (sourceIsWiderThanDestination) {
newSize.width = destination.height / sourceAspectRatio;
} else {
newSize.height = destination.width * sourceAspectRatio;
}
break;
case 'stretch':
break;
default:
throw new Error('Invalid resizing mode');
}
return makeRect(
(destination.width - newSize.width) / 2.0,
(destination.height - newSize.height) / 2.0,
newSize.width,
newSize.height,
);
}
================================================
FILE: src/jsonUtils/makeSvgLayer/graphics/types.ts
================================================
export type Point = { x: number; y: number };
export type Size = { width: number; height: number };
================================================
FILE: src/jsonUtils/makeSvgLayer/index.sketch.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { toSJSON } from '../sketchJson/toSJSON';
import { LayoutInfo } from '../../types';
export function makeSvgLayer(_layout: LayoutInfo, name: string, svg: string): FileFormat.Group {
const svgString = NSString.stringWithString(svg);
const svgData = svgString.dataUsingEncoding(NSUTF8StringEncoding);
const svgImporter = MSSVGImporter.svgImporter();
svgImporter.prepareToImportFromData(svgData);
const frame = NSMakeRect(0, 0, svgImporter.graph().width(), svgImporter.graph().height());
const root = MSLayerGroup.alloc().initWithFrame(frame);
root.name = name;
svgImporter.graph().makeLayerWithParentLayer_progress(root, null);
root.ungroupSingleChildDescendentGroups();
svgImporter.scale_rootGroup(svgImporter.importer().scaleValue(), root);
return toSJSON(root) as FileFormat.Group;
}
================================================
FILE: src/jsonUtils/makeSvgLayer/index.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import svgModel from '@lona/svg-model';
import { LayoutInfo, ViewStyle } from '../../types';
import { makeShapeGroup, makeShapePath } from '../shapeLayers';
import { makeRect } from '../models';
import { createUniformBorder } from '../borders';
import { layerGroup } from '../layerGroup';
import { makePathsFromCommands, makeLineCapStyle } from './graphics/path';
import { unionRects, scaleRect, makeBoundingRectFromCommands, resize } from './graphics/rect';
function makeLayerFromPathElement(pathElement: any, _parentFrame: FileFormat.Rect, scale: number) {
const {
data: {
params: { commands, style },
},
} = pathElement;
// Paths are created using the original frame
const pathFrame = makeBoundingRectFromCommands(commands);
const paths = makePathsFromCommands(commands, pathFrame);
// Scale the frame to fill the viewBox
const shapeGroupFrame = scaleRect(pathFrame, scale);
// Each shape path has an origin of {0, 0}, since the shapeGroup layer stores the real origin,
// and we don't want to apply the origin translation twice.
const shapePathFrame = makeRect(0, 0, shapeGroupFrame.width, shapeGroupFrame.height);
const shapePaths = paths.map((path) => makeShapePath(shapePathFrame, path));
const viewStyle: ViewStyle = {};
if (style.fill) {
viewStyle.backgroundColor = style.fill;
}
const shapeGroup = makeShapeGroup(shapeGroupFrame, shapePaths, viewStyle);
if (style.stroke && shapeGroup.style) {
const lineCap = makeLineCapStyle(style.strokeLineCap);
const borderStyle = createUniformBorder(
style.strokeWidth * scale,
style.stroke,
undefined,
FileFormat.BorderPosition.Center,
lineCap,
lineCap,
);
shapeGroup.style = { ...shapeGroup.style, ...borderStyle };
}
return shapeGroup;
}
function makeLayerGroup(
frame: FileFormat.Rect,
layers: (
| FileFormat.Group
| FileFormat.Oval
| FileFormat.Polygon
| FileFormat.Rectangle
| FileFormat.ShapePath
| FileFormat.Star
| FileFormat.Triangle
| FileFormat.ShapeGroup
| FileFormat.Text
| FileFormat.SymbolMaster
| FileFormat.SymbolInstance
| FileFormat.Slice
| FileFormat.Hotspot
| FileFormat.Bitmap
)[],
name: string,
) {
const group = layerGroup(frame.x, frame.y, frame.width, frame.height, 1);
group.name = name;
group.layers = layers;
return group;
}
export function makeSvgLayer(layout: LayoutInfo, name: string, svg: string) {
const {
data: { params, children },
} = svgModel(svg);
const {
viewBox = {
x: layout.left,
y: layout.top,
width: layout.width,
height: layout.height,
},
} = params;
// Determine the rect to generate layers within
const croppedRect = resize(viewBox, layout, 'contain');
const scale = croppedRect.width / viewBox.width;
// The top-level frame is the union of every path within
const frame = unionRects(
...children.map((pathElement: any) =>
makeBoundingRectFromCommands(pathElement.data.params.commands),
),
);
// Scale the frame to fill the viewBox
const scaledFrame = scaleRect(frame, scale);
const layers = children.map((element: any) =>
makeLayerFromPathElement(element, scaledFrame, scale),
);
return makeLayerGroup(croppedRect, layers, name);
}
================================================
FILE: src/jsonUtils/models.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import seedrandom from 'seedrandom';
import normalizeColor, { rgba } from 'normalize-css-color';
import { Color, ResizeConstraints } from '../types';
import { makeResizeConstraint } from './resizeConstraint';
const lut: string[] = [];
for (let i = 0; i < 256; i += 1) {
lut[i] = (i < 16 ? '0' : '') + i.toString(16);
}
// Hack (http://stackoverflow.com/a/21963136)
function e7(seed?: string) {
const random = seed ? seedrandom(`${seed}0`) : Math.random;
const d0 = (random() * 0xffffffff) | 0;
const d1 = (random() * 0xffffffff) | 0;
const d2 = (random() * 0xffffffff) | 0;
const d3 = (random() * 0xffffffff) | 0;
return `${
lut[d0 & 0xff] + lut[(d0 >> 8) & 0xff] + lut[(d0 >> 16) & 0xff] + lut[(d0 >> 24) & 0xff]
}-${lut[d1 & 0xff]}${lut[(d1 >> 8) & 0xff]}-${lut[((d1 >> 16) & 0x0f) | 0x40]}${
lut[(d1 >> 24) & 0xff]
}-${lut[(d2 & 0x3f) | 0x80]}${lut[(d2 >> 8) & 0xff]}-${lut[(d2 >> 16) & 0xff]}${
lut[(d2 >> 24) & 0xff]
}${lut[d3 & 0xff]}${lut[(d3 >> 8) & 0xff]}${lut[(d3 >> 16) & 0xff]}${lut[(d3 >> 24) & 0xff]}`;
}
// Keep track on previous numbers that are generated
let previousNumber = 1;
// Will always produce a unique Number (Int) based on of the current date
function generateIdNumber() {
let date = Date.now();
if (date <= previousNumber) {
previousNumber += 1;
date = previousNumber;
} else {
previousNumber = date;
}
return date;
}
// Keep track of previous seeds
const previousSeeds: { [seed: string]: number } = {};
export function generateID(seed?: string, hardcoded?: boolean): string {
let _seed = seed;
if (seed) {
if (!previousSeeds[seed]) {
previousSeeds[seed] = 0;
}
previousSeeds[seed] += 1;
if (!hardcoded) {
_seed = `${seed}${previousSeeds[seed]}`;
}
}
return e7(_seed);
}
const safeToLower = (input: Color): Color => {
if (typeof input === 'string') {
return input.toLowerCase();
}
return input;
};
// Takes colors as CSS hex, name, rgb, rgba, hsl or hsla
export const makeColorFromCSS = (input: Color, alpha: number = 1): FileFormat.Color => {
const nullableColor = normalizeColor(safeToLower(input));
const colorInt: number = nullableColor == null ? 0x00000000 : nullableColor;
const { r, g, b, a } = rgba(colorInt);
return {
_class: 'color',
red: r / 255,
green: g / 255,
blue: b / 255,
alpha: a * alpha,
};
};
export const emptyGradient: FileFormat.Gradient = {
_class: 'gradient',
elipseLength: 0,
from: '{0.5, 0}',
gradientType: 0,
to: '{0.5, 1}',
stops: [
{
_class: 'gradientStop',
position: 0,
color: {
_class: 'color',
alpha: 1,
blue: 1,
green: 1,
red: 1,
},
},
{
_class: 'gradientStop',
position: 1,
color: {
_class: 'color',
alpha: 1,
blue: 0,
green: 0,
red: 0,
},
},
],
};
// Solid color fill
export const makeColorFill = (cssColor: Color): FileFormat.Fill => ({
_class: 'fill',
isEnabled: true,
color: makeColorFromCSS(cssColor),
fillType: FileFormat.FillType.Color,
noiseIndex: 0,
noiseIntensity: 0,
patternFillType: FileFormat.PatternFillType.Fill,
patternTileScale: 1,
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
gradient: emptyGradient,
});
export const makeImageFill = (
image: FileFormat.ImageDataRef,
patternFillType: FileFormat.PatternFillType = FileFormat.PatternFillType.Fill,
): FileFormat.Fill => ({
_class: 'fill',
isEnabled: true,
fillType: FileFormat.FillType.Pattern,
color: makeColorFromCSS('white'),
image,
noiseIndex: 0,
noiseIntensity: 0,
patternFillType,
patternTileScale: 1,
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
gradient: emptyGradient,
});
// Used in frames, etc
export const makeRect = (x: number, y: number, width: number, height: number): FileFormat.Rect => ({
_class: 'rect',
constrainProportions: false,
x,
y,
width,
height,
});
export const makeJSONDataReference = (image: {
data: string;
sha1: string;
}): FileFormat.ImageDataRef => ({
_class: 'MSJSONOriginalDataReference',
_ref: `images/${generateID()}`,
_ref_class: 'MSImageData',
data: {
_data: image.data,
// TODO(gold): can I just declare this as a var instead of using Cocoa?
},
sha1: {
_data: image.sha1,
},
});
export const makeOverride = (
path: string,
type: 'symbolID' | 'stringValue' | 'layerStyle' | 'textStyle' | 'flowDestination' | 'image',
value: string | FileFormat.ImageDataRef,
): FileFormat.OverrideValue => ({
_class: 'overrideValue',
do_objectID: generateID(),
overrideName: `${path}_${type}`,
// @ts-ignore https://github.com/sketch-hq/sketch-file-format-ts/issues/9
value,
});
export const makeSymbolInstance = (
frame: FileFormat.Rect,
symbolID: string,
name: string,
resizingConstraint?: ResizeConstraints | null,
): FileFormat.SymbolInstance => ({
_class: 'symbolInstance',
horizontalSpacing: 0,
verticalSpacing: 0,
nameIsFixed: true,
isVisible: true,
do_objectID: generateID(`symbolInstance:${name}:${symbolID}`),
resizingConstraint: makeResizeConstraint(resizingConstraint),
name,
symbolID,
frame,
booleanOperation: FileFormat.BooleanOperation.NA,
isLocked: false,
isFixedToViewport: false,
isFlippedHorizontal: false,
isFlippedVertical: false,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
overrideValues: [],
scale: 1,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
});
export const makeSymbolMaster = (
frame: FileFormat.Rect,
symbolID: string,
name: string,
): FileFormat.SymbolMaster => ({
_class: 'symbolMaster',
do_objectID: generateID(`symbolMaster:${name}`, !!name),
nameIsFixed: true,
isVisible: true,
backgroundColor: makeColorFromCSS('white'),
hasBackgroundColor: false,
name,
changeIdentifier: generateIdNumber(),
symbolID,
frame,
booleanOperation: FileFormat.BooleanOperation.NA,
isLocked: false,
isFixedToViewport: false,
isFlippedHorizontal: false,
isFlippedVertical: false,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
resizingConstraint: makeResizeConstraint(),
hasClickThrough: false,
layers: [],
horizontalRulerData: {
_class: 'rulerData',
base: 0,
guides: [],
},
verticalRulerData: {
_class: 'rulerData',
base: 0,
guides: [],
},
includeInCloudUpload: true,
includeBackgroundColorInExport: false,
includeBackgroundColorInInstance: false,
isFlowHome: false,
resizesContent: false,
allowsOverrides: true,
overrideProperties: [],
});
================================================
FILE: src/jsonUtils/resizeConstraint.ts
================================================
import { ResizeConstraints } from '../types';
/*
RESIZE CONSTRAINT RULES
Order of properties as map keys:
1. top
2. right
3. bottom
4: left
5. fixedHeight
6. fixedWidth
*/
const RESIZE_CONSTRAINTS: { [key: string]: number } = {
top_left_fixedHeight_fixedWidth: 9,
top_right_left_fixedHeight: 10,
top_left_fixedHeight: 11,
top_right_fixedHeight_fixedWidth: 12,
top_fixedHeight_fixedWidth: 13,
top_right_fixedHeight: 14,
top_fixedHeight: 15,
top_bottom_left_fixedWidth: 17,
top_right_bottom_left: 18,
top_bottom_left: 19,
top_right_bottom_fixedWidth: 20,
top_bottom_fixedWidth: 21,
top_right_bottom: 22,
top_bottom: 23,
top_left_fixedWidth: 25,
top_right_left: 26,
top_left: 27,
top_right_fixedWidth: 28,
top_fixedWidth: 29,
top_right: 30,
top: 31,
bottom_left_fixedHeight_fixedWidth: 33,
right_bottom_left_fixedHeight: 34,
bottom_left_fixedHeight: 35,
right_bottom_fixedHeight_fixedWidth: 36,
bottom_fixedHeight_fixedWidth: 37,
right_bottom_fixedHeight: 38,
bottom_fixedHeight: 39,
left_fixedHeight_fixedWidth: 41,
right_left_fixedHeight: 42,
left_fixedHeight: 43,
right_fixedHeight_fixedWidth: 44,
fixedHeight_fixedWidth: 45,
right_fixedHeight: 46,
fixedHeight: 47,
bottom_left_fixedWidth: 49,
right_bottom_left: 50,
bottom_left: 51,
right_bottom_fixedWidth: 52,
bottom_fixedWidth: 53,
right_bottom: 54,
bottom: 55,
left_fixedWidth: 57,
right_left: 58,
left: 59,
right_fixedWidth: 60,
fixedWidth: 61,
right: 62,
none: 63,
};
export function makeResizeConstraint(resizingConstraint?: ResizeConstraints | null): number {
if (resizingConstraint) {
const constraints = [];
const { top, right, bottom, left, fixedHeight, fixedWidth } = resizingConstraint;
if (top) {
constraints.push('top');
}
if (right) {
constraints.push('right');
}
if (bottom) {
constraints.push('bottom');
}
if (left) {
constraints.push('left');
}
if (fixedHeight) {
constraints.push('fixedHeight');
}
if (fixedWidth) {
constraints.push('fixedWidth');
}
if (constraints.length > 0) {
const constraint = RESIZE_CONSTRAINTS[constraints.join('_')];
if (!constraint) {
throw new Error(
`\n${JSON.stringify(
resizingConstraint,
null,
2,
)}\nconstraint is not a valid combination.`,
);
}
return constraint;
}
}
return RESIZE_CONSTRAINTS.none; // No constraints
}
================================================
FILE: src/jsonUtils/shapeLayers.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeResizeConstraint } from './resizeConstraint';
import { generateID, makeRect, makeColorFromCSS, emptyGradient } from './models';
import { makeStyle } from './style';
import { Color, ResizeConstraints, ViewStyle } from '../types';
type Radii = (number | null)[];
export const makeHorizontalPath = (): Pick => ({
isClosed: false,
points: [
{
_class: 'curvePoint',
cornerRadius: 0,
curveFrom: '{0, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{0, 0.5}',
},
{
_class: 'curvePoint',
cornerRadius: 0,
curveFrom: '{0, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{1, 0.5}',
},
],
});
export const makeVerticalPath = (): Pick => ({
isClosed: false,
points: [
{
_class: 'curvePoint',
cornerRadius: 0,
curveFrom: '{0, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{0.5, 0}',
},
{
_class: 'curvePoint',
cornerRadius: 0,
curveFrom: '{0, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{0.5, 1}',
},
],
});
export const makeRectPath = (
radii: Radii = [0, 0, 0, 0],
): Pick => {
const [r0, r1, r2, r3] = radii;
return {
isClosed: true,
points: [
{
_class: 'curvePoint',
cornerRadius: r0 || 0,
curveFrom: '{0, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{0, 0}',
},
{
_class: 'curvePoint',
cornerRadius: r1 || 0,
curveFrom: '{1, 0}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{1, 0}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{1, 0}',
},
{
_class: 'curvePoint',
cornerRadius: r2 || 0,
curveFrom: '{1, 1}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{1, 1}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{1, 1}',
},
{
_class: 'curvePoint',
cornerRadius: r3 || 0,
curveFrom: '{0, 1}',
curveMode: FileFormat.CurveMode.Straight,
curveTo: '{0, 1}',
hasCurveFrom: false,
hasCurveTo: false,
point: '{0, 1}',
},
],
};
};
export const makeShapePath = (
frame: FileFormat.Rect,
path: Pick,
resizingConstraint?: ResizeConstraints,
): FileFormat.ShapePath => ({
_class: 'shapePath',
frame,
do_objectID: generateID(),
isFlippedHorizontal: false,
isFlippedVertical: false,
isLocked: false,
isVisible: true,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
name: 'Path',
nameIsFixed: false,
resizingConstraint: makeResizeConstraint(resizingConstraint),
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
booleanOperation: FileFormat.BooleanOperation.NA,
edited: false,
...path,
isFixedToViewport: false,
pointRadiusBehaviour: FileFormat.PointsRadiusBehaviour.Rounded,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
});
export const makeRectShapeLayer = (
x: number,
y: number,
width: number,
height: number,
radii: Radii = [0, 0, 0, 0],
resizingConstraint?: ResizeConstraints | null,
): FileFormat.Rectangle => {
const fixedRadius = radii[0] || 0;
const path = makeRectPath(radii);
return {
...path,
_class: 'rectangle',
do_objectID: generateID(),
frame: makeRect(x, y, width, height),
isFlippedHorizontal: false,
isFlippedVertical: false,
isLocked: false,
isVisible: true,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
name: 'Path',
nameIsFixed: false,
resizingConstraint: makeResizeConstraint(resizingConstraint),
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
booleanOperation: FileFormat.BooleanOperation.NA,
edited: false,
fixedRadius,
hasConvertedToNewRoundCorners: true,
isFixedToViewport: false,
pointRadiusBehaviour: FileFormat.PointsRadiusBehaviour.Rounded,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
};
};
export const makeShapeGroup = (
frame: FileFormat.Rect,
layers: (
| FileFormat.ShapePath
| FileFormat.Rectangle
| FileFormat.SymbolMaster
| FileFormat.Group
| FileFormat.Polygon
| FileFormat.Star
| FileFormat.Triangle
| FileFormat.ShapeGroup
| FileFormat.Text
| FileFormat.SymbolInstance
| FileFormat.Slice
| FileFormat.Hotspot
| FileFormat.Bitmap
)[] = [],
style?: ViewStyle,
shadows?: (ViewStyle | undefined | null)[] | null,
fills?: FileFormat.Fill[],
resizingConstraint?: ResizeConstraints,
): FileFormat.ShapeGroup => ({
_class: 'shapeGroup',
do_objectID: generateID(),
frame,
isLocked: false,
isVisible: true,
name: 'ShapeGroup',
nameIsFixed: false,
resizingConstraint: makeResizeConstraint(resizingConstraint),
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
style: makeStyle(style, fills, shadows),
hasClickThrough: false,
layers,
clippingMaskMode: 0,
hasClippingMask: false,
windingRule: FileFormat.WindingRule.EvenOdd,
isFixedToViewport: false,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
isFlippedHorizontal: false,
isFlippedVertical: false,
booleanOperation: FileFormat.BooleanOperation.NA,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
});
export const makeVerticalBorder = (
x: number,
y: number,
length: number,
thickness: number,
color: Color,
): FileFormat.ShapeGroup => {
const frame = makeRect(x, y, thickness, length);
const shapeFrame = makeRect(0, 0, thickness, length);
const shapePath = makeShapePath(shapeFrame, makeVerticalPath());
const content = makeShapeGroup(frame, [shapePath]);
if (!content.style) {
return content;
}
content.style.borders = [
{
_class: 'border',
isEnabled: true,
color: makeColorFromCSS(color),
fillType: FileFormat.FillType.Color,
position: FileFormat.BorderPosition.Center,
thickness,
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
gradient: emptyGradient,
},
];
return content;
};
export const makeHorizontalBorder = (
x: number,
y: number,
length: number,
thickness: number,
color: Color,
): FileFormat.ShapeGroup => {
const frame = makeRect(x, y, length, thickness);
const shapeFrame = makeRect(0, 0, length, thickness);
const shapePath = makeShapePath(shapeFrame, makeHorizontalPath());
const content = makeShapeGroup(frame, [shapePath]);
if (!content.style) {
return content;
}
content.style.borders = [
{
_class: 'border',
isEnabled: true,
color: makeColorFromCSS(color),
fillType: FileFormat.FillType.Color,
position: FileFormat.BorderPosition.Center,
thickness,
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
gradient: emptyGradient,
},
];
return content;
};
================================================
FILE: src/jsonUtils/sketchJson/fromSJSON.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { SketchLayer } from '../../types';
/*
Versions based on discussion info: http://sketchplugins.com/d/316-sketch-version
*/
// Internal Sketch Version (ex: 95 => v47 and below)
const SKETCH_HIGHEST_COMPATIBLE_VERSION = '95';
/**
* Takes a Sketch JSON tree and turns it into a native object. May throw on invalid data
*/
export function fromSJSON(
jsonTree: FileFormat.AnyLayer | FileFormat.AnyObject,
version = SKETCH_HIGHEST_COMPATIBLE_VERSION,
): SketchLayer {
const err = MOPointer.alloc().init();
const unarchivedObjectFromDictionary =
MSJSONDictionaryUnarchiver.unarchivedObjectFromDictionary_asVersion_corruptionDetected_error ||
MSJSONDictionaryUnarchiver.unarchiveObjectFromDictionary_asVersion_corruptionDetected_error;
const decoded = unarchivedObjectFromDictionary(jsonTree, version, null, err);
if (err.value() !== null) {
console.error(err.value());
throw new Error(err.value());
}
const mutableClass = decoded.class().mutableClass();
return mutableClass.alloc().initWithImmutableModelObject(decoded);
}
================================================
FILE: src/jsonUtils/sketchJson/toSJSON.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { SketchLayer } from '../../types';
export function toSJSON(
sketchObject: SketchLayer,
): FileFormat.AnyObject | FileFormat.AnyLayer | null {
if (!sketchObject) {
return null;
}
const imm = sketchObject.immutableModelObject();
const err = MOPointer.alloc().init();
const data = MSJSONDataArchiver.archiveStringWithRootObject_error(imm, err);
if (err.value() !== null) {
console.error(err.value());
throw new Error(err.value());
}
return data ? JSON.parse(data) : data;
}
================================================
FILE: src/jsonUtils/style.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeColorFromCSS, makeColorFill } from './models';
import { ViewStyle, TextStyle, BorderStyle } from '../types';
import { hasAnyDefined } from '../utils/hasAnyDefined';
import { DEFAULT_BORDER_STYLE } from './borders';
const DEFAULT_SHADOW_COLOR = '#000';
const SHADOW_STYLES = [
'shadowColor',
'shadowOffset',
'shadowOpacity',
'shadowRadius',
'shadowSpread',
'textShadowColor',
'textShadowOffset',
'textShadowOpacity',
'textShadowRadius',
'textShadowSpread',
];
const makeDashPattern = (style: BorderStyle, width: number): number[] => {
switch (style) {
case 'dashed':
return [width * 3, width * 3];
case 'dotted':
return [width, width];
case 'solid':
return [];
default:
return [];
}
};
export const makeBorderOptions = (
style: BorderStyle,
width: number,
lineCapStyle: FileFormat.LineCapStyle = FileFormat.LineCapStyle.Butt,
lineJoinStyle: FileFormat.LineJoinStyle = FileFormat.LineJoinStyle.Miter,
): FileFormat.BorderOptions => ({
_class: 'borderOptions',
isEnabled: false,
dashPattern: makeDashPattern(style, width),
lineCapStyle,
lineJoinStyle,
});
export const makeShadow = (
style: ViewStyle | TextStyle,
): FileFormat.Shadow | FileFormat.InnerShadow => {
const opacity =
style.shadowOpacity !== undefined && style.shadowOpacity !== null
? style.shadowOpacity
: 'textShadowOpacity' in style &&
style.textShadowOpacity !== undefined &&
style.textShadowOpacity !== null
? style.textShadowOpacity
: 1;
const color =
style.shadowColor ||
('textShadowColor' in style && style.textShadowColor) ||
DEFAULT_SHADOW_COLOR;
const radius =
style.shadowRadius !== undefined && style.shadowRadius !== null
? style.shadowRadius
: 'textShadowRadius' in style &&
style.textShadowRadius !== undefined &&
style.textShadowRadius !== null
? style.textShadowRadius
: 1;
const spread =
style.shadowSpread !== undefined && style.shadowSpread !== null
? style.shadowSpread
: 'textShadowSpread' in style &&
style.textShadowSpread !== undefined &&
style.textShadowSpread !== null
? style.textShadowSpread
: 1;
let { width: offsetX, height: offsetY } =
style.shadowOffset || ('textShadowOffset' in style && style.textShadowOffset) || {};
if (!offsetX) {
offsetX = 0;
}
if (!offsetY) {
offsetY = 0;
}
const commonProps = {
isEnabled: true,
blurRadius: radius,
color: makeColorFromCSS(color, opacity),
contextSettings: {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: 1,
},
offsetX,
offsetY,
spread,
} as const;
if (style.shadowInner) {
return {
_class: 'innerShadow',
...commonProps,
};
}
return {
_class: 'shadow',
...commonProps,
};
};
export const makeStyle = (
style?: ViewStyle | TextStyle,
fills?: FileFormat.Fill[],
shadowsProp?: (ViewStyle | undefined | null)[] | null,
): FileFormat.Style => {
const json: FileFormat.Style = {
_class: 'style',
fills: [],
miterLimit: 10,
innerShadows: [],
shadows: [],
borderOptions: makeBorderOptions(DEFAULT_BORDER_STYLE, 0, 0, 0),
startMarkerType: FileFormat.MarkerType.OpenArrow,
endMarkerType: FileFormat.MarkerType.OpenArrow,
windingRule: FileFormat.WindingRule.EvenOdd,
colorControls: {
_class: 'colorControls',
isEnabled: false,
brightness: 1,
contrast: 1,
hue: 1,
saturation: 1,
},
};
if (fills && fills.length) {
json.fills = (json.fills || []).concat(fills);
}
if (!style) {
return json;
}
if (style.opacity) {
json.contextSettings = {
_class: 'graphicsContextSettings',
blendMode: FileFormat.BlendMode.Normal,
opacity: style.opacity,
};
}
if (style.backgroundColor) {
const fill = makeColorFill(style.backgroundColor);
(json.fills || []).unshift(fill);
}
if (hasAnyDefined(style, SHADOW_STYLES)) {
const shadow = [makeShadow(style)];
if (style.shadowInner) {
json.innerShadows = shadow as FileFormat.InnerShadow[];
} else {
json.shadows = shadow as FileFormat.Shadow[];
}
}
if (shadowsProp) {
shadowsProp.forEach((shadowStyle) => {
if (!shadowStyle) {
return;
}
const shadow = makeShadow(shadowStyle);
if (shadowStyle.shadowInner) {
(json.innerShadows || []).push(shadow as FileFormat.InnerShadow);
} else {
(json.shadows || []).push(shadow as FileFormat.Shadow);
}
});
}
return json;
};
export function parseStyle(json: FileFormat.Style): ViewStyle {
const style: ViewStyle = {};
if (json.contextSettings && json.contextSettings.opacity !== 1) {
style.opacity = json.contextSettings.opacity;
}
if (
json.fills &&
json.fills.length > 0 &&
json.fills[0].fillType === FileFormat.FillType.Color &&
json.fills[0].isEnabled
) {
const color = json.fills[0].color;
style.backgroundColor = `#${Math.round(color.red * 255).toString(16)}${Math.round(
color.green * 255,
).toString(16)}${Math.round(color.blue * 255).toString(16)}`;
if (color.alpha !== 1) {
style.backgroundColor += `${Math.round(color.alpha * 255).toString(16)}`;
}
}
if (
(json.shadows && json.shadows.length > 0 && json.shadows[0].isEnabled) ||
(json.innerShadows && json.innerShadows.length > 0 && json.innerShadows[0].isEnabled)
) {
const isNormalShadow = json.shadows && json.shadows.length > 0 && json.shadows[0].isEnabled;
const shadow = isNormalShadow ? (json.shadows || [])[0] : (json.innerShadows || [])[0];
style.shadowRadius = shadow.blurRadius;
style.shadowSpread = shadow.spread;
style.shadowOffset = {
width: shadow.offsetX,
height: shadow.offsetY,
};
const color = shadow.color;
style.shadowColor = `#${Math.round(color.red * 255).toString(16)}${Math.round(
color.green * 255,
).toString(16)}${Math.round(color.blue * 255).toString(16)}`;
if (color.alpha !== 1) {
style.shadowOpacity = color.alpha;
}
if (!isNormalShadow) {
style.shadowInner = true;
}
}
return style;
}
================================================
FILE: src/jsonUtils/textLayers.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { makeResizeConstraint } from './resizeConstraint';
import { TextNode, ResizeConstraints, TextStyle, ViewStyle, PlatformBridge } from '../types';
import { generateID, makeColorFromCSS } from './models';
import { makeStyle, parseStyle } from './style';
export const TEXT_DECORATION_UNDERLINE: { [key: string]: number } = {
none: FileFormat.UnderlineStyle.None,
underline: FileFormat.UnderlineStyle.Underlined,
double: 9,
'line-through': 0,
};
export const TEXT_ALIGN: { [key: string]: number } = {
auto: FileFormat.TextHorizontalAlignment.Left,
left: FileFormat.TextHorizontalAlignment.Left,
right: FileFormat.TextHorizontalAlignment.Right,
center: FileFormat.TextHorizontalAlignment.Centered,
justify: FileFormat.TextHorizontalAlignment.Justified,
};
const TEXT_ALIGN_REVERSE: { [key: number]: 'center' | 'auto' | 'left' | 'right' | 'justify' } = {
[FileFormat.TextHorizontalAlignment.Natural]: 'left',
[FileFormat.TextHorizontalAlignment.Right]: 'right',
[FileFormat.TextHorizontalAlignment.Centered]: 'center',
[FileFormat.TextHorizontalAlignment.Justified]: 'justify',
} as const;
export const TEXT_DECORATION_LINETHROUGH: { [key: string]: number } = {
none: 0,
underline: 0,
double: 0,
'line-through': 1,
};
export const TEXT_TRANSFORM: { [key: string]: number } = {
uppercase: FileFormat.TextTransform.Uppercase,
lowercase: FileFormat.TextTransform.Lowercase,
initial: FileFormat.TextTransform.None,
inherit: FileFormat.TextTransform.None,
none: FileFormat.TextTransform.None,
capitalize: FileFormat.TextTransform.None,
};
// this borrows heavily from react-native's RCTFont class
// thanks y'all
// https://github.com/facebook/react-native/blob/master/React/Views/RCTFont.mm
export const FONT_STYLES: { [key: string]: boolean } = {
normal: false,
italic: true,
oblique: true,
};
const makeFontDescriptor = (bridge: PlatformBridge) => (
style: TextStyle,
): FileFormat.FontDescriptor => ({
_class: 'fontDescriptor',
attributes: {
name: bridge.findFontName(style), // will default to the system font
size: style.fontSize || 14,
},
});
const makeTextStyleAttributes = (bridge: PlatformBridge) => (style: TextStyle) =>
({
underlineStyle: style.textDecoration ? TEXT_DECORATION_UNDERLINE[style.textDecoration] || 0 : 0,
strikethroughStyle: style.textDecoration
? TEXT_DECORATION_LINETHROUGH[style.textDecoration] || 0
: 0,
paragraphStyle: {
_class: 'paragraphStyle',
alignment: TEXT_ALIGN[style.textAlign || 'auto'],
paragraphSpacing: style.paragraphSpacing || 0,
...(typeof style.lineHeight !== 'undefined' && style.lineHeight !== null
? {
minimumLineHeight: style.lineHeight,
maximumLineHeight: style.lineHeight,
lineHeightMultiple: 1.0,
}
: {}),
},
...(typeof style.letterSpacing !== 'undefined' && style.letterSpacing !== null
? {
kerning: style.letterSpacing,
}
: {}),
...(typeof style.textTransform !== 'undefined' && style.textTransform !== null
? {
MSAttributedStringTextTransformAttribute: TEXT_TRANSFORM[style.textTransform] * 1,
}
: {}),
MSAttributedStringFontAttribute: makeFontDescriptor(bridge)(style),
textStyleVerticalAlignmentKey: 0,
MSAttributedStringColorAttribute: makeColorFromCSS(style.color || 'black'),
} as const);
const makeAttribute = (bridge: PlatformBridge) => (
node: TextNode,
location: number,
): FileFormat.StringAttribute => ({
_class: 'stringAttribute',
location,
length: node.content.length,
attributes: makeTextStyleAttributes(bridge)(node.textStyles),
});
const makeAttributedString = (bridge: PlatformBridge) => (
textNodes: TextNode[],
): FileFormat.AttributedString => {
const json: FileFormat.AttributedString = {
_class: 'attributedString',
string: '',
attributes: [],
};
let location = 0;
textNodes.forEach((node) => {
json.attributes.push(makeAttribute(bridge)(node, location));
json.string += node.content;
location += node.content.length;
});
return json;
};
export const makeTextStyle = (bridge: PlatformBridge) => (
style: TextStyle,
shadows?: (ViewStyle | undefined | null)[] | null,
): FileFormat.Style => {
const json = makeStyle(style, undefined, shadows);
json.textStyle = {
_class: 'textStyle',
encodedAttributes: makeTextStyleAttributes(bridge)(style),
verticalAlignment: FileFormat.TextVerticalAlignment.Top,
};
return json;
};
export const parseTextStyle = (json: FileFormat.Style): TextStyle => {
const style: TextStyle = parseStyle(json);
if (json.textStyle) {
const attr = json.textStyle.encodedAttributes;
if (attr.underlineStyle) {
style.textDecoration = attr.underlineStyle === 9 ? 'double' : 'underline';
}
if (attr.strikethroughStyle) {
style.textDecoration = 'line-through';
}
if (
attr.paragraphStyle &&
attr.paragraphStyle.alignment &&
TEXT_ALIGN_REVERSE[attr.paragraphStyle.alignment]
) {
style.textAlign = TEXT_ALIGN_REVERSE[attr.paragraphStyle.alignment];
}
if (attr.paragraphStyle && typeof attr.paragraphStyle.minimumLineHeight !== 'undefined') {
style.lineHeight = attr.paragraphStyle.minimumLineHeight;
}
if (typeof attr.kerning !== 'undefined') {
style.letterSpacing = attr.kerning;
}
const color = json.textStyle.encodedAttributes.MSAttributedStringColorAttribute;
if (color) {
style.color = `#${Math.round(color.red * 255).toString(16)}${Math.round(
color.green * 255,
).toString(16)}${Math.round(color.blue * 255).toString(16)}`;
if (color.alpha !== 1) {
style.color += `${Math.round(color.alpha * 255).toString(16)}`;
}
}
if (
json.textStyle.encodedAttributes.MSAttributedStringTextTransformAttribute !==
FileFormat.TextTransform.None
) {
style.textTransform =
json.textStyle.encodedAttributes.MSAttributedStringTextTransformAttribute ===
FileFormat.TextTransform.Lowercase
? 'lowercase'
: 'uppercase';
}
const font = json.textStyle.encodedAttributes.MSAttributedStringFontAttribute;
style.fontSize = font.attributes.size;
// we are cheating here, setting the name of the font instead of parsing
// the family, weight and traits. react-sketchapp will handle it nevertheless
style.fontFamily = font.attributes.name;
}
return style;
};
export const makeTextLayer = (bridge: PlatformBridge) => (
frame: FileFormat.Rect,
name: string,
textNodes: TextNode[],
_style: ViewStyle,
resizingConstraint?: ResizeConstraints | null,
shadows?: (ViewStyle | undefined | null)[] | null,
): FileFormat.Text => ({
_class: 'text',
do_objectID: generateID(`text:${name}-${textNodes.map((node) => node.content).join('')}`),
frame,
isFlippedHorizontal: false,
isFlippedVertical: false,
isLocked: false,
isVisible: true,
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
name,
nameIsFixed: false,
resizingConstraint: makeResizeConstraint(resizingConstraint),
resizingType: FileFormat.ResizeType.Stretch,
rotation: 0,
shouldBreakMaskChain: false,
attributedString: makeAttributedString(bridge)(textNodes),
style: makeTextStyle(bridge)((textNodes[0] || { textStyles: {} }).textStyles, shadows),
automaticallyDrawOnUnderlyingPath: false,
dontSynchroniseWithSymbol: false,
// NOTE(akp): I haven't fully figured out the meaning of glyphBounds
glyphBounds: '',
// glyphBounds: '{{0, 0}, {116, 17}}',
lineSpacingBehaviour: FileFormat.LineSpacingBehaviour.ConsistentBaseline,
textBehaviour: FileFormat.TextBehaviour.Fixed,
booleanOperation: FileFormat.BooleanOperation.NA,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
isFixedToViewport: false,
});
================================================
FILE: src/platformBridges/macos.ts
================================================
import { PlatformBridge } from '../types';
import { createStringMeasurer, findFontName, makeImageDataFromUrl } from 'node-sketch-bridge';
const NodeMacOSBridge: PlatformBridge = {
createStringMeasurer,
findFontName,
makeImageDataFromUrl,
};
export default NodeMacOSBridge;
================================================
FILE: src/platformBridges/sketch/createStringMeasurer.ts
================================================
import { Size, TextNode, TextStyle } from '../../types';
import {
TEXT_DECORATION_UNDERLINE,
TEXT_DECORATION_LINETHROUGH,
TEXT_ALIGN,
TEXT_TRANSFORM,
} from '../../jsonUtils/textLayers';
import { makeColorFromCSS } from '../../jsonUtils/models';
import { findFont } from './findFontName';
// TODO(lmr): do something more sensible here
const FLOAT_MAX = 999999;
function makeParagraphStyle(textStyle: TextStyle) {
const pStyle = NSMutableParagraphStyle.alloc().init();
if (textStyle.lineHeight !== undefined) {
pStyle.minimumLineHeight = textStyle.lineHeight;
pStyle.lineHeightMultiple = 1.0;
pStyle.maximumLineHeight = textStyle.lineHeight;
}
if (textStyle.textAlign) {
pStyle.alignment = TEXT_ALIGN[textStyle.textAlign];
}
// TODO: check against only positive spacing values?
if (textStyle.paragraphSpacing !== undefined) {
pStyle.paragraphSpacing = textStyle.paragraphSpacing;
}
return pStyle;
}
// This shouldn't need to call into Sketch, but it does currently, which is bad for perf :(
function createStringAttributes(textStyles: TextStyle): Object {
const font = findFont(textStyles);
const { textDecoration } = textStyles;
const underline = textDecoration && TEXT_DECORATION_UNDERLINE[textDecoration];
const strikethrough = textDecoration && TEXT_DECORATION_LINETHROUGH[textDecoration];
const attribs: any = {
MSAttributedStringFontAttribute: font.fontDescriptor(),
NSFont: font,
NSParagraphStyle: makeParagraphStyle(textStyles),
NSUnderline: underline || 0,
NSStrikethrough: strikethrough || 0,
};
const color = makeColorFromCSS(textStyles.color || 'black');
attribs.MSAttributedStringColorAttribute = color;
if (textStyles.letterSpacing !== undefined && textStyles.letterSpacing !== null) {
attribs.NSKern = textStyles.letterSpacing;
}
if (textStyles.textTransform !== undefined && textStyles.textTransform !== null) {
attribs.MSAttributedStringTextTransformAttribute = TEXT_TRANSFORM[textStyles.textTransform] * 1;
}
return attribs;
}
type NSAttributedString = any;
function createAttributedString(textNode: TextNode): NSAttributedString {
const { content, textStyles } = textNode;
const attribs = createStringAttributes(textStyles);
return NSAttributedString.attributedStringWithString_attributes_(content, attribs);
}
export function createStringMeasurer(textNodes: TextNode[], width: number): Size {
const fullStr = NSMutableAttributedString.alloc().init();
textNodes.forEach((textNode) => {
const newString = createAttributedString(textNode);
fullStr.appendAttributedString(newString);
});
const {
height: measureHeight,
width: measureWidth,
} = fullStr.boundingRectWithSize_options_context(
CGSizeMake(width, FLOAT_MAX),
NSStringDrawingUsesLineFragmentOrigin,
null,
).size;
return { width: measureWidth, height: measureHeight };
}
================================================
FILE: src/platformBridges/sketch/findFontName.ts
================================================
import { hashStyle } from '../../utils/hashStyle';
import { TextStyle } from '../../types';
import { FONT_STYLES } from '../../jsonUtils/textLayers';
// this borrows heavily from react-native's RCTFont class
// thanks y'all
// https://github.com/facebook/react-native/blob/master/React/Views/RCTFont.mm
const FONT_WEIGHTS: { [key: string]: number } = {
ultralight: -0.8,
'100': -0.8,
thin: -0.6,
'200': -0.6,
light: -0.4,
'300': -0.4,
normal: 0,
regular: 0,
'400': 0,
semibold: 0.23,
demibold: 0.23,
'500': 0.23,
'600': 0.3,
bold: 0.4,
'700': 0.4,
extrabold: 0.56,
ultrabold: 0.56,
heavy: 0.56,
'800': 0.56,
black: 0.62,
'900': 0.62,
};
type NSFont = any;
const isItalicFont = (font: NSFont): boolean => {
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue();
return (symbolicTraits & NSFontItalicTrait) !== 0;
};
const isCondensedFont = (font: NSFont): boolean => {
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue();
return (symbolicTraits & NSFontCondensedTrait) !== 0;
};
const weightOfFont = (font: NSFont): number => {
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
const weight = traits[NSFontWeightTrait].doubleValue();
if (weight === 0.0) {
const weights = Object.keys(FONT_WEIGHTS);
const fontName = String(font.fontName()).toLowerCase();
const matchingWeight = weights.find((w) => fontName.endsWith(w));
if (matchingWeight) {
return FONT_WEIGHTS[matchingWeight];
}
}
return weight;
};
const fontNamesForFamilyName = (familyName: string): Array => {
const manager = NSFontManager.sharedFontManager();
const members = NSArray.arrayWithArray(manager.availableMembersOfFontFamily(familyName));
const results = [];
for (let i = 0; i < members.length; i += 1) {
results.push(members[i][0]);
}
return results;
};
const useCache = true;
const _cache: Map = new Map();
const getCached = (key: string): NSFont => {
if (!useCache) return undefined;
return _cache.get(key);
};
export const findFont = (style: TextStyle): NSFont => {
const cacheKey = hashStyle(style);
let font = getCached(cacheKey);
if (font) {
return font;
}
const defaultFontFamily = NSFont.systemFontOfSize(14).familyName();
const defaultFontWeight = NSFontWeightRegular;
const defaultFontSize = 14;
const fontSize = style.fontSize ? style.fontSize : defaultFontSize;
let fontWeight = FONT_WEIGHTS[String(style.fontWeight).toLowerCase()] || defaultFontWeight;
let familyName = defaultFontFamily;
let isItalic = false;
let isCondensed = false;
if (style.fontFamily) {
familyName = style.fontFamily;
}
if (style.fontStyle) {
isItalic = FONT_STYLES[style.fontStyle] || false;
}
let didFindFont = false;
// Handle system font as special case. This ensures that we preserve
// the specific metrics of the standard system font as closely as possible.
if (familyName === defaultFontFamily || familyName === 'System') {
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight);
if (font) {
didFindFont = true;
if (isItalic || isCondensed) {
let fontDescriptor = font.fontDescriptor();
let symbolicTraits = fontDescriptor.symbolicTraits();
if (isItalic) {
symbolicTraits |= NSFontItalicTrait;
}
if (isCondensed) {
symbolicTraits |= NSFontCondensedTrait;
}
fontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits);
font = NSFont.fontWithDescriptor_size(fontDescriptor, fontSize);
}
}
}
const fontNames = fontNamesForFamilyName(familyName);
// Gracefully handle being given a font name rather than font family, for
// example: "Helvetica Light Oblique" rather than just "Helvetica".
if (!didFindFont && fontNames.length === 0) {
font = NSFont.fontWithName_size(familyName, fontSize);
if (font) {
// It's actually a font name, not a font family name,
// but we'll do what was meant, not what was said.
familyName = font.familyName();
fontWeight = style.fontWeight ? fontWeight : weightOfFont(font);
isItalic = style.fontStyle ? isItalic : isItalicFont(font);
isCondensed = isCondensedFont(font);
} else {
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight);
}
didFindFont = true;
}
if (!didFindFont) {
// Get the closest font that matches the given weight for the fontFamily
let closestWeight = Infinity;
for (let i = 0; i < fontNames.length; i += 1) {
const match = NSFont.fontWithName_size(fontNames[i], fontSize);
if (isItalic === isItalicFont(match) && isCondensed === isCondensedFont(match)) {
const testWeight = weightOfFont(match);
if (Math.abs(testWeight - fontWeight) < Math.abs(closestWeight - fontWeight)) {
font = match;
closestWeight = testWeight;
}
}
}
}
// If we still don't have a match at least return the first font in the fontFamily
// This is to support built-in font Zapfino and other custom single font families like Impact
if (!font) {
if (fontNames.length > 0) {
font = NSFont.fontWithName_size(fontNames[0], fontSize);
}
}
// TODO(gold): support opentype features: small-caps & number types
if (font) {
_cache.set(cacheKey, font);
}
return font;
};
export function findFontName(style: TextStyle): string {
const font = findFont(style);
return String(font.fontDescriptor().postscriptName());
}
================================================
FILE: src/platformBridges/sketch/index.ts
================================================
import { PlatformBridge } from '../../types';
import { createStringMeasurer } from './createStringMeasurer';
import { findFontName } from './findFontName';
import { makeImageDataFromUrl } from './makeImageDataFromUrl';
const SketchBridge: PlatformBridge = {
createStringMeasurer,
findFontName,
makeImageDataFromUrl,
};
export default SketchBridge;
================================================
FILE: src/platformBridges/sketch/makeImageDataFromUrl.ts
================================================
export function makeImageDataFromUrl(url?: string) {
let fetchedData = url ? NSData.dataWithContentsOfURL(NSURL.URLWithString(url)) : undefined;
if (fetchedData) {
const firstByte = String(
NSString.alloc().initWithData_encoding(fetchedData, NSISOLatin1StringEncoding),
).charCodeAt(0);
// Check for first byte to see if we have an image.
// 0xFF = JPEG, 0x89 = PNG, 0x47 = GIF, 0x49 = TIFF, 0x4D = TIFF
if (
firstByte !== 0xff &&
firstByte !== 0x89 &&
firstByte !== 0x47 &&
firstByte !== 0x49 &&
firstByte !== 0x4d
) {
fetchedData = null;
}
}
let image: any;
if (!fetchedData) {
const errorUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM8w8DwHwAEOQHNmnaaOAAAAABJRU5ErkJggg==';
image = NSImage.alloc().initWithContentsOfURL(NSURL.URLWithString(errorUrl));
} else {
image = NSImage.alloc().initWithData(fetchedData);
}
let imageData: any;
if (MSImageData.alloc().initWithImage_convertColorSpace !== undefined) {
imageData = MSImageData.alloc().initWithImage_convertColorSpace(image, false);
} else {
imageData = MSImageData.alloc().initWithImage(image);
}
return String(
imageData.data().base64EncodedStringWithOptions(NSDataBase64EncodingEndLineWithCarriageReturn),
);
}
================================================
FILE: src/render.tsx
================================================
import * as React from 'react';
import { fromSJSON } from './jsonUtils/sketchJson/fromSJSON';
import { buildTree } from './buildTree';
import { flexToSketchJSON } from './flexToSketchJSON';
import { resetLayer, resetDocument } from './resets';
import { injectSymbols } from './symbol';
import {
SketchDocumentData,
SketchLayer,
SketchPage,
TreeNode,
WrappedSketchLayer,
PlatformBridge,
} from './types';
import { RedBox } from './components/RedBox';
import {
getDocumentDataFromContainer,
getDocumentDataFromContext,
getDocumentData,
} from './utils/getDocument';
import { isNativeDocument } from './utils/isNativeDocument';
import { isNativePage } from './utils/isNativePage';
import { isNativeSymbolsPage } from './utils/isNativeSymbolsPage';
export const renderLayers = (layers: Array, container: SketchLayer): SketchLayer => {
if (container.addLayers === undefined) {
throw new Error(
` React SketchApp cannot render into this layer. You may be trying to render into a layer that does not take children. Try rendering into a LayerGroup, Artboard, or Page.`,
);
}
container.addLayers(layers);
return container;
};
const getDefaultPage = (): SketchLayer => {
const doc = getDocumentDataFromContext(context);
const currentPage = doc.currentPage();
return isNativeSymbolsPage(currentPage) ? doc.addBlankPage() : currentPage;
};
const renderContents = (bridge: PlatformBridge) => (
tree: TreeNode | string,
container: SketchLayer,
): SketchLayer => {
const json = flexToSketchJSON(bridge)(tree);
const layer = fromSJSON(json, '119');
return renderLayers([layer], container);
};
const renderPage = (bridge: PlatformBridge) => (
tree: TreeNode,
page: SketchPage,
): Array => {
const children = tree.children || [];
// assume if name is set on this nested page, the intent is to overwrite
// the name of the page it is getting rendered into
if (tree.props.name) {
page.setName(tree.props.name);
}
return children.map((child) => renderContents(bridge)(child, page));
};
const renderDocument = (bridge: PlatformBridge) => (
tree: TreeNode,
documentData: SketchDocumentData,
): Array => {
const initialPage = documentData.currentPage();
const shouldRenderInitialPage = !isNativeSymbolsPage(initialPage);
const children = tree.children || [];
return children.map((child, i) => {
if (typeof child === 'string' || child.type !== 'sketch_page') {
throw new Error('Document children must be of type Page');
}
const page = i === 0 && shouldRenderInitialPage ? initialPage : documentData.addBlankPage();
return renderPage(bridge)(child, page);
});
};
const renderTree = (bridge: PlatformBridge) => (
tree: TreeNode,
_container?: SketchLayer,
): SketchLayer | Array => {
if (tree.type === 'sketch_document') {
if (_container && !isNativeDocument(_container)) {
throw new Error('Cannot render a Document into a child of Document');
}
const doc = getDocumentData(_container);
if (!doc) {
return;
}
resetDocument(doc);
return renderDocument(bridge)(tree, doc);
}
if (isNativeDocument(_container)) {
throw new Error('You need to render a Document into Document');
}
if (tree.type === 'sketch_page') {
if (_container && !isNativePage(_container)) {
throw new Error('You need to render a Page into Page');
}
}
const container = _container || getDefaultPage();
resetLayer(container);
return tree.type === 'sketch_page'
? renderPage(bridge)(tree, container)
: renderContents(bridge)(tree, container);
};
export const render = (bridge: PlatformBridge) => (
element: React.ReactElement,
container?: SketchLayer | WrappedSketchLayer,
): SketchLayer | Array => {
let nativeContainer: SketchLayer | undefined;
if (container && container.sketchObject) {
nativeContainer = container.sketchObject;
} else if (container) {
nativeContainer = container;
}
// The Symbols page holds a special meaning within Sketch / react-sketchapp
// and due to how `makeSymbol` works, we cannot render into it
if (isNativeSymbolsPage(nativeContainer)) {
throw Error('Cannot render into Symbols page');
}
try {
const tree = buildTree(bridge)(element);
injectSymbols(getDocumentDataFromContainer(nativeContainer));
return renderTree(bridge)(tree, nativeContainer);
} catch (err) {
console.error(err);
const tree = buildTree(bridge)( );
return renderContents(bridge)(tree, nativeContainer);
}
};
================================================
FILE: src/renderToJSON.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { PlatformBridge } from './types';
import { buildTree } from './buildTree';
import { flexToSketchJSON } from './flexToSketchJSON';
import * as React from 'react';
export const renderToJSON = (platformBridge: PlatformBridge) => (
element: React.ReactElement,
): FileFormat.AnyLayer => {
const tree = buildTree(platformBridge)(element);
return flexToSketchJSON(platformBridge)(tree);
};
================================================
FILE: src/renderers/ArtboardRenderer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { generateID, makeRect, makeColorFromCSS } from '../jsonUtils/models';
import { makeResizeConstraint } from '../jsonUtils/resizeConstraint';
import { SketchRenderer } from './SketchRenderer';
import { TreeNode } from '../types';
import { Props } from '../components/Artboard';
export class ArtboardRenderer extends SketchRenderer {
renderGroupLayer({ layout, style, props }: TreeNode): FileFormat.Artboard {
let color: FileFormat.Color | undefined;
if (style.backgroundColor !== undefined) {
color = makeColorFromCSS(style.backgroundColor);
}
return {
_class: 'artboard',
do_objectID: generateID(`artboard:${props.name}`, !!props.name),
frame: makeRect(layout.left, layout.top, layout.width, layout.height),
name: props.name || 'Artboard',
nameIsFixed: props.name !== undefined,
isVisible: true,
backgroundColor: color || makeColorFromCSS('white'),
hasBackgroundColor: color !== undefined,
isFlowHome: !!props.isHome,
...(props.viewport && {
presetDictionary: {
allowResizedMatching: 0,
offersLandscapeVariant: 1,
name: props.viewport.name,
width: props.viewport.width,
height: props.viewport.height,
},
}),
isFlippedHorizontal: false,
isFlippedVertical: false,
isFixedToViewport: false,
isLocked: false,
booleanOperation: FileFormat.BooleanOperation.NA,
exportOptions: {
_class: 'exportOptions',
exportFormats: [],
includedLayerIds: [],
layerOptions: 0,
shouldTrim: false,
},
layerListExpandedType: FileFormat.LayerListExpanded.Undecided,
resizingType: FileFormat.ResizeType.Stretch,
shouldBreakMaskChain: false,
hasClickThrough: false,
layers: [],
includeInCloudUpload: true,
includeBackgroundColorInExport: color !== undefined,
horizontalRulerData: {
_class: 'rulerData',
base: 0,
guides: [],
},
verticalRulerData: {
_class: 'rulerData',
base: 0,
guides: [],
},
resizingConstraint: makeResizeConstraint(),
resizesContent: false,
rotation: 0,
};
}
}
================================================
FILE: src/renderers/ImageRenderer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { SketchRenderer } from './SketchRenderer';
import { getImageDataFromURL } from '../utils/getImageDataFromURL';
// import processTransform from './processTransform';
import { makeRect, makeImageFill, makeJSONDataReference, generateID } from '../jsonUtils/models';
import { makeRectShapeLayer, makeShapeGroup } from '../jsonUtils/shapeLayers';
import { createBorders } from '../jsonUtils/borders';
import { TreeNode } from '../types';
import { Props } from '../components/Image';
function extractURLFromSource(source?: string | { uri?: string } | null): string | undefined {
if (typeof source === 'string') {
return source;
}
return (source || {}).uri;
}
export class ImageRenderer extends SketchRenderer {
renderBackingLayers({
layout,
style,
props,
}: TreeNode) {
let layers: FileFormat.ShapeGroup[] = [];
const {
borderTopLeftRadius = 0,
borderTopRightRadius = 0,
borderBottomRightRadius = 0,
borderBottomLeftRadius = 0,
} = style;
const url = extractURLFromSource(props.source);
const image = getImageDataFromURL(this.platformBridge)(url);
const fillImage = makeJSONDataReference(image);
const frame = makeRect(0, 0, layout.width, layout.height);
const radii = [
borderTopLeftRadius,
borderTopRightRadius,
borderBottomRightRadius,
borderBottomLeftRadius,
];
const shapeLayer = makeRectShapeLayer(0, 0, layout.width, layout.height, radii);
const fills = [makeImageFill(fillImage, props.resizeMode)];
const content = makeShapeGroup(frame, [shapeLayer], style, props.shadows, fills);
// try to keep a constant ID based on the URL
content.do_objectID = generateID(url);
const contents = createBorders(content, layout, style);
layers = layers.concat(contents);
return layers;
}
}
================================================
FILE: src/renderers/SketchRenderer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { layerGroup } from '../jsonUtils/layerGroup';
import { hotspotLayer } from '../jsonUtils/hotspotLayer';
import { TreeNode, PlatformBridge } from '../types';
import { processTransform } from '../utils/processTransform';
const DEFAULT_OPACITY = 1.0;
export class SketchRenderer {
protected readonly platformBridge: PlatformBridge;
constructor(bridge: PlatformBridge) {
this.platformBridge = bridge;
}
getDefaultGroupName(_props: any) {
return 'Group';
}
renderGroupLayer({
layout,
style,
props,
}: TreeNode):
| FileFormat.SymbolMaster
| FileFormat.Artboard
| FileFormat.Group
| FileFormat.ShapeGroup
| FileFormat.SymbolInstance {
// Default SketchRenderer just renders an empty group
const transform = processTransform(layout, style);
const opacity =
style.opacity !== undefined && style.opacity !== null ? style.opacity : DEFAULT_OPACITY;
return {
...layerGroup(
layout.left,
layout.top,
layout.width,
layout.height,
opacity,
props.resizingConstraint,
),
name: props.name || this.getDefaultGroupName(props),
...transform,
...(props.flow && hotspotLayer(props.flow)),
};
}
renderBackingLayers(
_node: TreeNode,
): (
| FileFormat.ShapePath
| FileFormat.Rectangle
| FileFormat.SymbolMaster
| FileFormat.Group
| FileFormat.Polygon
| FileFormat.Star
| FileFormat.Triangle
| FileFormat.ShapeGroup
| FileFormat.Text
| FileFormat.SymbolInstance
| FileFormat.Slice
| FileFormat.Hotspot
| FileFormat.Bitmap
)[] {
return [];
}
}
================================================
FILE: src/renderers/SvgRenderer.ts
================================================
import { ViewRenderer } from './ViewRenderer';
import { TreeNode } from '../types';
import { makeSvgLayer } from '../jsonUtils/makeSvgLayer';
import { Props } from '../components/Svg/Svg';
const snakeExceptions = [
'gradientUnits',
'gradientTransform',
'patternUnits',
'patternTransform',
'stdDeviation',
'numOctaves',
'specularExponent',
'specularConstant',
'surfaceScale',
'viewBox',
];
function toSnakeCase(string: string) {
if (string === 'href') {
return 'xlink:href';
}
if (snakeExceptions.indexOf(string) !== -1) {
return string;
}
return string.replace(/([A-Z])/g, ($1) => `-${$1.toLowerCase()}`);
}
function makeSvgString(el: string | TreeNode) {
if (typeof el === 'string') {
return el;
}
const { type, props, children } = el;
if (props && props.textNodes && props.textNodes.length) {
return props.textNodes.reduce((prev, textNode) => prev + textNode.content, '');
}
if (!type || type.indexOf('svg_') !== 0) {
throw new Error(
`Could not render type '${type}'. Make sure to only have components inside .`,
);
}
const cleanedType = type.slice(4);
const attributes = Object.keys(props || {}).reduce(
// @ts-ignore
(prev, k) => (props[k] ? `${prev} ${toSnakeCase(k)}="${props[k]}"` : prev),
'',
);
let string = `<${cleanedType}${attributes}`;
if (!children || !children.length) {
string += '/>\n';
} else {
string += '>\n';
string += (children || []).reduce((prev, c) => `${prev} ${makeSvgString(c)}`, '');
string += `${cleanedType}>\n`;
}
return string;
}
export class SvgRenderer extends ViewRenderer {
getDefaultGroupName(props: Props) {
return props.name || 'Svg';
}
renderBackingLayers(node: TreeNode) {
const layers = super.renderBackingLayers(node);
const { layout, props, children, style } = node;
// add the "xmlns:xlink" namespace so we can use `href`
props['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
const svgString = makeSvgString({
type: 'svg_svg',
props,
children,
style,
layout,
});
const svgLayer = makeSvgLayer(layout, 'Shape', svgString);
layers.push(svgLayer);
return layers;
}
}
================================================
FILE: src/renderers/SymbolInstanceRenderer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { SketchRenderer } from './SketchRenderer';
import {
makeSymbolInstance,
makeRect,
makeJSONDataReference,
makeOverride,
} from '../jsonUtils/models';
import { TreeNode } from '../types';
import { getSymbolMasterById, SymbolInstanceProps } from '../symbol';
import { getImageDataFromURL } from '../utils/getImageDataFromURL';
type Override = {
type: 'symbolID' | 'stringValue' | 'layerStyle' | 'textStyle' | 'flowDestination' | 'image';
path: string;
name: string;
symbolID?: string;
};
const findInGroup = (layer: FileFormat.AnyGroup, type: string): FileFormat.AnyLayer | undefined =>
layer && layer.layers && layer.layers.find((l) => l._class === type);
const hasImageFill = (layer: FileFormat.AnyLayer): boolean =>
!!(layer.style && layer.style.fills && layer.style.fills.some((f) => f.image));
const removeDuplicateOverrides = (overrides: Array): Array => {
const seen: { [path: string]: boolean } = {};
return overrides.filter(({ path }) => {
const isDuplicate = typeof seen[path] !== 'undefined';
seen[path] = true;
return !isDuplicate;
});
};
const extractOverridesReducer = (path: string) => (
overrides: Override[],
layer: FileFormat.AnyLayer,
): Override[] => {
if (layer._class === 'text') {
return overrides.concat({
type: 'stringValue',
path: `${path}${layer.do_objectID}`,
name: layer.name,
});
}
if (layer._class === 'group') {
// here we're doing some look-ahead to see if this group contains a group
// that contains text. this is the structure that will appear if the user
// creates a ` ` element with a custom name
const subGroup = findInGroup(layer, 'group') as FileFormat.Group;
const textLayer = findInGroup(subGroup, 'text') as FileFormat.Text;
if (textLayer) {
return overrides.concat({
type: 'stringValue',
path: `${path}${textLayer.do_objectID}`,
name: textLayer.name,
});
}
// here we're doing look-ahead to see if this group contains a shapeGroup
// with an image fill. if it does we can do an image override on that
// fill
const shapeGroup = findInGroup(layer, 'shapeGroup');
if (shapeGroup && hasImageFill(shapeGroup)) {
return overrides.concat({
type: 'image',
path: `${path}${shapeGroup.do_objectID}`,
name: layer.name,
});
}
}
if (layer._class === 'symbolInstance') {
return overrides.concat({
type: 'symbolID',
path: `${path}${layer.do_objectID}`,
name: layer.name,
symbolID: layer.symbolID,
});
}
if (
(layer._class === 'shapeGroup' || layer._class === 'artboard' || layer._class === 'group') &&
layer.layers
) {
return layer.layers.reduce(extractOverridesReducer(path), overrides);
}
return overrides;
};
const extractOverrides = (layers: FileFormat.AnyLayer[] = [], path?: string): Override[] => {
const overrides = layers.reduce(extractOverridesReducer(path || ''), []);
return removeDuplicateOverrides(overrides);
};
export class SymbolInstanceRenderer extends SketchRenderer {
renderGroupLayer({
layout,
props,
}: TreeNode) {
const bridge = this.platformBridge;
const masterTree = getSymbolMasterById(props.symbolID);
if (!masterTree) {
throw new Error(
'Trying to create a symbol instance for a Symbol Master that does not exists',
);
}
const symbolInstance = makeSymbolInstance(
makeRect(layout.left, layout.top, layout.width, layout.height),
masterTree.symbolID,
props.name,
props.resizingConstraint,
);
const { overrides } = props;
if (!overrides) {
return symbolInstance;
}
const overridableLayers = extractOverrides(masterTree.layers);
const overrideValues = overridableLayers.reduce(function inject(
memo: FileFormat.OverrideValue[],
reference: Override,
) {
if (reference.type === 'symbolID') {
const newPath = `${reference.path}/`;
const originalMaster = getSymbolMasterById(reference.symbolID);
if (!originalMaster) {
return memo;
}
if (reference.name in overrides) {
const overrideValue = overrides[reference.name];
// @ts-ignore
const overrideSymbolId = overrideValue.symbolID;
if (typeof overrideValue !== 'function' || typeof overrideSymbolId !== 'string') {
throw new Error(
`The overriden nested symbol needs to be the constructor of another symbol.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
const replacementMaster = getSymbolMasterById(overrideSymbolId);
if (!replacementMaster) {
return memo;
}
if (
originalMaster.frame.width !== replacementMaster.frame.width ||
originalMaster.frame.height !== replacementMaster.frame.height
) {
throw new Error(
`The overriden nested symbol needs to have the same dimensions.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
memo.push(makeOverride(reference.path, reference.type, replacementMaster.symbolID));
extractOverrides(replacementMaster.layers, newPath).reduce(inject, memo);
return memo;
}
extractOverrides(originalMaster.layers, newPath).reduce(inject, memo);
return memo;
}
if (!overrides.hasOwnProperty(reference.name)) {
return memo;
}
const overrideValue = overrides[reference.name];
if (reference.type === 'stringValue') {
if (typeof overrideValue !== 'string') {
throw new Error(
`The override value of a Text must be a string.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
memo.push(makeOverride(reference.path, reference.type, overrideValue));
}
if (reference.type === 'image') {
if (typeof overrideValue !== 'string') {
throw new Error(
`The override value of an Image must be a url.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
memo.push(
makeOverride(
reference.path,
reference.type,
makeJSONDataReference(getImageDataFromURL(bridge)(overrideValue)),
),
);
}
return memo;
},
[]);
symbolInstance.overrideValues = overrideValues;
return symbolInstance;
}
}
================================================
FILE: src/renderers/SymbolMasterRenderer.ts
================================================
import { makeSymbolMaster, makeRect } from '../jsonUtils/models';
import { SketchRenderer } from './SketchRenderer';
import { TreeNode } from '../types';
import { SymbolMasterProps } from '../symbol';
export class SymbolMasterRenderer extends SketchRenderer {
renderGroupLayer({
layout,
props,
}: TreeNode) {
return makeSymbolMaster(
makeRect(layout.left, layout.top, layout.width, layout.height),
props.symbolID,
props.name,
);
}
}
================================================
FILE: src/renderers/TextRenderer.ts
================================================
import { SketchRenderer } from './SketchRenderer';
import { TreeNode } from '../types';
import { makeTextLayer } from '../jsonUtils/textLayers';
import { makeRect } from '../jsonUtils/models';
import { TextStyles } from '../sharedStyles/TextStyles';
import { Props } from '../components/Text';
export class TextRenderer extends SketchRenderer {
getDefaultGroupName(props: Props) {
return props.name || 'Text';
}
renderBackingLayers({ layout, style, textStyle, props }: TreeNode) {
// Append all text nodes's content into one string if name is missing
const resolvedName = props.name
? props.name
: props.textNodes.map((textNode) => textNode.content).join('');
const frame = makeRect(0, 0, layout.width, layout.height);
const layer = makeTextLayer(this.platformBridge)(
frame,
resolvedName,
props.textNodes,
style,
props.resizingConstraint,
props.shadows,
);
const resolvedTextStyle = TextStyles(() => this.platformBridge).resolve(textStyle);
if (resolvedTextStyle) {
if (!layer.style) {
layer.style = resolvedTextStyle.sketchStyle;
}
layer.sharedStyleID = resolvedTextStyle.sharedObjectID;
}
return [layer];
}
}
================================================
FILE: src/renderers/ViewRenderer.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import { SketchRenderer } from './SketchRenderer';
import { makeRect } from '../jsonUtils/models';
import { makeRectShapeLayer, makeShapeGroup } from '../jsonUtils/shapeLayers';
import { TreeNode } from '../types';
import { createBorders } from '../jsonUtils/borders';
import { hasAnyDefined } from '../utils/hasAnyDefined';
import { Props } from '../components/View';
const VISIBLE_STYLES = [
'shadowColor',
'shadowOffset',
'shadowOpacity',
'shadowRadius',
'shadowSpread',
'backgroundColor',
'borderColor',
'borderTopColor',
'borderRightColor',
'borderBottomColor',
'borderLeftColor',
'borderStyle',
'borderTopStyle',
'borderRightStyle',
'borderBottomStyle',
'borderLeftStyle',
'borderWidth',
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
];
const OVERFLOW_STYLES = ['overflow', 'overflowX', 'overflowY'];
export class ViewRenderer extends SketchRenderer {
getDefaultGroupName(_props: Props) {
return 'View';
}
renderBackingLayers({
layout,
style,
props,
}: TreeNode): (
| FileFormat.ShapePath
| FileFormat.Rectangle
| FileFormat.SymbolMaster
| FileFormat.Group
| FileFormat.Polygon
| FileFormat.Star
| FileFormat.Triangle
| FileFormat.ShapeGroup
| FileFormat.Text
| FileFormat.SymbolInstance
| FileFormat.Slice
| FileFormat.Hotspot
| FileFormat.Bitmap
)[] {
let layers: FileFormat.ShapeGroup[] = [];
// NOTE(lmr): the group handles the position, so we just care about width/height here
const {
borderTopLeftRadius = 0,
borderTopRightRadius = 0,
borderBottomRightRadius = 0,
borderBottomLeftRadius = 0,
} = style;
if (!hasAnyDefined(style, VISIBLE_STYLES)) {
return layers;
}
const frame = makeRect(0, 0, layout.width, layout.height);
const radii = [
borderTopLeftRadius,
borderTopRightRadius,
borderBottomRightRadius,
borderBottomLeftRadius,
];
const shapeLayer = makeRectShapeLayer(
0,
0,
layout.width,
layout.height,
radii,
props.resizingConstraint,
);
const content = makeShapeGroup(frame, [shapeLayer], style, props.shadows);
if (hasAnyDefined(style, OVERFLOW_STYLES)) {
if (
style.overflow === 'hidden' ||
style.overflow === 'scroll' ||
style.overflowX === 'hidden' ||
style.overflowX === 'scroll' ||
style.overflowY === 'hidden' ||
style.overflowY === 'scroll'
) {
content.hasClippingMask = true;
}
}
const contents = createBorders(content, layout, style);
layers = layers.concat(contents);
return layers;
}
}
================================================
FILE: src/renderers/index.ts
================================================
export { ArtboardRenderer as sketch_artboard } from './ArtboardRenderer';
export { ImageRenderer as sketch_image } from './ImageRenderer';
export { SvgRenderer as sketch_svg } from './SvgRenderer';
export { TextRenderer as sketch_text } from './TextRenderer';
export { ViewRenderer as sketch_view } from './ViewRenderer';
export { SymbolInstanceRenderer as sketch_symbolinstance } from './SymbolInstanceRenderer';
export { SymbolMasterRenderer as sketch_symbolmaster } from './SymbolMasterRenderer';
================================================
FILE: src/resets.ts
================================================
import { SketchDocumentData, SketchPage } from './types';
import { isNativeDocument } from './utils/isNativeDocument';
import { isNativeSymbolsPage } from './utils/isNativeSymbolsPage';
export const resetLayer = (container: SketchDocumentData | SketchPage) => {
if (isNativeDocument(container)) {
resetDocument(container);
return;
}
const layers = container.children();
// Skip last child since it is the container itself
for (let l = 0; l < layers.length - 1; l += 1) {
const layer = layers[l];
layer.removeFromParent();
}
};
// Clear out all document pages and layers
export const resetDocument = (documentData: SketchDocumentData) => {
// Get Pages and delete them all (Except Symbols Page)
const pages = documentData.pages();
for (let index = pages.length - 1; index >= 0; index -= 1) {
const page = pages[index];
// Don't delete symbols page
if (!isNativeSymbolsPage(page)) {
if (pages.length > 1) {
documentData.removePageAtIndex(index);
} else {
resetLayer(page);
}
}
}
};
================================================
FILE: src/sharedStyles/TextStyles.ts
================================================
import { FileFormat1 as FileFormat } from '@sketch-hq/sketch-file-format-ts';
import {
SketchDocumentData,
SketchDocument,
WrappedSketchDocument,
TextStyle,
PlatformBridge,
} from '../types';
import { getSketchVersion } from '../utils/getSketchVersion';
import { hashStyle } from '../utils/hashStyle';
import { getDocument } from '../utils/getDocument';
import { sharedTextStyles } from '../utils/sharedTextStyles';
import { makeTextStyle } from '../jsonUtils/textLayers';
import { pick } from '../utils/pick';
import { INHERITABLE_FONT_STYLES } from '../utils/constants';
type MurmurHash = string;
type RegisteredStyle = {
cssStyle: TextStyle;
name: string;
sketchStyle: FileFormat.Style;
sharedObjectID: FileFormat.Uuid;
};
type StyleHash = { [key: string]: RegisteredStyle };
type Options = {
clearExistingStyles?: boolean;
document?: SketchDocumentData | SketchDocument | WrappedSketchDocument;
};
let _styles: StyleHash = {};
const _byName: { [key: string]: MurmurHash } = {};
export const TextStyles = (getDefaultBridge: () => PlatformBridge) => ({
registerStyle(
name: string,
style: TextStyle,
platformBridge: PlatformBridge = getDefaultBridge(),
) {
const safeStyle = pick(style, INHERITABLE_FONT_STYLES);
const hash = hashStyle(safeStyle);
const sketchStyle = makeTextStyle(platformBridge)(safeStyle);
const sharedObjectID = sharedTextStyles.addStyle(name, sketchStyle);
// FIXME(gold): side effect :'(
_byName[name] = hash;
_styles[hash] = {
cssStyle: safeStyle,
name,
sketchStyle,
sharedObjectID,
};
},
create(
styles: { [key: string]: TextStyle },
options: Options = {},
platformBridge: PlatformBridge = getDefaultBridge(),
): StyleHash {
const { clearExistingStyles, document } = options;
const doc = getDocument(document);
const sketchVersion = getSketchVersion();
if (sketchVersion !== 'NodeJS' && sketchVersion < 50) {
if (doc) {
doc.showMessage('💎 Requires Sketch 50+ 💎');
}
return {};
}
sharedTextStyles.setDocument(doc);
if (clearExistingStyles) {
_styles = {};
sharedTextStyles.setStyles([]);
}
Object.keys(styles).forEach((name) => this.registerStyle(name, styles[name], platformBridge));
return _styles;
},
resolve(style?: TextStyle): RegisteredStyle | undefined {
if (!style) {
return undefined;
}
const safeStyle = pick(style, INHERITABLE_FONT_STYLES);
const hash = hashStyle(safeStyle);
return _styles[hash];
},
get(
name: string,
document?: SketchDocumentData | SketchDocument | WrappedSketchDocument,
): TextStyle | undefined {
const hash = _byName[name];
const style = _styles[hash];
if (style) {
return style.cssStyle;
}
return sharedTextStyles.getStyle(name, document ? getDocument(document) : undefined);
},
clear(): void {
_styles = {};
sharedTextStyles.setStyles([]);
},
toJSON(): FileFormat.SharedStyle[] {
return Object.keys(_styles).map((k) => ({
_class: 'sharedStyle',
do_objectID: _styles[k].sharedObjectID,
name: _styles[k].name,
value: _styles[k].sketchStyle,
}));
},
styles() {
return _styles;
},
});
================================================
FILE: src/stylesheet/expandStyle.ts
================================================
import { RawStyle, Style } from './types';
const { hasOwnProperty } = Object.prototype;
const styleShortHands: { [key: string]: { [subKey: string]: boolean } } = {
borderColor: {
borderTopColor: true,
borderRightColor: true,
borderBottomColor: true,
borderLeftColor: true,
},
borderRadius: {
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderBottomRightRadius: true,
borderBottomLeftRadius: true,
},
borderStyle: {
borderTopStyle: true,
borderRightStyle: true,
borderBottomStyle: true,
borderLeftStyle: true,
},
borderWidth: {
borderTopWidth: true,
borderRightWidth: true,
borderBottomWidth: true,
borderLeftWidth: true,
},
margin: {
marginTop: true,
marginRight: true,
marginBottom: true,
marginLeft: true,
},
marginHorizontal: {
marginRight: true,
marginLeft: true,
},
marginVertical: {
marginTop: true,
marginBottom: true,
},
overflow: {
overflowX: true,
overflowY: true,
},
padding: {
paddingTop: true,
paddingRight: true,
paddingBottom: true,
paddingLeft: true,
},
paddingHorizontal: {
paddingRight: true,
paddingLeft: true,
},
paddingVertical: {
paddingTop: true,
paddingBottom: true,
},
textDecorationLine: {
textDecoration: true,
},
writingDirection: {
direction: true,
},
};
/**
* Alpha-sort properties, apart from shorthands – they must appear before the
* longhand properties that they expand into. This lets more specific styles
* override less specific styles, whatever the order in which they were
* originally declared.
*/
const sortProps = (propsArray: string[]) =>
propsArray.sort((a, b) => {
const expandedA = styleShortHands[a];
const expandedB = styleShortHands[b];
if (expandedA && expandedA[b]) {
return -1;
}
if (expandedB && expandedB[a]) {
return 1;
}
return a < b ? -1 : a > b ? 1 : 0;
});
/**
* Expand the shorthand properties to isolate every declaration from the others.
*/
export const expandStyle = (style: RawStyle): Style => {
if (!style) return style;
const propsArray = Object.keys(style);
const sortedProps = sortProps(propsArray);
const resolvedStyle: Style = {};
for (let i = 0; i < sortedProps.length; i++) {
const key = sortedProps[i];
const expandedProps = styleShortHands[key];
const value = style[key];
if (expandedProps) {
for (const propName in expandedProps) {
if (hasOwnProperty.call(expandedProps, propName)) {
resolvedStyle[propName] = value;
}
}
} else {
resolvedStyle[key] = value;
}
}
return resolvedStyle;
};
================================================
FILE: src/stylesheet/index.ts
================================================
import { expandStyle } from './expandStyle';
import {
RawStyle,
RawStyles,
Rules,
Style,
StyleId,
StyleSheetInstance,
Transform,
UserStyle,
UserStyles,
} from './types';
let _id = 0;
const guid = () => _id++;
const declarationRegistry: { [id: string]: Style } = {};
const extractRules = (style: RawStyle): Rules => {
const declarations: { [key: string]: any } = {};
Object.keys(style).forEach((key) => {
if (key[0] === ':') {
// pseudo style. ignore for now.
} else if (key[0] === '@') {
// Media query. ignore for now.
} else {
declarations[key] = style[key];
}
});
return {
declarations,
};
};
const registerStyle = (style: RawStyle): StyleId => {
// TODO(lmr):
// do "proptype"-like validation here in non-production build
const id = guid();
const rules = extractRules(style);
declarationRegistry[id] = expandStyle(rules.declarations);
return id;
};
const getStyle = (id: StyleId): Style => declarationRegistry[id];
const create = (styles: RawStyles): StyleSheetInstance => {
const result: StyleSheetInstance = {};
Object.keys(styles).forEach((key) => {
result[key] = registerStyle(styles[key]);
});
return result;
};
const mergeTransforms = (a?: Transform, b?: Transform): Transform | undefined => {
if (!a || a.length === 0) return b; // in this case, a has nothing to contribute.
const result: Transform = [];
const transformsInA: { [key: string]: number } = a.reduce((hash, t) => {
const key = Object.keys(t)[0];
result.push(t);
hash[key] = result.length - 1;
return hash;
}, {});
(b || []).forEach((t) => {
const key = Object.keys(t)[0];
const index = transformsInA[key];
if (index !== undefined) {
result[index] = t;
} else {
result.push(t);
}
});
return result;
};
// merge two style hashes together. Sort of like `Object.assign`, but is aware of `transform` as a
// special case.
// NOTE(lmr): mutates the first argument!
const mergeStyle = (a: Style, b: Style): Style => {
Object.keys(b).forEach((key) => {
if (key === 'transform') {
a[key] = mergeTransforms(a[key], b[key]);
} else {
a[key] = b[key];
}
});
return a;
};
const flattenStyle = (input?: UserStyles | null): Style | undefined => {
if (Array.isArray(input)) {
let acc: Style = {};
return input.reduce