├── .nvmrc ├── .gitignore ├── src ├── errors │ ├── index.js │ ├── clientError.js │ └── serverError.js ├── template │ ├── .gitignore │ ├── tsconfig.test.json │ ├── tsconfig.json │ ├── eslint.config.mjs │ ├── README.md │ ├── tests │ │ ├── fixtures │ │ │ ├── screens.json │ │ │ ├── index.ts │ │ │ ├── project.json │ │ │ ├── version.json │ │ │ └── components.json │ │ └── index.test.ts │ ├── jest.config.mjs │ └── src │ │ └── index.ts ├── config │ ├── webpack.exec.mjs │ ├── webpack.prod.mjs │ ├── constants.js │ ├── webpack.dev.mjs │ └── webpack.common.mjs ├── utils │ ├── package.js │ ├── paths.js │ ├── webpack │ │ ├── transform-config.js │ │ └── manifest-builder.js │ ├── eslint.js │ └── highlight-syntax │ │ ├── index.js │ │ └── colorize.js ├── sample-data │ ├── index.js │ ├── screens.json │ ├── project.json │ ├── version.json │ └── components.json ├── commands │ ├── build.js │ ├── start.js │ ├── test.js │ ├── publish │ │ ├── authenticationService.js │ │ ├── manifest-validator.js │ │ ├── apiClient.js │ │ └── index.js │ ├── create.js │ └── exec.js └── index.js ├── eslint.config.mjs ├── pull_request_template.md ├── LICENSE ├── package.json ├── .circleci └── config.yml └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.9.4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | npm-debug.log* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /src/errors/index.js: -------------------------------------------------------------------------------- 1 | export * from './clientError.js'; 2 | export * from './serverError.js'; -------------------------------------------------------------------------------- /src/template/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | .DS_Store 5 | Thumbs.db 6 | npm-debug.log* 7 | dist 8 | build -------------------------------------------------------------------------------- /src/template/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*", 5 | "tests/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/config/webpack.exec.mjs: -------------------------------------------------------------------------------- 1 | import { merge } from "webpack-merge"; 2 | import dev from "./webpack.dev.mjs"; 3 | 4 | export default merge(dev, { 5 | target: "node" 6 | }); -------------------------------------------------------------------------------- /src/config/webpack.prod.mjs: -------------------------------------------------------------------------------- 1 | import { merge } from "webpack-merge"; 2 | import common from "./webpack.common.mjs"; 3 | 4 | export default merge(common, { 5 | mode: "production", 6 | output: { 7 | filename: "[name].[chunkhash:8].js" 8 | } 9 | }); -------------------------------------------------------------------------------- /src/config/constants.js: -------------------------------------------------------------------------------- 1 | import { isCI } from "ci-info"; 2 | 3 | export const constants = { 4 | buildDirName: "build", 5 | bundleName: "main", 6 | isCI, 7 | accessToken: process.env.ZEM_ACCESS_TOKEN, 8 | apiBaseUrl: process.env.API_BASE_URL || "https://api.zeplin.io", 9 | apiClientId: process.env.API_CLIENT_ID || "5bbc983af6c410493afb03f1" 10 | }; -------------------------------------------------------------------------------- /src/errors/clientError.js: -------------------------------------------------------------------------------- 1 | export class ClientError extends Error { 2 | constructor(status, extra, msg) { 3 | const message = `${msg || "Client error"}`; 4 | super(message); 5 | 6 | Error.captureStackTrace(this, this.constructor); 7 | 8 | this.status = status; 9 | this.message = message; 10 | this.name = "ClientError"; 11 | this.extra = extra; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/errors/serverError.js: -------------------------------------------------------------------------------- 1 | export class ServerError extends Error { 2 | constructor(status, extra, msg) { 3 | const message = `${msg || `(${status}) Server error`}`; 4 | super(message); 5 | 6 | Error.captureStackTrace(this, this.constructor); 7 | 8 | this.status = status; 9 | this.message = message; 10 | this.name = "ServerError"; 11 | this.extra = extra; 12 | } 13 | }; -------------------------------------------------------------------------------- /src/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "Node16", 5 | "moduleResolution": "node16", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "isolatedModules": true, 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "baseUrl": "." 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/package.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import * as paths from "./paths.js"; 3 | 4 | export const getPackageJson = () => { 5 | const packageJsonFile = paths.resolveExtensionPath("./package.json"); 6 | if (fs.existsSync(packageJsonFile)) { 7 | return fs.readJSONSync(packageJsonFile); 8 | } 9 | }; 10 | 11 | export const getPackageVersion = () => { 12 | const packageJson = getPackageJson(); 13 | if (packageJson) { 14 | return packageJson.version; 15 | } 16 | 17 | return undefined; 18 | }; -------------------------------------------------------------------------------- /src/utils/paths.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { constants } from "../config/constants.js"; 5 | 6 | const extensionRoot = fs.realpathSync(process.cwd()); 7 | 8 | export function resolveExtensionPath(relativePath = "") { 9 | return path.resolve(extensionRoot, relativePath); 10 | } 11 | 12 | export function resolveBuildPath(relativePath = "") { 13 | return path.resolve(extensionRoot, constants.buildDirName, relativePath); 14 | } 15 | 16 | export function getRcFilePath() { 17 | return path.resolve(os.homedir(), ".zemrc"); 18 | } 19 | -------------------------------------------------------------------------------- /src/template/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import js from "@eslint/js"; 3 | import globals from "globals"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default defineConfig([ 7 | js.configs.recommended, 8 | tseslint.configs.recommended, 9 | { 10 | languageOptions: { 11 | globals: { 12 | ...globals.browser 13 | }, 14 | ecmaVersion: 2017, 15 | sourceType: "module" 16 | }, 17 | ignores: ["eslint.config.mjs"], 18 | rules: { 19 | "no-console": "error" 20 | } 21 | }]); 22 | -------------------------------------------------------------------------------- /src/sample-data/index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import path from "node:path"; 3 | import fs from "fs-extra"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | const project = fs.readJSONSync(`${__dirname}/project.json`); 9 | const componentVariants = fs.readJSONSync(`${__dirname}/componentVariants.json`); 10 | const components = fs.readJSONSync(`${__dirname}/components.json`); 11 | const screens = fs.readJSONSync(`${__dirname}/screens.json`); 12 | const version = fs.readJSONSync(`${__dirname}/version.json`); 13 | 14 | export default { 15 | componentVariants, 16 | components, 17 | project, 18 | screens, 19 | screenVersion: version 20 | }; -------------------------------------------------------------------------------- /src/utils/webpack/transform-config.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs-extra"; 3 | import { resolveExtensionPath } from "../paths.js"; 4 | 5 | export default async function (config) { 6 | const userConfigPath = resolveExtensionPath("webpack.zem.js"); 7 | 8 | try { 9 | if (fs.existsSync(userConfigPath)) { 10 | const transformer = (await import(userConfigPath)).default; 11 | 12 | return transformer(config); 13 | } 14 | } catch (error) { 15 | console.log(chalk.red(`An error occurred while applying user config:`)); 16 | console.error(error); 17 | console.log(chalk.yellow("Falling back to default build configuration!")); 18 | } 19 | 20 | return config; 21 | }; -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import { defineConfig } from "eslint/config"; 3 | import js from "@eslint/js"; 4 | import zeplinEslintConfig from "@zeplin/eslint-config/node.js"; 5 | 6 | export default defineConfig([ 7 | js.configs.recommended, 8 | { 9 | plugins: { 10 | zeplin: zeplinEslintConfig 11 | }, 12 | rules: { 13 | "no-process-exit": "off", 14 | "no-sync": "off", 15 | "class-methods-use-this": "off", 16 | "no-unused-vars": ["error", { "caughtErrors": "none" }] 17 | } 18 | }, 19 | { files: ["**/*.js"], languageOptions: { sourceType: "module" } }, 20 | { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.node } }, 21 | { ignores: ["src/template/*"] }, 22 | ]); -------------------------------------------------------------------------------- /src/template/README.md: -------------------------------------------------------------------------------- 1 | # {{displayName}} 2 | 3 | {{description}} 4 | 5 | ## Getting started 6 | 7 | Add the extension to your project from [extensions.zeplin.io](https://extensions.zeplin.io). 8 | 9 | 16 | 17 | 25 | 26 | ## Development 27 | 28 | This extension is developed using [zem](https://github.com/zeplin/zem), Zeplin Extension Manager. zem is a command line tool that lets you quickly create, test and publish extensions. 29 | 30 | To learn more about creating Zeplin extensions, [see documentation](https://github.com/zeplin/zeplin-extension-documentation). 31 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Change description 2 | 3 | > Description here 4 | 5 | ## Type of change 6 | - [ ] Bug fix (fixes an issue) 7 | - [ ] New feature (adds functionality) 8 | 9 | ## Related issues 10 | 11 | > Fix [#1]() 12 | 13 | ## Checklists 14 | 15 | ### Development 16 | 17 | - [ ] Lint rules pass locally 18 | - [ ] Application changes have been tested thoroughly 19 | - [ ] Automated tests covering modified code pass 20 | 21 | ### Security 22 | 23 | - [ ] Security impact of change has been considered 24 | - [ ] Code follows company security practices and guidelines 25 | 26 | ### Code review 27 | 28 | - [ ] Pull request has a descriptive title and context useful to a reviewer. Screenshots or screencasts are attached as necessary 29 | - [ ] "Ready for review" label attached and reviewers assigned 30 | - [ ] Changes have been reviewed by at least one other contributor 31 | - [ ] Pull request linked to task tracker where applicable 32 | -------------------------------------------------------------------------------- /src/template/tests/fixtures/screens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Simple screen", 4 | "description": "", 5 | "tags": [] 6 | }, 7 | { 8 | "name": "Screen with description", 9 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lacus magna, elementum vel orci a, ornare molestie augue. Fusce semper pharetra augue ac condimentum. Suspendisse eget varius nulla. Vivamus feugiat ligula nec volutpat tincidunt. Curabitur iaculis purus convallis, scelerisque nulla vitae, condimentum ante. Curabitur vehicula nunc in massa congue, quis varius metus placerat. Integer feugiat, orci eu sagittis interdum, tortor nulla pulvinar mauris, egestas suscipit eros orci sit amet sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", 10 | "tags": [] 11 | }, 12 | { 13 | "name": "Screen with 3 tags", 14 | "description": "", 15 | "tags": ["First tag", "Second tag", "Third tag"] 16 | } 17 | ] -------------------------------------------------------------------------------- /src/template/jest.config.mjs: -------------------------------------------------------------------------------- 1 | import { ESM_TS_TRANSFORM_PATTERN } from 'ts-jest'; 2 | 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | 10 | export default { 11 | displayName: "unit", 12 | rootDir: path.resolve(__dirname, "."), 13 | testMatch: [ 14 | "/tests/**/*.test.ts" 15 | ], 16 | extensionsToTreatAsEsm: [".ts"], 17 | moduleDirectories: ["node_modules"], 18 | transform: { 19 | [ESM_TS_TRANSFORM_PATTERN]: ["ts-jest", { 20 | tsconfig: "/tsconfig.test.json", 21 | useESM: true 22 | }] 23 | }, 24 | transformIgnorePatterns: [ 25 | "(.+)\\.json$" 26 | ], 27 | moduleNameMapper: { 28 | "(.+)\\.js$": "$1" 29 | }, 30 | collectCoverageFrom: [ 31 | "src/**/*.{js,ts}", 32 | "!**/node_modules/**", 33 | "!**/vendor/**", 34 | "!**/tests/**", 35 | "!**/coverage/**" 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/eslint.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "fs-extra"; 3 | 4 | export function findESLintConfig(cwd = process.cwd()) { 5 | const configFiles = [ 6 | "eslint.config.js", 7 | "eslint.config.mjs", 8 | "eslint.config.cjs", 9 | "eslint.config.ts", 10 | "eslint.config.mts", 11 | "eslint.config.cts", 12 | ".eslintrc.js", 13 | ".eslintrc.cjs", 14 | ".eslintrc.yaml", 15 | ".eslintrc.yml", 16 | ".eslintrc.json", 17 | ".eslintrc", // Legacy format (usually JSON/YAML) 18 | "package.json" 19 | ]; 20 | 21 | for (const file of configFiles) { 22 | const fullPath = path.join(cwd, file); 23 | if (fs.existsSync(fullPath)) { 24 | if (file === "package.json") { 25 | const pkg = fs.readJSONSync(fullPath, "utf8"); 26 | if (pkg.eslintConfig) { 27 | return pkg.eslintConfig; 28 | } 29 | } else { 30 | return fullPath; 31 | } 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zeplin, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/config/webpack.dev.mjs: -------------------------------------------------------------------------------- 1 | import { merge } from "webpack-merge"; 2 | import common from "./webpack.common.mjs"; 3 | import { resolveExtensionPath } from "../utils/paths.js"; 4 | 5 | const readmePath = resolveExtensionPath("README.md"); 6 | const packageJsonPath = resolveExtensionPath("package.json"); 7 | 8 | export default merge(common, { 9 | output: { 10 | filename: "[name].js", 11 | publicPath: "/" 12 | }, 13 | devtool: "inline-source-map", 14 | devServer: { 15 | hot: true, 16 | host: "127.0.0.1", 17 | port: 7070, 18 | allowedHosts: "all", 19 | watchFiles: [packageJsonPath, readmePath], 20 | headers: { 21 | "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Range", 22 | "Access-Control-Allow-Origin": "*" 23 | }, 24 | // Disable cross-origin header check to allow the extension to be installed from a different origin. 25 | // Might be removed after the following issue is resolved: 26 | // https://github.com/webpack/webpack-dev-server/issues/5446#issuecomment-2768816082 27 | setupMiddlewares: middlewares => middlewares.filter(middleware => middleware.name !== "cross-origin-header-check") 28 | } 29 | }); -------------------------------------------------------------------------------- /src/commands/build.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import webpack from "webpack"; 3 | import transformConfig from "../utils/webpack/transform-config.js"; 4 | 5 | export default async function (webpackConfig, { throwOnError = false, printStats = true } = {}) { 6 | const options = await transformConfig(webpackConfig); 7 | const compiler = webpack(options); 8 | 9 | console.log("Building extension...\n"); 10 | 11 | return new Promise((resolve, reject) => { 12 | compiler.run((err, stats) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | 17 | if (stats.hasErrors() && throwOnError) { 18 | const error = new Error("Compile error"); 19 | error.name = "CompileError"; 20 | error.stats = stats; 21 | 22 | return reject(error); 23 | } 24 | 25 | if (printStats) { 26 | console.log(`${stats.toString({ 27 | errors: true, 28 | colors: true 29 | })}\n`); 30 | } 31 | 32 | if (!stats.hasErrors() && !stats.hasWarnings()) { 33 | console.log(chalk.green("Compiled successfully")); 34 | } 35 | 36 | return resolve({ 37 | stats 38 | }); 39 | }); 40 | }); 41 | }; -------------------------------------------------------------------------------- /src/template/tests/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ComponentData, 4 | ComponentVariant, 5 | ComponentVariantData, 6 | Context, 7 | ContextData, 8 | Screen, 9 | ScreenData, 10 | Version, 11 | VersionData, 12 | } from '@zeplin/extension-model'; 13 | 14 | import project from './project.json'; 15 | import screensData from './screens.json'; 16 | import componentsData from './components.json'; 17 | import componentVariantsData from './componentVariants.json'; 18 | import versionData from './version.json'; 19 | import pkg from '../../package.json'; 20 | 21 | const defaultOptions = pkg.zeplin.options?.reduce((options: ContextData['options'], option: { 22 | id: string; 23 | default: string; 24 | }) => { 25 | options[option.id] = option.default; 26 | return options; 27 | }, {} as ContextData['options']); 28 | 29 | const singleComponents = componentsData.map((data: ComponentData) => new Component(data)); 30 | const variantComponents = componentVariantsData.map((data: ComponentVariantData) => new ComponentVariant(data)).reduce( 31 | (cs: Component[], variant: ComponentVariant) => cs.concat(variant.components!), [] as Component[], 32 | ); 33 | 34 | export const context = new Context({ project, options: defaultOptions }); 35 | export const version = new Version(versionData as VersionData); 36 | export const screens = screensData.map((data: ScreenData) => new Screen(data)); 37 | export const components = singleComponents.concat(variantComponents); 38 | -------------------------------------------------------------------------------- /src/sample-data/screens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Simple screen", 4 | "description": "", 5 | "tags": [] 6 | }, 7 | { 8 | "name": "Screen with description", 9 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lacus magna, elementum vel orci a, ornare molestie augue. Fusce semper pharetra augue ac condimentum. Suspendisse eget varius nulla. Vivamus feugiat ligula nec volutpat tincidunt. Curabitur iaculis purus convallis, scelerisque nulla vitae, condimentum ante. Curabitur vehicula nunc in massa congue, quis varius metus placerat. Integer feugiat, orci eu sagittis interdum, tortor nulla pulvinar mauris, egestas suscipit eros orci sit amet sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", 10 | "tags": [] 11 | }, 12 | { 13 | "name": "Screen with 3 tags", 14 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lacus magna, elementum vel orci a, ornare molestie augue. Fusce semper pharetra augue ac condimentum. Suspendisse eget varius nulla. Vivamus feugiat ligula nec volutpat tincidunt. Curabitur iaculis purus convallis, scelerisque nulla vitae, condimentum ante. Curabitur vehicula nunc in massa congue, quis varius metus placerat. Integer feugiat, orci eu sagittis interdum, tortor nulla pulvinar mauris, egestas suscipit eros orci sit amet sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", 15 | "tags": ["First tag", "Second tag", "Third tag"] 16 | } 17 | ] -------------------------------------------------------------------------------- /src/utils/highlight-syntax/index.js: -------------------------------------------------------------------------------- 1 | import Prism from "prismjs"; 2 | import colorize from "./colorize.js"; 3 | 4 | function flattenToken(token, type = "literal") { 5 | if (typeof token === "string") { 6 | return { 7 | token, 8 | type 9 | }; 10 | } 11 | 12 | if (typeof token.content === "string") { 13 | return { 14 | token: token.content, 15 | type: token.type 16 | }; 17 | } 18 | 19 | return [].concat(...token.content.map( 20 | tk => flattenToken(tk, token.type) 21 | )); 22 | } 23 | 24 | function groupTokensByLine(tokens) { 25 | let group = []; 26 | const groups = [group]; 27 | 28 | for (const token of tokens) { 29 | if (typeof token === "string" && token.includes("\n")) { 30 | const lines = token.split("\n"); 31 | 32 | group.push(lines[0]); 33 | 34 | for (let i = 1; i < lines.length - 1; i++) { 35 | groups.push([lines[i]]); 36 | } 37 | 38 | group = [lines[lines.length - 1]]; 39 | groups.push(group); 40 | } else { 41 | group.push(token); 42 | } 43 | } 44 | 45 | return groups; 46 | } 47 | 48 | function tokenize(code, lang) { 49 | const tokens = Prism.tokenize(code, Prism.languages[lang]); 50 | 51 | return [].concat(...tokens.map(flattenToken)); 52 | } 53 | 54 | function isSupported(language) { 55 | return !!Prism.languages[language]; 56 | } 57 | 58 | export default function (code, language) { 59 | if (!isSupported(language)) { 60 | return code; 61 | } 62 | 63 | return groupTokensByLine(tokenize(code, language)).map(line => 64 | line.map(({ token, type }) => colorize(token, type)).join("") 65 | ).join("\n"); 66 | }; -------------------------------------------------------------------------------- /src/template/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CodeExportObject, 3 | CodeObject, 4 | Component, 5 | Context, 6 | Extension, 7 | Layer, 8 | Screen, 9 | Version, 10 | } from '@zeplin/extension-model'; 11 | 12 | /** 13 | * Implement functions you want to work with, see documentation for details: 14 | * https://zeplin.github.io/extension-model/ 15 | */ 16 | const extension: Extension = { 17 | colors(context: Context): CodeObject { 18 | throw new Error('Not implemented yet.'); 19 | }, 20 | 21 | comment(context: Context, text: string): string { 22 | throw new Error('Not implemented yet.'); 23 | }, 24 | 25 | component(context: Context, selectedVersion: Version, selectedComponent: Component): CodeObject { 26 | throw new Error('Not implemented yet.'); 27 | }, 28 | 29 | exportColors(context: Context): CodeExportObject | CodeExportObject[] { 30 | throw new Error('Not implemented yet.'); 31 | }, 32 | 33 | exportSpacing(context: Context): CodeExportObject | CodeExportObject[] { 34 | throw new Error('Not implemented yet.'); 35 | }, 36 | 37 | exportTextStyles(context: Context): CodeExportObject | CodeExportObject[] { 38 | throw new Error('Not implemented yet.'); 39 | }, 40 | 41 | layer(context: Context, selectedLayer: Layer, version: Version): CodeObject { 42 | throw new Error('Not implemented yet.'); 43 | }, 44 | 45 | screen(context: Context, selectedVersion: Version, selectedScreen: Screen): CodeObject { 46 | throw new Error('Not implemented yet.'); 47 | }, 48 | 49 | spacing(context: Context): CodeObject { 50 | throw new Error('Not implemented yet.'); 51 | }, 52 | 53 | textStyles(context: Context): CodeObject { 54 | throw new Error('Not implemented yet.'); 55 | } 56 | }; 57 | 58 | export default extension; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zem", 3 | "version": "2.0.7", 4 | "description": "Create, test and publish Zeplin extensions with no build configuration", 5 | "homepage": "https://zeplin.io", 6 | "type": "module", 7 | "keywords": [ 8 | "zeplin", 9 | "zeplin-extension", 10 | "extension" 11 | ], 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/zeplin/zem.git" 16 | }, 17 | "engines": { 18 | "node": ">=20" 19 | }, 20 | "bin": { 21 | "zem": "./src/index.js" 22 | }, 23 | "files": [ 24 | "src" 25 | ], 26 | "scripts": { 27 | "lint": "eslint ./src" 28 | }, 29 | "dependencies": { 30 | "@babel/core": "^7.26.10", 31 | "@babel/preset-env": "^7.26.9", 32 | "@zeplin/extension-model": "^3.0.2", 33 | "adm-zip": "^0.5.16", 34 | "ajv": "^8.17.1", 35 | "ajv-formats": "^3.0.1", 36 | "babel-jest": "^29.7.0", 37 | "babel-loader": "^10.0.0", 38 | "case": "^1.6.3", 39 | "chalk": "^5.4.1", 40 | "ci-info": "^4.2.0", 41 | "commander": "^14.0.0", 42 | "copy-webpack-plugin": "^13.0.0", 43 | "core-js": "^3.41.0", 44 | "eslint-webpack-plugin": "^5.0.1", 45 | "fs-extra": "^11.3.0", 46 | "http-status-codes": "^2.3.0", 47 | "jest": "^29.7.0", 48 | "jest-config": "^30.0.0", 49 | "jsonwebtoken": "^9.0.2", 50 | "prismjs": "^1.30.0", 51 | "prompts": "^2.4.2", 52 | "qs": "^6.14.0", 53 | "resolve": "^1.22.10", 54 | "undici": "^7.8.0", 55 | "update-notifier": "^7.3.1", 56 | "webpack": "^5.99.7", 57 | "webpack-dev-server": "^5.2.1", 58 | "webpack-merge": "^6.0.1" 59 | }, 60 | "devDependencies": { 61 | "@eslint/js": "^9.26.0", 62 | "@types/fs-extra": "^11.0.4", 63 | "@types/jest": "^29.5.14", 64 | "@zeplin/eslint-config": "^2.3.2", 65 | "eslint": "^9.26.0", 66 | "globals": "^16.1.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build_and_test: 4 | docker: 5 | - image: zeplin/amazon-linux-ami:cci-node20 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | key: dependency-cache-{{ checksum "package.json" }} 10 | - run: npm install 11 | - save_cache: 12 | key: dependency-cache-{{ checksum "package.json" }} 13 | paths: 14 | - ./node_modules 15 | - run: npm run lint -- --quiet 16 | 17 | publish: 18 | docker: 19 | - image: zeplin/amazon-linux-ami:cci-node20 20 | steps: 21 | - checkout 22 | - run: 23 | name: Publish to NPM 24 | command: | 25 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 26 | npm publish --access public 27 | 28 | publish_beta: 29 | docker: 30 | - image: zeplin/amazon-linux-ami:cci-node20 31 | steps: 32 | - checkout 33 | - run: 34 | name: Publish to NPM 35 | command: | 36 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 37 | npm publish --access public --tag beta 38 | 39 | workflows: 40 | version: 2 41 | build_test_and_publish: 42 | jobs: 43 | # Run the build_and_test for all branches 44 | - build_and_test: 45 | filters: # required since `deploy` has tag filters AND requires `build` 46 | tags: 47 | only: /v[0-9]+(\.[0-9]+)*/ 48 | - publish: 49 | requires: 50 | - build_and_test 51 | filters: 52 | # Ignore any commit on any branch 53 | branches: 54 | ignore: /.*/ 55 | # Run the job only on version tags 56 | tags: 57 | only: /v[0-9]+(\.[0-9]+)*/ 58 | - publish_beta: 59 | requires: 60 | - build_and_test 61 | filters: 62 | branches: 63 | only: beta 64 | -------------------------------------------------------------------------------- /src/commands/start.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import WebpackDevServer from "webpack-dev-server"; 3 | import webpack from "webpack"; 4 | import transformConfig from "../utils/webpack/transform-config.js"; 5 | import devWebpackConfig from "../config/webpack.dev.mjs"; 6 | 7 | function createCompiler(config) { 8 | let compiler; 9 | try { 10 | compiler = webpack(config); 11 | } catch (err) { 12 | console.log(chalk.red("Compilation failed:")); 13 | console.error(err.message || err); 14 | process.exit(1); 15 | } 16 | 17 | compiler.hooks.done.tap("logStatPlugin", stats => { 18 | console.log(stats.toString({ 19 | errors: true, 20 | colors: true 21 | })); 22 | }); 23 | 24 | return compiler; 25 | } 26 | 27 | export default async function (host, port, allowedHosts) { 28 | const config = await transformConfig(devWebpackConfig); 29 | const compiler = createCompiler(config); 30 | 31 | const serverConfig = { 32 | ...devWebpackConfig.devServer, 33 | host: host || devWebpackConfig.devServer.host, 34 | port: port || devWebpackConfig.devServer.port, 35 | allowedHosts: allowedHosts ? allowedHosts.split(",") : devWebpackConfig.devServer.allowedHosts 36 | }; 37 | 38 | const server = new WebpackDevServer(serverConfig, compiler); 39 | 40 | const startServer = async () => { 41 | try { 42 | await server.start(); 43 | console.log(`Extension is served from ${chalk.blue.bold(`http://${serverConfig.host}:${serverConfig.port}/manifest.json`)}\n`); 44 | } catch (err) { 45 | console.log(err); 46 | } 47 | }; 48 | 49 | startServer(); 50 | 51 | const closeServer = async () => { 52 | await server.stop(); 53 | process.exit(); 54 | }; 55 | 56 | process.on("SIGTERM", closeServer); 57 | process.on("SIGINT", closeServer); 58 | process.on("warning", warning => { 59 | console.log(warning.stack); 60 | }); 61 | }; -------------------------------------------------------------------------------- /src/commands/test.js: -------------------------------------------------------------------------------- 1 | import { spawn } from "node:child_process"; 2 | import { readConfig } from "jest-config"; 3 | import { getPackageJson } from "../utils/package.js"; 4 | import resolve from "resolve"; 5 | 6 | async function getJestConfig(cwd = process.cwd()) { 7 | try { 8 | const { config, configPath } = await readConfig({}, cwd); 9 | return { config, configPath }; 10 | } catch (error) { 11 | console.log(`Could not found jest config: ${error.message}`); 12 | return false; 13 | } 14 | } 15 | 16 | function findJestBin(cwd = process.cwd()) { 17 | return new Promise((resolvePath, reject) => { 18 | resolve( 19 | "jest/bin/jest.js", 20 | { 21 | basedir: cwd, 22 | preserveSymlinks: false 23 | }, 24 | (err, res) => { 25 | if (err) return reject(err); 26 | resolvePath(res); 27 | } 28 | ); 29 | }); 30 | } 31 | 32 | export default async function (args) { 33 | const packageJson = getPackageJson(); 34 | let nodeOptions = process.env.NODE_OPTIONS; 35 | if (packageJson && packageJson.type === "module") { 36 | nodeOptions = `${nodeOptions ? `${nodeOptions} ` : ""}--experimental-vm-modules`; 37 | } 38 | 39 | const configFile = (await getJestConfig()).configPath; 40 | 41 | const jestBin = await findJestBin(); 42 | 43 | const subprocess = spawn("node", [ 44 | jestBin, 45 | "--config", 46 | configFile, 47 | ...args 48 | ], { 49 | env: { 50 | ...process.env, 51 | NODE_OPTIONS: nodeOptions 52 | }, 53 | stdio: "inherit" 54 | }); 55 | 56 | return new Promise((resolve, reject) => { 57 | subprocess.on("close", (code) => { 58 | if (code === 0) { 59 | resolve(true); 60 | } else { 61 | resolve(false); 62 | } 63 | }); 64 | 65 | subprocess.on("error", (err) => { 66 | reject(err); 67 | }); 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /src/template/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import extension from '../src/index.js'; 2 | import { components, context, screens, version } from './fixtures/index.js'; 3 | 4 | describe("Colors", () => { 5 | it("should generate code snippet", () => { 6 | const code = extension.colors!(context); 7 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 8 | }); 9 | 10 | it("should generate exportable file", () => { 11 | const code = extension.exportColors!(context); 12 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 13 | }); 14 | }); 15 | 16 | 17 | describe("Text Styles", () => { 18 | it("should generate code snippet", () => { 19 | const code = extension.textStyles!(context); 20 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 21 | }); 22 | 23 | it("should generate exportable file", () => { 24 | const code = extension.exportTextStyles!(context); 25 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 26 | }); 27 | }); 28 | 29 | 30 | describe("Spacing", () => { 31 | it("should generate code snippet", () => { 32 | const code = extension.spacing!(context); 33 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 34 | }); 35 | 36 | it("should generate exportable file", () => { 37 | const code = extension.exportSpacing!(context); 38 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 39 | }); 40 | }); 41 | 42 | 43 | version.layers!.map(layer => { 44 | describe(`Layer \`${layer.name}\``, () => { 45 | it("should generate code snippet", async () => { 46 | const code = extension.layer!(context, layer, version); 47 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 48 | }); 49 | }); 50 | }); 51 | 52 | 53 | screens.map(screen => { 54 | describe(`Screen \`${screen.name}\``, () => { 55 | it("should generate code snippet", async () => { 56 | const code = extension.screen!(context, version, screen); 57 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 58 | }); 59 | }); 60 | }); 61 | 62 | 63 | components.map(component => { 64 | describe(`Component \`${component.name}\``, () => { 65 | it("should generate code snippet", async () => { 66 | const code = extension.component!(context, component.latestVersion!, component); 67 | return expect(Promise.resolve(code)).resolves.toMatchSnapshot(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/config/webpack.common.mjs: -------------------------------------------------------------------------------- 1 | import ESLintPlugin from "eslint-webpack-plugin"; 2 | import CopyWebpackPlugin from "copy-webpack-plugin"; 3 | import ManifestBuilder from "../utils/webpack/manifest-builder.js"; 4 | import { resolveBuildPath, resolveExtensionPath } from "../utils/paths.js"; 5 | import { constants } from "./constants.js"; 6 | import { getPackageJson } from "../utils/package.js"; 7 | import { findESLintConfig } from "../utils/eslint.js"; 8 | 9 | const extensionPath = resolveExtensionPath(); 10 | const buildPath = resolveBuildPath(); 11 | const readmePath = resolveExtensionPath("README.md"); 12 | 13 | const { main, exports: _exports } = getPackageJson() || {}; 14 | 15 | const entryPoint = (_exports?.["."]?.import ?? _exports?.["."] ?? _exports ?? main) || "./src/index.js"; 16 | 17 | const eslintConfigFile = findESLintConfig(entryPoint) || findESLintConfig(); 18 | 19 | export default { 20 | mode: "none", 21 | entry: { [constants.bundleName]: entryPoint }, 22 | output: { 23 | path: buildPath, 24 | library: { 25 | name: "extension", 26 | type: "umd", 27 | export: "default" 28 | }, 29 | globalObject: "typeof self !== 'undefined' ? self : this" 30 | }, 31 | module: { 32 | rules: [{ 33 | test: /\.(?:js|mjs|cjs)$/, 34 | exclude: /node_modules/, 35 | use: [{ 36 | loader: "babel-loader", 37 | options: { 38 | presets: [ 39 | [ 40 | "@babel/preset-env", 41 | { 42 | useBuiltIns: "usage", 43 | corejs: 3, 44 | modules: false, // Should be false to run tree shaking. See: https://webpack.js.org/guides/tree-shaking/ 45 | targets: { 46 | chrome: 62, 47 | safari: 11, 48 | firefox: 59, 49 | edge: 15 50 | } 51 | } 52 | ] 53 | ] 54 | } 55 | }] 56 | }] 57 | }, 58 | plugins: [ 59 | ...(eslintConfigFile ? [new ESLintPlugin({ 60 | overrideConfigFile: eslintConfigFile ?? undefined 61 | })] : []), 62 | new CopyWebpackPlugin({ 63 | patterns: [ 64 | { from: readmePath, to: "README.md" }, 65 | { from: "src/**" } 66 | ] 67 | }), 68 | new ManifestBuilder(extensionPath, constants.bundleName) 69 | ] 70 | }; 71 | -------------------------------------------------------------------------------- /src/commands/publish/authenticationService.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import jwt from "jsonwebtoken"; 3 | import prompts from "prompts"; 4 | import { getRcFilePath } from "../../utils/paths.js"; 5 | import { constants } from "../../config/constants.js"; 6 | import { generateAuthToken, login } from "./apiClient.js"; 7 | 8 | const { accessToken, isCI } = constants; 9 | const EXIT_CODE_FOR_SIGTERM = 130; 10 | 11 | function readToken() { 12 | const rcFilePath = getRcFilePath(); 13 | 14 | if (fs.existsSync(rcFilePath)) { 15 | const rcFile = fs.readJSONSync(rcFilePath, 'utf-8'); 16 | 17 | return rcFile.token; 18 | } 19 | } 20 | 21 | function updateRCFile(token) { 22 | const rcFilePath = getRcFilePath(); 23 | const rcContent = fs.existsSync(rcFilePath) ? fs.readJSONSync(rcFilePath, 'utf-8') : {}; 24 | 25 | rcContent.token = token; 26 | fs.writeFileSync(rcFilePath, JSON.stringify(rcContent)); 27 | } 28 | 29 | function getAuthInfo(authToken) { 30 | if (!authToken) { 31 | throw new Error('Authentication token is not provided'); 32 | } 33 | 34 | const { aud } = jwt.decode(authToken, { complete: false }); 35 | if (!aud) { 36 | throw new Error('Invalid authentication token'); 37 | } 38 | 39 | const [, userId] = aud.split(':'); 40 | if (!userId) { 41 | throw new Error('Invalid authentication token'); 42 | } 43 | 44 | return { 45 | authToken, 46 | userId, 47 | }; 48 | } 49 | 50 | export default class AuthenticationService { 51 | authenticate() { 52 | if (isCI || accessToken) { 53 | return getAuthInfo(accessToken); 54 | } 55 | 56 | const tokenFromFile = readToken(); 57 | if (tokenFromFile) { 58 | return getAuthInfo(tokenFromFile); 59 | } 60 | return this.login(); 61 | } 62 | 63 | async login() { 64 | const { handle, password } = await prompts([ 65 | { 66 | type: 'text', 67 | name: 'handle', 68 | message: 'Username or email address: ', 69 | validate: value => Boolean(value && value.length) || 'Username or email address must not be empty', 70 | }, 71 | { 72 | type: 'password', 73 | name: 'password', 74 | message: 'Password', 75 | validate: value => Boolean(value && value.length) || 'Password must not be empty', 76 | }, 77 | ]); 78 | 79 | if (!password) { 80 | process.exit(EXIT_CODE_FOR_SIGTERM); 81 | } 82 | 83 | const token = await login({ handle, password }); 84 | const authToken = await generateAuthToken(token); 85 | 86 | const authInfo = getAuthInfo(authToken); 87 | 88 | await updateRCFile(authToken); 89 | 90 | return authInfo; 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /src/utils/highlight-syntax/colorize.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | function lightTheme(type) { 4 | switch (type) { 5 | case "comment": 6 | case "prolog": 7 | case "doctype": 8 | case "cdata": 9 | return [112, 128, 144]; 10 | case "punctuation": 11 | return [153, 153, 153]; 12 | case "property": 13 | case "tag": 14 | case "boolean": 15 | case "number": 16 | case "constant": 17 | case "symbol": 18 | case "deleted": 19 | return [153, 0, 85]; 20 | case "selector": 21 | case "attr-name": 22 | case "string": 23 | case "char": 24 | case "builtin": 25 | case "inserted": 26 | return [102, 153, 0]; 27 | case "operator": 28 | case "entity": 29 | case "url": 30 | return [166, 127, 89]; 31 | case "atrule": 32 | case "attr-value": 33 | case "keyword": 34 | return [0, 119, 170]; 35 | case "function": 36 | return [221, 74, 104]; 37 | case "regex": 38 | case "important": 39 | case "variable": 40 | return [238, 153, 0]; 41 | } 42 | } 43 | 44 | function darkTheme(type) { 45 | switch (type) { 46 | case "comment": 47 | case "prolog": 48 | case "doctype": 49 | case "cdata": 50 | return [153, 127, 102]; 51 | case "punctuation": 52 | return [153, 153, 153]; 53 | case "property": 54 | case "tag": 55 | case "boolean": 56 | case "number": 57 | case "constant": 58 | case "symbol": 59 | return [209, 147, 158]; 60 | case "selector": 61 | case "attr-name": 62 | case "string": 63 | case "char": 64 | case "builtin": 65 | case "inserted": 66 | return [188, 224, 81]; 67 | case "operator": 68 | case "entity": 69 | case "url": 70 | return [244, 183, 61]; 71 | case "atrule": 72 | case "attr-value": 73 | case "keyword": 74 | return [209, 147, 158]; 75 | case "function": 76 | return [221, 74, 104]; 77 | case "regex": 78 | case "important": 79 | case "variable": 80 | return [238, 153, 0]; 81 | case "deleted": 82 | return [255, 0, 0]; 83 | } 84 | } 85 | 86 | function tokenColor(type, light) { 87 | const theme = light ? lightTheme : darkTheme; 88 | 89 | return theme(type); 90 | } 91 | 92 | export default function colorize(token, type, light = false) { 93 | const color = tokenColor(type, light); 94 | 95 | if (!color) { 96 | return token; 97 | } 98 | 99 | return chalk.rgb(...color)(token); 100 | }; -------------------------------------------------------------------------------- /src/utils/webpack/manifest-builder.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import path from "node:path"; 3 | import webpack from "webpack"; 4 | 5 | const { Compilation, sources } = webpack; 6 | 7 | function parseShortcutRepoUrl(shortcutUrl) { 8 | if (shortcutUrl.startsWith("http")) { 9 | return shortcutUrl; 10 | } 11 | 12 | const match = shortcutUrl.match(/(?:([a-z]+):)?(.*)/i); 13 | 14 | if (!match) { 15 | return; 16 | } 17 | 18 | const [type, id] = match.slice(1); 19 | 20 | switch (type) { 21 | case "gist": 22 | return `https://gist.github.com/${id}`; 23 | case "bitbucket": 24 | return `https://bitbucket.org/${id}`; 25 | case "gitlab": 26 | return `https://gitlab.com/${id}`; 27 | case "github": 28 | return `https://github.com/${id}`; 29 | default: 30 | if (/[^/]+\/[^/]+/.test(id)) { 31 | return `https://github.com/${id}`; 32 | } 33 | } 34 | } 35 | 36 | function parseRepository(repoInfo) { 37 | if (typeof repoInfo === "string") { 38 | return parseShortcutRepoUrl(repoInfo); 39 | } 40 | 41 | return repoInfo.url; 42 | } 43 | 44 | export default class ManifestBuilder { 45 | constructor(extensionPath, bundleName) { 46 | this.extensionPath = extensionPath; 47 | this.bundleName = bundleName; 48 | } 49 | 50 | apply(compiler) { 51 | compiler.hooks.compilation.tap(this.constructor.name, compilation => { 52 | compilation.hooks.processAssets.tap({ 53 | name: this.constructor.name, 54 | stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL 55 | }, () => { 56 | const chunk = Array.from(compilation.chunks).find(c => c.name === this.bundleName); 57 | 58 | if (!chunk) { 59 | return; 60 | } 61 | 62 | const manifest = {}; 63 | const pkgInfoPath = path.join(this.extensionPath, "package.json"); 64 | 65 | // Invalidate cached package.json content 66 | // delete require.cache[require.resolve(pkgInfoPath)]; 67 | const pkgInfo = fs.readJSONSync(pkgInfoPath); 68 | 69 | manifest.packageName = pkgInfo.name; 70 | manifest.name = pkgInfo.zeplin && pkgInfo.zeplin.displayName || pkgInfo.name; 71 | manifest.description = pkgInfo.description; 72 | manifest.version = pkgInfo.version; 73 | manifest.author = pkgInfo.author; 74 | manifest.options = pkgInfo.zeplin && pkgInfo.zeplin.options; 75 | manifest.platforms = pkgInfo.zeplin && (pkgInfo.zeplin.platforms || pkgInfo.zeplin.projectTypes); 76 | manifest.moduleURL = `./${chunk.files.keys().next().value}`; 77 | 78 | if (pkgInfo.repository) { 79 | manifest.repository = parseRepository(pkgInfo.repository); 80 | } 81 | 82 | if (fs.existsSync(path.join(this.extensionPath, "README.md"))) { 83 | manifest.readmeURL = "./README.md"; 84 | } 85 | 86 | const content = JSON.stringify(manifest, null, 2); 87 | 88 | compilation.emitAsset("manifest.json", new sources.RawSource(content)); 89 | }); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/sample-data/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sample extension project", 3 | "type": "ios", 4 | "density": "1x", 5 | "remPreferences": { 6 | "rootFontSize": 16, 7 | "useForMeasurements": true, 8 | "useForFontSizes": true 9 | }, 10 | "colors": [ 11 | { 12 | "r": 255, 13 | "g": 0, 14 | "b": 0, 15 | "a": 1, 16 | "name": "red", 17 | "sourceId": "655bd9de-1c8d-4ca3-9356-bc244a2b697e" 18 | }, 19 | { 20 | "r": 0, 21 | "g": 255, 22 | "b": 0, 23 | "a": 1, 24 | "name": "green", 25 | "sourceId": "7db136dd-7e06-43b8-8c44-13b1f7425e6c" 26 | }, 27 | { 28 | "r": 0, 29 | "g": 0, 30 | "b": 255, 31 | "a": 1, 32 | "name": "blue", 33 | "sourceId": "cc0496f7-fdab-4996-9ff7-f43cc269c4f6" 34 | }, 35 | { 36 | "r": 0, 37 | "g": 0, 38 | "b": 255, 39 | "a": 1, 40 | "name": "background color", 41 | "sourceId": "e719f81a-9ded-4ff1-84f6-e5d0b8cd950c" 42 | }, 43 | { 44 | "r": 255, 45 | "g": 255, 46 | "b": 0, 47 | "a": 1, 48 | "name": "yellow", 49 | "sourceId": "375584e6-2dff-4cc3-8d07-dc61e3836c6c" 50 | }, 51 | { 52 | "r": 0, 53 | "g": 0, 54 | "b": 0, 55 | "a": 1, 56 | "name": "black", 57 | "sourceId": "b3be79a3-c886-44de-b3e0-fe5f25684aa8" 58 | }, 59 | { 60 | "r": 0, 61 | "g": 0, 62 | "b": 0, 63 | "a": 0.5, 64 | "name": "black50", 65 | "sourceId": "64243cac-3cb3-4c09-90dd-e8a33ee27303" 66 | }, 67 | { 68 | "r": 255, 69 | "g": 255, 70 | "b": 255, 71 | "a": 1, 72 | "name": "white", 73 | "sourceId": "e5a911ed-3626-48ac-8c16-523b0edf0d42" 74 | } 75 | ], 76 | "textStyles": [ 77 | { 78 | "name": "Sample text style", 79 | "fontFace": "SFProText-Regular", 80 | "fontSize": 20, 81 | "fontWeight": 400, 82 | "fontStyle": "normal", 83 | "fontFamily": "SFProText", 84 | "fontStretch": "normal", 85 | "textAlign": "left", 86 | "weightText": "regular", 87 | "color": { 88 | "r": 0, 89 | "g": 0, 90 | "b": 0, 91 | "a": 1 92 | } 93 | }, 94 | { 95 | "name": "Sample text style with color", 96 | "fontFace": "SFProText-Regular", 97 | "fontSize": 20, 98 | "fontWeight": 400, 99 | "fontStyle": "normal", 100 | "fontFamily": "SFProText", 101 | "fontStretch": "normal", 102 | "textAlign": "left", 103 | "weightText": "regular", 104 | "color": { 105 | "r": 255, 106 | "g": 0, 107 | "b": 0, 108 | "a": 1 109 | } 110 | } 111 | ], 112 | "spacingSections": [ 113 | { 114 | "name": "Simple spacing", 115 | "description": "", 116 | "baseTokenId": 1, 117 | "spacingTokens": [ 118 | { 119 | "token": "base", 120 | "color": { "r": 127, "b": 127, "g": 127, "a": 1 }, 121 | "value": 16, 122 | "_id": 1 123 | }, 124 | { 125 | "token": "double", 126 | "color": { "r": 255, "b": 255, "g": 255, "a": 1 }, 127 | "value": 32, 128 | "_id": 2 129 | }, 130 | { 131 | "token": "half", 132 | "color": { "r": 63, "b": 63, "g": 63, "a": 1 }, 133 | "value": 8, 134 | "_id": 3 135 | } 136 | ] 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /src/template/tests/fixtures/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sample extension project", 3 | "type": "ios", 4 | "density": "1x", 5 | "remPreferences": { 6 | "rootFontSize": 16, 7 | "useForMeasurements": true, 8 | "useForFontSizes": true 9 | }, 10 | "colors": [ 11 | { 12 | "r": 255, 13 | "g": 0, 14 | "b": 0, 15 | "a": 1, 16 | "name": "red", 17 | "sourceId": "655bd9de-1c8d-4ca3-9356-bc244a2b697e" 18 | }, 19 | { 20 | "r": 0, 21 | "g": 255, 22 | "b": 0, 23 | "a": 1, 24 | "name": "green", 25 | "sourceId": "7db136dd-7e06-43b8-8c44-13b1f7425e6c" 26 | }, 27 | { 28 | "r": 0, 29 | "g": 0, 30 | "b": 255, 31 | "a": 1, 32 | "name": "blue", 33 | "sourceId": "cc0496f7-fdab-4996-9ff7-f43cc269c4f6" 34 | }, 35 | { 36 | "r": 0, 37 | "g": 0, 38 | "b": 255, 39 | "a": 1, 40 | "name": "background color", 41 | "sourceId": "e719f81a-9ded-4ff1-84f6-e5d0b8cd950c" 42 | }, 43 | { 44 | "r": 255, 45 | "g": 255, 46 | "b": 0, 47 | "a": 1, 48 | "name": "yellow", 49 | "sourceId": "375584e6-2dff-4cc3-8d07-dc61e3836c6c" 50 | }, 51 | { 52 | "r": 0, 53 | "g": 0, 54 | "b": 0, 55 | "a": 1, 56 | "name": "black", 57 | "sourceId": "b3be79a3-c886-44de-b3e0-fe5f25684aa8" 58 | }, 59 | { 60 | "r": 0, 61 | "g": 0, 62 | "b": 0, 63 | "a": 0.5, 64 | "name": "black50", 65 | "sourceId": "64243cac-3cb3-4c09-90dd-e8a33ee27303" 66 | }, 67 | { 68 | "r": 255, 69 | "g": 255, 70 | "b": 255, 71 | "a": 1, 72 | "name": "white", 73 | "sourceId": "e5a911ed-3626-48ac-8c16-523b0edf0d42" 74 | } 75 | ], 76 | "textStyles": [ 77 | { 78 | "name": "Sample text style", 79 | "fontFace": "SFProText-Regular", 80 | "fontSize": 20, 81 | "fontWeight": 400, 82 | "fontStyle": "normal", 83 | "fontFamily": "SFProText", 84 | "fontStretch": "normal", 85 | "textAlign": "left", 86 | "weightText": "regular", 87 | "color": { 88 | "r": 0, 89 | "g": 0, 90 | "b": 0, 91 | "a": 1 92 | } 93 | }, 94 | { 95 | "name": "Sample text style with color", 96 | "fontFace": "SFProText-Regular", 97 | "fontSize": 20, 98 | "fontWeight": 400, 99 | "fontStyle": "normal", 100 | "fontFamily": "SFProText", 101 | "fontStretch": "normal", 102 | "textAlign": "left", 103 | "weightText": "regular", 104 | "color": { 105 | "r": 255, 106 | "g": 0, 107 | "b": 0, 108 | "a": 1 109 | } 110 | } 111 | ], 112 | "spacingSections": [ 113 | { 114 | "name": "Simple spacing", 115 | "description": "", 116 | "baseTokenId": 1, 117 | "spacingTokens": [ 118 | { 119 | "token": "base", 120 | "color": { "r": 127, "b": 127, "g": 127, "a": 1 }, 121 | "value": 16, 122 | "_id": 1 123 | }, 124 | { 125 | "token": "double", 126 | "color": { "r": 255, "b": 255, "g": 255, "a": 1 }, 127 | "value": 32, 128 | "_id": 2 129 | }, 130 | { 131 | "token": "half", 132 | "color": { "r": 63, "b": 63, "g": 63, "a": 1 }, 133 | "value": 8, 134 | "_id": 3 135 | } 136 | ] 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zeplin Extension Manager 2 | 3 | Create, test and publish Zeplin extensions with no build configuration. ⚗️🦄 4 | 5 | ## Getting started 6 | 7 | You can run Zeplin Extension Manager directly to create an extension: 8 | 9 | ```sh 10 | npx zem create my-extension 11 | ``` 12 | 13 | You can also use `-y` option to create package with default configuration. 14 | 15 | ```sh 16 | npx zem create my-extension -y 17 | ``` 18 | 19 | ## Overview 20 | 21 | Extensions created using the manager have built-in scripts to ease development, build, test and publish processes. No need to setup tools like Webpack or Babel—they are preconfigured and hidden by the manager. 22 | 23 | ### Scripts 24 | 25 | #### `npm start` 26 | 27 | Starts a local server, serving the extension (by default, at http://127.0.0.1:7070). Hostname, port and the list of hosts allowed to access the local server can be provided as options. 28 | 29 | Follow the [tutorial](https://zeplin.github.io/extension-model/documents/Tutorial.html) to learn how to add a local extension to a Zeplin project. 30 | 31 | ``` 32 | Usage: npm start -- [options] 33 | 34 | Options: 35 | 36 | -h --host Host name (default: localhost) 37 | -p --port Port (default: 7070) 38 | -a --allowed-hosts Allowed hosts 39 | ``` 40 | 41 | #### `npm run build` 42 | 43 | Builds extension source, creating resources targeting production environment. 44 | 45 | ``` 46 | Usage: npm run build -- [options] 47 | 48 | Options: 49 | 50 | -d --dev Target development environment 51 | ``` 52 | 53 | #### `npm run exec` 54 | 55 | Executes extension function(s) with sample data. 56 | 57 | This is a super useful script to debug and test your extension, without running in it Zeplin. 58 | 59 | ``` 60 | Usage: npm run exec -- [function-name] [options] 61 | 62 | Options: 63 | 64 | --no-build Use existing build. 65 | --defaults Set default extension option values (e.g, flag=false,prefix=\"pre\") 66 | ``` 67 | 68 | #### `npm test` 69 | 70 | Runs test scripts via Jest. Extension packages created using zem include a boilerplate test module. It uses Jest's snapshot testing feature to match the output of your extensions with the expected results. For example, you can take a look at our [React Native extension](https://github.com/zeplin/react-native-extension/blob/develop/src/index.test.js). 71 | 72 | ``` 73 | Usage: npm test -- [options] 74 | ``` 75 | 76 | You can check [Jest's docs](https://jestjs.io/docs/en/cli.html) for options. 77 | 78 | #### `npm run clean` 79 | 80 | Cleans build directory. 81 | 82 | 83 | #### `npm run publish` 84 | 85 | Publish extension, sending it for review to be listed on [extensions.zeplin.io](https://extensions.zeplin.io). 86 | 87 | ``` 88 | Usage: npm run publish -- [options] 89 | 90 | Options: 91 | 92 | --path Path for the extension build to publish (default: Path used by the build command) 93 | ``` 94 | 95 | 96 | ##### Usage with access token: 97 | 98 | Zeplin Extension Manager can authenticate using an access token instead of your Zeplin credentials which makes it easier to integrate it into your CI workflow. 99 | 100 | 1. Get a `zem` access token from your [Profile](https://app.zeplin.io/profile/connected-apps) in Zeplin. 101 | 2. Set `ZEM_ACCESS_TOKEN` environment variable in your CI. 102 | 103 | ## Tidbits 104 | 105 | - Modules are transpiled to target Safari 9.1, as extensions are run both on the Web app and on the Mac app using JavaScriptCore, supporting macOS El Capitan. 106 | - Add an ESLint configuration and the source code will automatically be linted before building. 107 | - You can create `webpack.zem.js` at your root to customize webpack config. The module should export a function 108 | that takes current webpack config as an argument and return customized webpack config. For example: 109 | 110 | ```javascript 111 | module.exports = function({ module: { rules, ...module }, ...webpackConfig }) { 112 | return { 113 | ...webpackConfig, 114 | 115 | resolve: { 116 | extensions: [".ts"] 117 | }, 118 | module: { 119 | ...module, 120 | rules: [ 121 | { 122 | test: /\.tsx?$/, 123 | use: "ts-loader", 124 | exclude: /node_modules/, 125 | }, 126 | ...rules, 127 | ], 128 | }, 129 | }; 130 | }; 131 | ``` 132 | 133 | ## Community solutions 134 | 135 | ### Zero 136 | 137 | [baybara-pavel/zero](https://github.com/baybara-pavel/zero) 138 | 139 | Similar to zem, Zero lets you quickly start working on a Zeplin extension with Webpack. 140 | -------------------------------------------------------------------------------- /src/commands/publish/manifest-validator.js: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | import addFormats from "ajv-formats" 3 | 4 | const ajv = new Ajv({ allErrors: true }); 5 | addFormats(ajv); 6 | 7 | const pickerOptionSchema = { 8 | type: "object", 9 | required: ["name", "value"], 10 | properties: { 11 | name: { type: "string" }, 12 | value: { type: "string" } 13 | } 14 | }; 15 | 16 | const pickerSchema = { 17 | type: "object", 18 | required: ["name", "type", "id", "default"], 19 | properties: { 20 | name: { type: "string" }, 21 | type: { const: "picker" }, 22 | id: { type: "string" }, 23 | default: { type: "string" }, 24 | options: { type: "array", items: pickerOptionSchema } 25 | } 26 | }; 27 | 28 | const switchSchema = { 29 | type: "object", 30 | required: ["name", "type", "id", "default"], 31 | properties: { 32 | name: { type: "string" }, 33 | type: { const: "switch" }, 34 | id: { type: "string" }, 35 | default: { type: "boolean" } 36 | } 37 | }; 38 | 39 | const textOptionSchema = { 40 | type: "object", 41 | required: ["name", "type", "id", "default"], 42 | properties: { 43 | name: { type: "string" }, 44 | type: { const: "text" }, 45 | id: { type: "string" }, 46 | default: { type: "string" } 47 | } 48 | }; 49 | 50 | const optionsSchema = { 51 | type: "array", 52 | items: { 53 | anyOf: [ 54 | pickerSchema, 55 | switchSchema, 56 | textOptionSchema 57 | ] 58 | } 59 | }; 60 | 61 | // Credit to Diego Perini https://gist.github.com/dperini/729294 62 | const urlPattern = "^" + 63 | // Protocol identifier (optional) 64 | // Short syntax // still required 65 | "(?:(?:(?:https?|ftp):)?\\/\\/)" + 66 | // User:pass BasicAuth (optional) 67 | "(?:\\S+(?::\\S*)?@)?" + 68 | "(?:" + 69 | // IP address exclusion 70 | // Private & local networks 71 | "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + 72 | "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + 73 | "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + 74 | // IP address dotted notation octets 75 | // Excludes loopback network 0.0.0.0 76 | // Excludes reserved space >= 224.0.0.0 77 | // Excludes network & broadcast addresses 78 | // (first & last IP address of each class) 79 | "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + 80 | "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + 81 | "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + 82 | "|" + 83 | // Host & domain names, may end with dot 84 | // Can be replaced by a shortest alternative 85 | // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ 86 | "(?:" + 87 | "(?:" + 88 | "[a-z0-9\\u00a1-\\uffff]" + 89 | "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + 90 | ")?" + 91 | "[a-z0-9\\u00a1-\\uffff]\\." + 92 | ")+" + 93 | // TLD identifier name, may end with dot 94 | "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + 95 | ")" + 96 | // Port number (optional) 97 | "(?::\\d{2,5})?" + 98 | // Resource path (optional) 99 | "(?:[/?#]\\S*)?" + 100 | "$"; 101 | 102 | const manifestSchema = { 103 | type: "object", 104 | required: ["packageName", "name", "description", "version", "moduleURL", "platforms"], 105 | properties: { 106 | packageName: { type: "string" }, 107 | name: { type: "string" }, 108 | description: { type: "string" }, 109 | version: { type: "string", pattern: "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" }, 110 | moduleURL: { type: "string" }, 111 | readmeURL: { type: "string" }, 112 | author: { 113 | type: "object", 114 | properties: { 115 | name: { type: "string" }, 116 | email: { type: "string", format: "email" }, 117 | url: { type: "string", pattern: urlPattern } 118 | } 119 | }, 120 | platforms: { 121 | type: "array", 122 | items: { 123 | enum: ["web", "android", "ios", "osx"] 124 | }, 125 | uniqueItems: true, 126 | minItems: 1 127 | }, 128 | options: optionsSchema 129 | } 130 | }; 131 | 132 | const validate = ajv.compile(manifestSchema); 133 | 134 | export default function (manifestObj) { 135 | const valid = validate(manifestObj); 136 | 137 | if (!valid) { 138 | return { 139 | valid, 140 | errors: ajv.errorsText(validate.errors, { dataVar: "manifest", separator: "\n" }) 141 | }; 142 | } 143 | 144 | return { valid }; 145 | }; 146 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs-extra"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import chalk from "chalk"; 6 | import updateNotifier from "update-notifier"; 7 | import { Command } from "commander"; 8 | import { resolveBuildPath } from "./utils/paths.js"; 9 | import build from "./commands/build.js"; 10 | import start from "./commands/start.js"; 11 | import exec from "./commands/exec.js"; 12 | import publish from "./commands/publish/index.js"; 13 | import create from "./commands/create.js"; 14 | import test from "./commands/test.js"; 15 | import execWebpackConfig from "./config/webpack.exec.mjs"; 16 | import prodWebpackConfig from "./config/webpack.prod.mjs"; 17 | import devWebpackConfig from "./config/webpack.dev.mjs"; 18 | 19 | const __filename = fileURLToPath(import.meta.url); 20 | const __dirname = path.dirname(__filename); 21 | 22 | const { name, version } = fs.readJsonSync(`${__dirname}/../package.json`) 23 | 24 | const seconds = 60; 25 | const minutes = 60; 26 | const hours = 24; 27 | const days = 5; 28 | const updateCheckInterval = days * hours * minutes * seconds; 29 | 30 | function beforeCommand() { 31 | const notifier = updateNotifier({ 32 | pkg: { name, version }, 33 | shouldNotifyInNpmScript: true, 34 | updateCheckInterval 35 | }); 36 | notifier.notify(); 37 | } 38 | 39 | const program = new Command(name).version(version); 40 | const TEST_ARGS_INDEX = 3; 41 | 42 | program 43 | .command("create ") 44 | .description("Create empty Zeplin extension at directory.") 45 | .option("-y --yes", "Create extension without prompt for configuration") 46 | .action((extensionDir, { yes }) => { 47 | const root = path.resolve(process.cwd(), extensionDir); 48 | 49 | return create({ root, disablePrompt: yes }); 50 | }); 51 | 52 | program 53 | .command("build") 54 | .description("Create build, targeting production environment.") 55 | .option("-d --dev", "Target development environment") 56 | .action(async options => { 57 | await build(options.dev ? devWebpackConfig : prodWebpackConfig); 58 | }); 59 | 60 | program 61 | .command("clean") 62 | .description("Clean build directory.") 63 | .action(() => { 64 | fs.removeSync(resolveBuildPath()); 65 | }); 66 | 67 | program 68 | .command("start") 69 | .description("Start local server, serving the extension.") 70 | .option("-h --host ", "Host name (Default: \"127.0.0.1\")") 71 | .option("-p --port ", "Port (Default: 7070)") 72 | .option("-a --allowed-hosts ", "Allowed hosts, comma-separated (e.g., localhost,127.0.0.1,example.com,*.example.com) (Default: \"all\")") 73 | .action(async command => { 74 | await start(command.host, command.port, command.allowedHosts); 75 | }); 76 | 77 | program 78 | .command("exec [function-name]") 79 | .description("Execute extension function with sample data.") 80 | .option("--no-build", "Use existing build.") 81 | .option("--defaults ", `Set default extension option values (e.g, flag=false,prefix=\\"pre\\")`) 82 | .action((fnName, options) => { 83 | let defaultOptions; 84 | 85 | if (options.defaults) { 86 | defaultOptions = {}; 87 | 88 | options.defaults.split(",").forEach(keyValue => { 89 | const [key, value] = keyValue.split("="); 90 | 91 | defaultOptions[key] = JSON.parse(value); 92 | }); 93 | } 94 | 95 | exec(execWebpackConfig, fnName, defaultOptions, options.build); 96 | }); 97 | 98 | program 99 | .command("publish") 100 | .description(`Publish extension, submitting it for review to be listed on ${chalk.underline("https://extensions.zeplin.io.")}`) 101 | .option("--path ", `Path for the extension build to be published`) 102 | .option("--verbose", "Enables verbose logs") 103 | .action(async ({ path: buildPath, verbose }) => { 104 | try { 105 | await publish({ buildPath, verbose }); 106 | } catch (_) { 107 | process.exitCode = 1; 108 | } 109 | }); 110 | 111 | program 112 | .command("test") 113 | .description(`Test via jest`) 114 | .allowUnknownOption() 115 | .action(async () => { 116 | const testSuccess = await test(process.argv.slice(TEST_ARGS_INDEX)); 117 | if (!testSuccess) { 118 | console.log("Tests failed."); 119 | process.exitCode = 1; 120 | } 121 | }); 122 | 123 | program.on("command:*", () => { 124 | program.outputHelp(); 125 | }); 126 | 127 | beforeCommand(); 128 | program.parse(process.argv); 129 | -------------------------------------------------------------------------------- /src/commands/publish/apiClient.js: -------------------------------------------------------------------------------- 1 | import { URL } from "node:url"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import { FormData, request as undiciRequest } from "undici"; 4 | import qs from "qs"; 5 | import { constants } from "../../config/constants.js"; 6 | import { ClientError, ServerError } from "../../errors/index.js"; 7 | 8 | const { apiBaseUrl, apiClientId } = constants; 9 | 10 | async function createError(response) { 11 | const { statusCode, body, headers } = response; 12 | const responseBody = await body.text(); 13 | const extra = { 14 | response: { 15 | statusCode, 16 | body: responseBody, 17 | headers 18 | } 19 | }; 20 | 21 | let errorMessage; 22 | if (headers["content-type"].startsWith("application/json")) { 23 | const { message, title } = JSON.parse(extra.response.body); 24 | errorMessage = `${title}${message ? `: ${message}` : ""}`; 25 | } 26 | 27 | if (statusCode >= StatusCodes.BAD_REQUEST && statusCode < StatusCodes.INTERNAL_SERVER_ERROR) { 28 | return new ClientError(statusCode, extra, errorMessage); 29 | } 30 | 31 | if (statusCode >= StatusCodes.INTERNAL_SERVER_ERROR) { 32 | return new ServerError(statusCode, extra, errorMessage); 33 | } 34 | 35 | return new Error("Zeplin API error"); 36 | } 37 | 38 | function getUrl(path) { 39 | return `${apiBaseUrl}${path}`; 40 | } 41 | 42 | async function request(path, opts) { 43 | const response = await undiciRequest(getUrl(path), opts); 44 | 45 | if (response.statusCode >= StatusCodes.BAD_REQUEST) { 46 | throw await createError(response); 47 | } 48 | 49 | const body = response.headers["content-type"].startsWith("application/json") 50 | ? await response.body.json() 51 | : await response.body.text(); 52 | 53 | return { 54 | body, 55 | statusCode: response.statusCode, 56 | headers: response.headers 57 | }; 58 | } 59 | 60 | 61 | export const login = async ({ handle, password }) => { 62 | const { body: { token } } = await request("/users/login", { 63 | method: "POST", 64 | body: JSON.stringify({ 65 | handle, 66 | password 67 | }), 68 | headers: { "Content-Type": "application/json" } 69 | }); 70 | 71 | return token; 72 | }; 73 | 74 | export const generateAuthToken = async loginToken => { 75 | const queryString = qs.stringify({ 76 | client_id: apiClientId, 77 | response_type: "token", 78 | scope: "write" 79 | }); 80 | const response = await request(`/oauth/authorize?${queryString}`, { 81 | method: "GET", 82 | headers: { 83 | "Zeplin-Token": loginToken 84 | }, 85 | maxRedirections: 0 86 | }); 87 | 88 | const { headers: { location }, statusCode } = response; 89 | 90 | if (statusCode !== StatusCodes.MOVED_TEMPORARILY) { 91 | throw await createError(response); 92 | } 93 | 94 | const { searchParams } = new URL(location); 95 | 96 | return searchParams.get("access_token"); 97 | }; 98 | 99 | export const getExtensions = async ({ authToken, owner }) => { 100 | const queryString = qs.stringify({ owner }); 101 | const { body: { extensions } } = await request(`/extensions?${queryString}`, { 102 | method: "GET", 103 | headers: { "Zeplin-Access-Token": authToken } 104 | }); 105 | 106 | return extensions; 107 | }; 108 | 109 | export const createExtension = ({ authToken, data }) => { 110 | const { 111 | packageName, 112 | name, 113 | description, 114 | platforms, 115 | version, 116 | packageBuffer 117 | } = data; 118 | 119 | 120 | const formData = new FormData(); 121 | formData.set("version", version); 122 | formData.set("packageName", packageName); 123 | formData.set("name", name); 124 | formData.set("description", description); 125 | formData.set("projectTypes", platforms); 126 | formData.set("package", new Blob([packageBuffer], { type: "application/zip" }), "package.zip"); 127 | 128 | return request("/extensions", { 129 | method: "POST", 130 | body: formData, 131 | headers: { 132 | "Zeplin-Access-Token": authToken 133 | } 134 | }); 135 | }; 136 | 137 | export const createExtensionVersion = ({ authToken, extensionId, version, packageBuffer }) => { 138 | const formData = new FormData(); 139 | formData.set("version", version); 140 | formData.set("package", new Blob([packageBuffer], { type: "application/zip" }), "package.zip"); 141 | 142 | return request(`/extensions/${extensionId}/versions`, { 143 | method: "POST", 144 | body: formData, 145 | headers: { 146 | "Zeplin-Access-Token": authToken 147 | } 148 | }); 149 | }; 150 | -------------------------------------------------------------------------------- /src/commands/publish/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import chalk from "chalk"; 3 | import path from "path"; 4 | import Zip from "adm-zip"; 5 | import prompts from "prompts"; 6 | import * as paths from "../../utils/paths.js"; 7 | import manifestValidator from "./manifest-validator.js"; 8 | import { constants } from "../../config/constants.js"; 9 | import AuthenticationService from "./authenticationService.js"; 10 | import { createExtension, createExtensionVersion, getExtensions } from "./apiClient.js"; 11 | import { getPackageVersion } from "../../utils/package.js"; 12 | 13 | const pathResolver = { 14 | init(root) { 15 | this.root = root; 16 | }, 17 | resolve(relativePath) { 18 | if (this.root) { 19 | return path.resolve(this.root, relativePath); 20 | } 21 | 22 | return paths.resolveBuildPath(relativePath); 23 | } 24 | }; 25 | 26 | function parseManifest() { 27 | const manifestPath = pathResolver.resolve("./manifest.json"); 28 | 29 | if (!fs.existsSync(manifestPath)) { 30 | throw new Error("Locating manifest.json failed. Please make sure that you run `npm run build` first!"); 31 | } 32 | 33 | const manifest = fs.readJSONSync(manifestPath); 34 | const { valid, errors } = manifestValidator(manifest); 35 | 36 | if (!valid) { 37 | throw new Error(`Validating manifest.json failed:\n${errors}\n\nPlease make sure that all fields in "package.json" are valid and you run \`npm run build\``); 38 | } 39 | 40 | if (getPackageVersion() !== manifest.version) { 41 | throw new Error( 42 | "Validating manifest.json failed: Extension version does not match the version in manifest.json.\n" + 43 | "Please make sure that you run `npm run build` first!" 44 | ); 45 | } 46 | 47 | return manifest; 48 | } 49 | 50 | function createArchive() { 51 | const archive = new Zip(); 52 | 53 | archive.addLocalFolder(pathResolver.resolve("./"), "./"); 54 | 55 | return archive; 56 | } 57 | 58 | async function confirm() { 59 | if (constants.isCI) { 60 | return true; 61 | } 62 | 63 | const { answer } = await prompts({ 64 | type: "confirm", 65 | name: "answer", 66 | message: "Are you sure to continue" 67 | }); 68 | return answer; 69 | } 70 | 71 | async function validateReadme({ hasOptions }) { 72 | const readmePath = pathResolver.resolve("./README.md"); 73 | 74 | if (!fs.existsSync(readmePath)) { 75 | throw new Error("Locating README.md failed. Please make sure that you create a readme file and run `npm run build`!"); 76 | } 77 | 78 | const readme = (await fs.readFile(readmePath)) 79 | .toString("utf8") 80 | .replace(//g, ""); 81 | 82 | if (!readme.match(/^## Output/m)) { 83 | console.log(chalk.yellow("Output section could not be found in README.md")); 84 | if (!await confirm()) { 85 | throw new Error("Output section could not be found in README.md. Please make sure that you add this section and run `npm run build`!"); 86 | } 87 | } 88 | 89 | if (hasOptions && !readme.match(/^## Options/m)) { 90 | console.log(chalk.yellow("Options section could not be found in README.md")); 91 | if (!await confirm()) { 92 | throw new Error("Options section could not be found in README.md. Please make sure that you add this section and run `npm run build`!"); 93 | } 94 | } 95 | } 96 | 97 | export default async function ({ buildPath, verbose }) { 98 | console.log("Publishing the extension...\n"); 99 | 100 | pathResolver.init(buildPath); 101 | 102 | try { 103 | const { 104 | packageName, 105 | version, 106 | name, 107 | description, 108 | platforms, 109 | options 110 | } = parseManifest(); 111 | 112 | await validateReadme({ hasOptions: Boolean(options) }); 113 | 114 | const authenticationService = new AuthenticationService(); 115 | const { authToken, userId } = await authenticationService.authenticate(); 116 | const extensions = await getExtensions({ authToken, owner: userId }); 117 | 118 | const extension = extensions.find(e => e.packageName === packageName); 119 | const packageBuffer = createArchive().toBuffer(); 120 | 121 | if (!extension) { 122 | const data = { 123 | packageName, 124 | version, 125 | name, 126 | description, 127 | platforms: platforms.join(","), 128 | packageBuffer 129 | }; 130 | await createExtension({ data, authToken }); 131 | console.log(`${chalk.bold(name)} (${version}) is now submitted. 🏄‍️\n`); 132 | } else { 133 | await createExtensionVersion({ authToken, extensionId: extension._id, version, packageBuffer }); 134 | console.log(`Version ${chalk.bold(version)} of ${chalk.bold(name)} is now submitted. 🏄‍️\n`); 135 | } 136 | 137 | console.log(`Big hugs for your contribution, you'll be notified via email once it's published on ${chalk.underline("https://extensions.zeplin.io")}.`); 138 | } catch (error) { 139 | console.log(chalk.red("Publishing extension failed:")); 140 | console.error(error.message || error); 141 | if (verbose && error.extra) { 142 | console.error(JSON.stringify(error.extra, null, 2)); 143 | } 144 | throw error; 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /src/commands/create.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs-extra"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import { spawn } from "node:child_process"; 6 | import prompts from "prompts"; 7 | import caseLib from "case"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const { name } = fs.readJSONSync(`${__dirname}/../../package.json`) 13 | 14 | const JSON_INDENT = 2; 15 | const EXIT_CODE_FOR_SIGTERM = 130; 16 | 17 | const installDeps = () => new Promise((resolve, reject) => { 18 | const npmArgs = ["install", "--save", name]; 19 | const child = spawn("npm", npmArgs, { stdio: "inherit", shell: true }); 20 | 21 | child.on("close", exitCode => { 22 | if (exitCode !== 0) { 23 | reject(); 24 | return; 25 | } 26 | 27 | resolve(); 28 | }); 29 | }); 30 | 31 | const getDefaultConfig = packageName => ({ 32 | packageName, 33 | description: "", 34 | displayName: caseLib.title(packageName), 35 | platforms: [] 36 | }); 37 | 38 | const getConfig = async defaultPackageName => { 39 | const response = await prompts([ 40 | { 41 | type: "text", 42 | name: "packageName", 43 | message: "Package name:", 44 | initial: defaultPackageName 45 | }, 46 | { 47 | type: "text", 48 | name: "description", 49 | message: "Description:" 50 | }, 51 | { 52 | type: "text", 53 | name: "displayName", 54 | message: "Display name:", 55 | initial: caseLib.title(defaultPackageName) 56 | }, 57 | { 58 | type: "multiselect", 59 | name: "platforms", 60 | message: "Platforms:", 61 | min: 1, 62 | choices: [ 63 | { name: "Web", value: "web" }, 64 | { name: "Android", value: "android" }, 65 | { name: "iOS", value: "ios" }, 66 | { name: "macOS", value: "osx" } 67 | ], 68 | instructions: false, 69 | hint: `Press ${chalk.blue("←")}/${chalk.blue("→")}/${chalk.green("[space]")} to select choices` 70 | } 71 | ]); 72 | 73 | if (!response.platforms) { 74 | process.exit(EXIT_CODE_FOR_SIGTERM); 75 | } 76 | 77 | return response; 78 | }; 79 | 80 | const createPackageJson = (root, { packageName, description, displayName, platforms }) => { 81 | const packageJson = { 82 | name: packageName, 83 | version: "0.1.0", 84 | description, 85 | type: "module", 86 | exports: "./dist/index.js", 87 | sideEffects: false, 88 | scripts: { 89 | start: "zem start", 90 | clean: "zem clean", 91 | prebuild: "npm run clean", 92 | build: "tsc && zem build", 93 | exec: "npm run build && zem exec", 94 | test: "npm run build && zem test", 95 | publish: "npm run build && zem publish", 96 | lint: "eslint src/ --ext .js,.ts" 97 | }, 98 | dependencies: { 99 | "@zeplin/extension-model": "^3.0.3", 100 | "zem": "^2.0.4" 101 | }, 102 | devDependencies: { 103 | "@eslint/js": "^9.26.0", 104 | "@types/jest": "^29.5.14", 105 | "eslint": "^9.26.0", 106 | "globals": "^16.1.0", 107 | "jest": "^29.7.0", 108 | "ts-jest": "^29.3.4", 109 | "typescript": "^5.8.3", 110 | "typescript-eslint": "^8.34.0" 111 | }, 112 | engines: { 113 | "node": ">=20" 114 | }, 115 | keywords: [ 116 | "zeplin", 117 | "extension", 118 | ], 119 | zeplin: { 120 | displayName, 121 | platforms, 122 | options: [] 123 | } 124 | }; 125 | 126 | return fs.writeFile(path.resolve(root, "package.json"), JSON.stringify(packageJson, null, JSON_INDENT)); 127 | }; 128 | 129 | const generateReadme = async options => { 130 | const readmeTemplate = (await fs.readFile("./README.md")) 131 | .toString("utf8"); 132 | 133 | const overrideOptions = { 134 | description: options.description || "" 135 | }; 136 | 137 | const readme = Object 138 | .entries(Object.assign({}, options, overrideOptions)) 139 | .reduce( 140 | (template, [key, value]) => template.replace(new RegExp(`{{${key}}}`, "g"), value), 141 | readmeTemplate 142 | ); 143 | return fs.writeFile("./README.md", readme); 144 | }; 145 | 146 | const create = async ({ root, disablePrompt }) => { 147 | if (fs.existsSync(root)) { 148 | console.log(chalk.red("Creating extension failed, directory already exists.")); 149 | process.exit(1); 150 | } 151 | 152 | const defaultPackageName = path.basename(root); 153 | 154 | const config = disablePrompt ? getDefaultConfig(defaultPackageName) : await getConfig(defaultPackageName); 155 | 156 | await fs.mkdir(root); 157 | 158 | const templatePath = path.join(__dirname, "../template"); 159 | 160 | await Promise.all([ 161 | createPackageJson(root, config), 162 | fs.copy(templatePath, root) 163 | ]); 164 | 165 | process.chdir(root); 166 | 167 | await Promise.all([ 168 | installDeps(), 169 | generateReadme(config) 170 | ]); 171 | 172 | console.log(`\n✅ Created extension at ${chalk.blue(root)}.\n`); 173 | console.log(`To get started, see documentation at ${chalk.underline("https://github.com/zeplin/zeplin-extension-documentation")}.`); 174 | }; 175 | 176 | export default create; 177 | -------------------------------------------------------------------------------- /src/commands/exec.js: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import chalk from "chalk"; 3 | import build from "./build.js"; 4 | import highlightSyntax from "../utils/highlight-syntax/index.js"; 5 | import sampleData from "../sample-data/index.js"; 6 | import { constants } from "../config/constants.js"; 7 | import { resolveBuildPath } from "../utils/paths.js"; 8 | 9 | import { Component, ComponentVariant, Context, Screen, Version } from "@zeplin/extension-model"; 10 | 11 | function getManifestDefaults() { 12 | const manifest = fs.readJSONSync(resolveBuildPath("manifest.json")); 13 | 14 | if (!manifest.options) { 15 | return {}; 16 | } 17 | 18 | return manifest.options.reduce((defaultOptions, option) => { 19 | defaultOptions[option.id] = option.default; 20 | 21 | return defaultOptions; 22 | }, {}); 23 | } 24 | 25 | function printCodeData(codeData, title) { 26 | if (title) { 27 | console.log(chalk.bold(title)); 28 | } 29 | 30 | if (!codeData) { 31 | return; 32 | } 33 | 34 | let output; 35 | 36 | if (typeof codeData === "string") { 37 | output = codeData; 38 | } else { 39 | const { code, language } = codeData; 40 | 41 | try { 42 | output = highlightSyntax(code, language); 43 | } catch (error) { 44 | output = code; 45 | } 46 | } 47 | 48 | console.log(output); 49 | } 50 | 51 | function callExtensionFunction(extension, fnName, ...args) { 52 | return Promise.resolve() 53 | .then(() => { 54 | if (typeof extension[fnName] !== "function") { 55 | return; 56 | } 57 | 58 | return extension[fnName](...args); 59 | }).catch(error => chalk.red(error.stack)); 60 | } 61 | 62 | function executeScreen(extension, context) { 63 | const version = new Version(sampleData.screenVersion); 64 | const screens = sampleData.screens.map(s => new Screen(s)); 65 | 66 | return Promise.all( 67 | screens.map(screen => callExtensionFunction(extension, "screen", context, version, screen)) 68 | ).then(results => { 69 | console.log(chalk.underline.bold("\nScreens:")); 70 | 71 | results.forEach((codeData, index) => { 72 | printCodeData(codeData, `${screens[index].name}:`); 73 | }); 74 | }); 75 | } 76 | 77 | function executeComponent(extension, context) { 78 | const singleComponents = sampleData.components.map(c => new Component(c)); 79 | const variantComponents = sampleData.componentVariants.map( 80 | variantData => new ComponentVariant(variantData) 81 | ).reduce((cs, variant) => cs.concat(variant.components), []); 82 | const components = singleComponents.concat(variantComponents); 83 | 84 | return Promise.all( 85 | components.map(component => callExtensionFunction(extension, "component", context, component.latestVersion, component)) 86 | ).then(results => { 87 | console.log(chalk.underline.bold("\nComponents:")); 88 | 89 | results.forEach((codeData, index) => { 90 | printCodeData(codeData, `${components[index].name}:`); 91 | }); 92 | }); 93 | } 94 | 95 | function executeLayer(extension, context) { 96 | const version = new Version(sampleData.screenVersion); 97 | 98 | return Promise.all( 99 | version 100 | .layers 101 | .map(layer => callExtensionFunction(extension, "layer", context, layer)) 102 | ).then(results => { 103 | console.log(chalk.underline.bold("\nLayers:")); 104 | 105 | results.forEach((codeData, index) => { 106 | printCodeData(codeData, `${version.layers[index].name}:`); 107 | }); 108 | }); 109 | } 110 | 111 | function executeColors(extension, context) { 112 | return callExtensionFunction(extension, "colors", context).then(codeData => { 113 | console.log(chalk.underline.bold("\nColors (Project):")); 114 | 115 | printCodeData(codeData); 116 | }); 117 | } 118 | 119 | function executeTextStyles(extension, context) { 120 | return callExtensionFunction(extension, "textStyles", context).then(codeData => { 121 | console.log(chalk.underline.bold("\nText styles (Project):")); 122 | 123 | printCodeData(codeData); 124 | }); 125 | } 126 | 127 | function executeSpacing(extension, context) { 128 | return callExtensionFunction(extension, "spacing", context).then(codeData => { 129 | console.log(chalk.underline.bold("\nSpacing (Project):")); 130 | 131 | printCodeData(codeData); 132 | }); 133 | } 134 | 135 | const EXTENSION_FUNCTIONS = { 136 | layer: executeLayer, 137 | colors: executeColors, 138 | textStyles: executeTextStyles, 139 | spacing: executeSpacing, 140 | screen: executeScreen, 141 | component: executeComponent 142 | }; 143 | 144 | function executeFunction(extension, fnName, context) { 145 | const fn = EXTENSION_FUNCTIONS[fnName]; 146 | 147 | if (!fn) { 148 | console.log(chalk.yellow(`Function “${fnName}” not defined.`)); 149 | return; 150 | } 151 | fn(extension, context); 152 | } 153 | 154 | function executeExtension(extension, fnName, defaultOptions = {}) { 155 | const options = Object.assign(getManifestDefaults(), defaultOptions); 156 | const context = new Context({ 157 | options, 158 | project: sampleData.project 159 | }); 160 | 161 | if (fnName) { 162 | executeFunction(extension, fnName, context); 163 | return; 164 | } 165 | 166 | executeFunction(extension, "colors", context); 167 | executeFunction(extension, "textStyles", context); 168 | executeFunction(extension, "spacing", context); 169 | executeFunction(extension, "component", context); 170 | executeFunction(extension, "screen", context); 171 | executeFunction(extension, "layer", context); 172 | } 173 | 174 | export default async function (webpackConfig, fnName, defaultOptions, shouldBuild) { 175 | const extensionModulePath = resolveBuildPath(`${constants.bundleName}.js`); 176 | let moduleBuild; 177 | 178 | if (!shouldBuild && fs.existsSync(extensionModulePath)) { 179 | moduleBuild = Promise.resolve(); 180 | } else { 181 | moduleBuild = build(webpackConfig, { throwOnError: true, printStats: false }); 182 | } 183 | 184 | return moduleBuild.then(async () => { 185 | try { 186 | const extension = (await import(extensionModulePath)).default; 187 | 188 | console.log(`Executing extension${fnName ? ` function ${chalk.blue(fnName)}` : ""} with sample data...`); 189 | 190 | executeExtension(extension, fnName, defaultOptions); 191 | } catch (error) { 192 | console.error(chalk.red("Execution failed:"), error); 193 | } 194 | }).catch(error => { 195 | console.log(chalk.red("Execution failed: cannot build the extension")); 196 | console.log(error) 197 | error.stats.toJson().errors.map(e=> JSON.stringify(e)).forEach(e => console.error(e)); 198 | }); 199 | }; -------------------------------------------------------------------------------- /src/template/tests/fixtures/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "snapshot": { 3 | "backgroundColor": { "r": 255, "b": 255, "g": 255, "a": 1 }, 4 | "source": "sketch", 5 | "height": 960, 6 | "densityScale": 1, 7 | "width": 1024, 8 | "links": [], 9 | "componentNames": [ 10 | "Simple Component", 11 | "Component with description" 12 | ], 13 | "url": "https://placekitten.com/1024/960", 14 | "_id": "5e38085266ecb97b1faaa969", 15 | "density": "1x", 16 | "assets": [], 17 | "layers": [ 18 | { 19 | "type": "shape", 20 | "name": "Sample layer", 21 | "sourceId": "15b2dbb4-3259-4c8e-a8e3-24b292145d21", 22 | "opacity": 1, 23 | "blendMode": "normal", 24 | "borderRadius": 0, 25 | "rect": { 26 | "x": 0, 27 | "y": 0, 28 | "width": 320, 29 | "height": 768 30 | }, 31 | "content": "", 32 | "borders": [], 33 | "fills": [], 34 | "shadows": [], 35 | "textStyles": [], 36 | "assets": [] 37 | }, 38 | { 39 | "type": "text", 40 | "name": "Text layer with multiple styles", 41 | "sourceId": "859f1b9e-0437-4408-b84c-ea6c10c37d8f", 42 | "exportable": false, 43 | "rotation": 0, 44 | "opacity": 1, 45 | "blendMode": "normal", 46 | "borderRadius": 0, 47 | "rect": { 48 | "y": 44, 49 | "x": 0, 50 | "width": 220, 51 | "height": 24 52 | }, 53 | "content": "Type something red", 54 | "borders": [], 55 | "fills": [], 56 | "shadows": [], 57 | "textStyles": [ 58 | { 59 | "range": { 60 | "location": 0, 61 | "length": 4 62 | }, 63 | "style": { 64 | "fontFace": "SFProText-Medium", 65 | "fontSize": 20, 66 | "fontWeight": 500, 67 | "fontStyle": "normal", 68 | "fontFamily": "SFProText", 69 | "fontStretch": "normal", 70 | "textAlign": "left", 71 | "weightText": "medium", 72 | "color": { 73 | "r": 0, 74 | "g": 0, 75 | "b": 0, 76 | "a": 1 77 | } 78 | } 79 | }, 80 | { 81 | "range": { 82 | "location": 5, 83 | "length": 9 84 | }, 85 | "style": { 86 | "fontFace": "SFProText-Regular", 87 | "fontSize": 20, 88 | "fontWeight": 400, 89 | "fontStyle": "normal", 90 | "fontFamily": "SFProText", 91 | "fontStretch": "normal", 92 | "textAlign": "left", 93 | "weightText": "regular", 94 | "color": { 95 | "r": 0, 96 | "g": 0, 97 | "b": 0, 98 | "a": 1 99 | } 100 | } 101 | }, 102 | { 103 | "range": { 104 | "location": 15, 105 | "length": 3 106 | }, 107 | "style": { 108 | "fontFace": "SFProText-Regular", 109 | "fontSize": 20, 110 | "fontWeight": 400, 111 | "fontStyle": "normal", 112 | "fontFamily": "SFProText", 113 | "fontStretch": "normal", 114 | "textAlign": "left", 115 | "weightText": "regular", 116 | "color": { 117 | "r": 255, 118 | "g": 0, 119 | "b": 0, 120 | "a": 1 121 | } 122 | } 123 | } 124 | ], 125 | "assets": [] 126 | }, 127 | { 128 | "type": "text", 129 | "name": "Text layer", 130 | "sourceId": "01c469df-4ff5-449d-8d2a-246c0e87f476", 131 | "exportable": false, 132 | "rotation": 0, 133 | "opacity": 1, 134 | "blendMode": "normal", 135 | "borderRadius": 0, 136 | "rect": { 137 | "y": 0, 138 | "x": 0, 139 | "width": 220, 140 | "height": 24 141 | }, 142 | "content": "Type something", 143 | "borders": [], 144 | "fills": [], 145 | "shadows": [], 146 | "textStyles": [ 147 | { 148 | "range": { 149 | "location": 0, 150 | "length": 14 151 | }, 152 | "style": { 153 | "fontFace": "SFProText-Regular", 154 | "fontSize": 20, 155 | "fontWeight": 400, 156 | "fontStyle": "normal", 157 | "fontFamily": "SFProText", 158 | "fontStretch": "normal", 159 | "textAlign": "left", 160 | "weightText": "regular", 161 | "color": { 162 | "r": 0, 163 | "g": 0, 164 | "b": 0, 165 | "a": 1 166 | } 167 | } 168 | } 169 | ], 170 | "assets": [] 171 | }, 172 | { 173 | "type": "shape", 174 | "name": "Layer with blur", 175 | "sourceId": "49ddae53-1bf3-48e4-ae70-b0ff0abcd1cf", 176 | "exportable": false, 177 | "rotation": 0, 178 | "opacity": 1, 179 | "blendMode": "normal", 180 | "borderRadius": 0, 181 | "rect": { 182 | "y": 530, 183 | "x": 170, 184 | "width": 100, 185 | "height": 100 186 | }, 187 | "content": "", 188 | "borders": [], 189 | "fills": [ 190 | { 191 | "fillType": "color", 192 | "blendMode": "normal", 193 | "color": { 194 | "r": 0, 195 | "g": 255, 196 | "b": 255, 197 | "a": 1 198 | } 199 | } 200 | ], 201 | "shadows": [], 202 | "textStyles": [], 203 | "assets": [], 204 | "blur": { 205 | "type": "gaussian", 206 | "radius": 10 207 | } 208 | }, 209 | { 210 | "type": "shape", 211 | "name": "Exportable layer", 212 | "sourceId": "f02c0baf-0353-4080-95a7-3448314b6084", 213 | "exportable": true, 214 | "rotation": 0, 215 | "opacity": 1, 216 | "blendMode": "normal", 217 | "borderRadius": 0, 218 | "rect": { 219 | "y": 530, 220 | "x": 50, 221 | "width": 100, 222 | "height": 100 223 | }, 224 | "content": "", 225 | "borders": [], 226 | "fills": [ 227 | { 228 | "fillType": "color", 229 | "blendMode": "normal", 230 | "color": { 231 | "r": 255, 232 | "g": 255, 233 | "b": 0, 234 | "a": 1 235 | } 236 | } 237 | ], 238 | "shadows": [], 239 | "textStyles": [], 240 | "assets": [] 241 | }, 242 | { 243 | "type": "shape", 244 | "name": "Layer with border radius", 245 | "sourceId": "66d0761e-246a-4151-87f9-33feb47c6401", 246 | "exportable": false, 247 | "rotation": 0, 248 | "opacity": 1, 249 | "blendMode": "normal", 250 | "borderRadius": 20, 251 | "rect": { 252 | "y": 410, 253 | "x": 50, 254 | "width": 100, 255 | "height": 100 256 | }, 257 | "content": "", 258 | "borders": [], 259 | "fills": [ 260 | { 261 | "fillType": "color", 262 | "blendMode": "normal", 263 | "color": { 264 | "r": 255, 265 | "g": 0, 266 | "b": 0, 267 | "a": 1 268 | } 269 | } 270 | ], 271 | "shadows": [], 272 | "textStyles": [], 273 | "assets": [] 274 | }, 275 | { 276 | "type": "shape", 277 | "name": "Rotated layer", 278 | "sourceId": "76ef71eb-04df-44fb-8ab8-c4214c2144e4", 279 | "exportable": false, 280 | "rotation": -45, 281 | "opacity": 1, 282 | "blendMode": "normal", 283 | "borderRadius": 0, 284 | "rect": { 285 | "y": 410, 286 | "x": 170, 287 | "width": 103, 288 | "height": 100 289 | }, 290 | "content": "", 291 | "borders": [], 292 | "fills": [ 293 | { 294 | "fillType": "color", 295 | "blendMode": "normal", 296 | "color": { 297 | "r": 0, 298 | "g": 255, 299 | "b": 0, 300 | "a": 1 301 | } 302 | } 303 | ], 304 | "shadows": [], 305 | "textStyles": [], 306 | "assets": [] 307 | }, 308 | { 309 | "type": "shape", 310 | "name": "Transparent layer with blend mode", 311 | "sourceId": "5267442e-a184-4c0c-b9c1-24eaf2924d51", 312 | "exportable": false, 313 | "rotation": 0, 314 | "opacity": 0.3, 315 | "blendMode": "multiply", 316 | "borderRadius": 0, 317 | "rect": { 318 | "y": 290, 319 | "x": 170, 320 | "width": 103, 321 | "height": 100 322 | }, 323 | "content": "", 324 | "borders": [], 325 | "fills": [ 326 | { 327 | "fillType": "color", 328 | "blendMode": "normal", 329 | "color": { 330 | "r": 0, 331 | "g": 255, 332 | "b": 0, 333 | "a": 1 334 | } 335 | } 336 | ], 337 | "shadows": [], 338 | "textStyles": [], 339 | "assets": [] 340 | }, 341 | { 342 | "type": "shape", 343 | "name": "Layer with shadow", 344 | "sourceId": "e1ca8514-3755-4305-a854-230d25d7af28", 345 | "exportable": false, 346 | "rotation": 0, 347 | "opacity": 1, 348 | "blendMode": "normal", 349 | "borderRadius": 0, 350 | "rect": { 351 | "y": 290, 352 | "x": 50, 353 | "width": 100, 354 | "height": 100 355 | }, 356 | "content": "", 357 | "borders": [], 358 | "fills": [], 359 | "shadows": [ 360 | { 361 | "type": "outer", 362 | "offsetX": 0, 363 | "offsetY": 2, 364 | "blurRadius": 4, 365 | "spread": 6, 366 | "color": { 367 | "r": 0, 368 | "g": 0, 369 | "b": 0, 370 | "a": 0.5 371 | } 372 | } 373 | ], 374 | "textStyles": [], 375 | "assets": [] 376 | }, 377 | { 378 | "type": "shape", 379 | "name": "Layer with gradient fill", 380 | "sourceId": "e156c599-4813-4a96-8516-0173799905c7", 381 | "exportable": false, 382 | "rotation": 0, 383 | "opacity": 1, 384 | "blendMode": "normal", 385 | "borderRadius": 0, 386 | "rect": { 387 | "y": 120, 388 | "x": 0, 389 | "width": 100, 390 | "height": 100 391 | }, 392 | "content": "", 393 | "borders": [], 394 | "fills": [ 395 | { 396 | "fillType": "gradient", 397 | "blendMode": "normal", 398 | "gradient": { 399 | "type": "linear", 400 | "from": { 401 | "x": 0.5, 402 | "y": 0 403 | }, 404 | "to": { 405 | "x": 0.5, 406 | "y": 1 407 | }, 408 | "colorStops": [ 409 | { 410 | "color": { 411 | "r": 255, 412 | "g": 255, 413 | "b": 255, 414 | "a": 0.5 415 | }, 416 | "position": 0 417 | }, 418 | { 419 | "color": { 420 | "r": 0, 421 | "g": 0, 422 | "b": 0, 423 | "a": 0.5 424 | }, 425 | "position": 1 426 | } 427 | ] 428 | } 429 | } 430 | ], 431 | "shadows": [], 432 | "textStyles": [], 433 | "assets": [] 434 | }, 435 | { 436 | "type": "shape", 437 | "name": "Layer with fill", 438 | "sourceId": "fd1c3674-bfca-4b6c-81d6-5033703e8891", 439 | "exportable": false, 440 | "rotation": 0, 441 | "opacity": 1, 442 | "blendMode": "normal", 443 | "borderRadius": 0, 444 | "rect": { 445 | "y": 0, 446 | "x": 0, 447 | "width": 100, 448 | "height": 100 449 | }, 450 | "content": "", 451 | "borders": [], 452 | "fills": [ 453 | { 454 | "fillType": "color", 455 | "blendMode": "normal", 456 | "color": { 457 | "r": 0, 458 | "g": 0, 459 | "b": 255, 460 | "a": 1, 461 | "sourceId": "e719f81a-9ded-4ff1-84f6-e5d0b8cd950c" 462 | } 463 | } 464 | ], 465 | "shadows": [], 466 | "textStyles": [], 467 | "assets": [] 468 | }, 469 | { 470 | "type": "shape", 471 | "name": "Layer with gradient border", 472 | "sourceId": "e308b2cb-aa94-4294-a5e3-5e37ee0af33c", 473 | "exportable": false, 474 | "rotation": 0, 475 | "opacity": 1, 476 | "blendMode": "normal", 477 | "borderRadius": 0, 478 | "rect": { 479 | "y": 120, 480 | "x": 0, 481 | "width": 100, 482 | "height": 100 483 | }, 484 | "content": "", 485 | "borders": [ 486 | { 487 | "position": "outside", 488 | "thickness": 6, 489 | "fillType": "gradient", 490 | "blendMode": "normal", 491 | "gradient": { 492 | "type": "radial", 493 | "from": { 494 | "x": 0.5, 495 | "y": 0 496 | }, 497 | "to": { 498 | "x": 0.5, 499 | "y": 1 500 | }, 501 | "colorStops": [ 502 | { 503 | "color": { 504 | "r": 255, 505 | "g": 0, 506 | "b": 0, 507 | "a": 1 508 | }, 509 | "position": 0 510 | }, 511 | { 512 | "color": { 513 | "r": 255, 514 | "g": 0, 515 | "b": 0, 516 | "a": 0 517 | }, 518 | "position": 1 519 | } 520 | ] 521 | } 522 | } 523 | ], 524 | "fills": [], 525 | "shadows": [], 526 | "textStyles": [], 527 | "assets": [] 528 | }, 529 | { 530 | "type": "shape", 531 | "name": "Layer with border", 532 | "sourceId": "8713de27-bf05-47d8-bc0f-90e1ce1cdbdc", 533 | "exportable": false, 534 | "rotation": 0, 535 | "opacity": 1, 536 | "blendMode": "normal", 537 | "borderRadius": 0, 538 | "rect": { 539 | "y": 0, 540 | "x": 0, 541 | "width": 100, 542 | "height": 100 543 | }, 544 | "content": "", 545 | "borders": [ 546 | { 547 | "position": "inside", 548 | "thickness": 2, 549 | "fillType": "color", 550 | "blendMode": "normal", 551 | "color": { 552 | "r": 0, 553 | "g": 0, 554 | "b": 0, 555 | "a": 1 556 | } 557 | } 558 | ], 559 | "fills": [], 560 | "shadows": [], 561 | "textStyles": [], 562 | "assets": [] 563 | }, 564 | { 565 | "type": "group", 566 | "name": "layer with layout", 567 | "sourceId": "49478193-d2c9-4aa4-90b1-b06d70dfa9f5", 568 | "layout": { 569 | "alignment": "min", 570 | "direction": "row", 571 | "gap": 30, 572 | "padding": { 573 | "horizontal": 40, 574 | "vertical": 20 575 | }, 576 | "sizingMode": "auto" 577 | }, 578 | "opacity": 1, 579 | "blendMode": "normal", 580 | "borderRadius": 0, 581 | "rect": { 582 | "x": 0, 583 | "y": 0, 584 | "width": 310, 585 | "height": 140 586 | }, 587 | "content": "", 588 | "layers": [ 589 | { 590 | "type": "shape", 591 | "name": "first child", 592 | "sourceId": "51081349-b540-4f27-997b-5fb405d286ff", 593 | "opacity": 1, 594 | "blendMode": "normal", 595 | "borderRadius": 0, 596 | "rect": { 597 | "x": 40, 598 | "y": 20, 599 | "width": 100, 600 | "height": 100 601 | }, 602 | "content": "", 603 | "borders": [], 604 | "fills": [], 605 | "shadows": [], 606 | "textStyles": [], 607 | "assets": [] 608 | }, 609 | { 610 | "type": "shape", 611 | "name": "second child", 612 | "sourceId": "98535715-30ee-4ffd-acbf-04a0be34d73d", 613 | "opacity": 1, 614 | "blendMode": "normal", 615 | "borderRadius": 0, 616 | "rect": { 617 | "x": 170, 618 | "y": 20, 619 | "width": 100, 620 | "height": 100 621 | }, 622 | 623 | "content": "", 624 | "borders": [], 625 | "fills": [], 626 | "shadows": [], 627 | "textStyles": [], 628 | "assets": [] 629 | } 630 | ], 631 | "borders": [], 632 | "fills": [], 633 | "shadows": [], 634 | "textStyles": [], 635 | "assets": [] 636 | } 637 | ] 638 | }, 639 | "version": "" 640 | } 641 | -------------------------------------------------------------------------------- /src/sample-data/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "snapshot": { 3 | "backgroundColor": { "r": 255, "b": 255, "g": 255, "a": 1 }, 4 | "source": "sketch", 5 | "height": 960, 6 | "densityScale": 1, 7 | "width": 1024, 8 | "links": [], 9 | "componentNames": [ 10 | "Simple Component", 11 | "Component with description" 12 | ], 13 | "url": "https://cdn.zeplin.io/5e2ef1da1b3cdb167efd54f9/screens/B0E3646F-9ED1-4DE8-B83D-3AB7AF80B5DE.png", 14 | "_id": "5e38085266ecb97b1faaa969", 15 | "density": "1x", 16 | "assets": [], 17 | "layers": [ 18 | { 19 | "type": "shape", 20 | "name": "Sample layer", 21 | "sourceId": "15b2dbb4-3259-4c8e-a8e3-24b292145d21", 22 | "opacity": 1, 23 | "blendMode": "normal", 24 | "borderRadius": 0, 25 | "rect": { 26 | "x": 0, 27 | "y": 0, 28 | "width": 320, 29 | "height": 768 30 | }, 31 | "content": "", 32 | "borders": [], 33 | "fills": [], 34 | "shadows": [], 35 | "textStyles": [], 36 | "assets": [] 37 | }, 38 | { 39 | "type": "text", 40 | "name": "Text layer with multiple styles", 41 | "sourceId": "859f1b9e-0437-4408-b84c-ea6c10c37d8f", 42 | "exportable": false, 43 | "rotation": 0, 44 | "opacity": 1, 45 | "blendMode": "normal", 46 | "borderRadius": 0, 47 | "rect": { 48 | "y": 44, 49 | "x": 0, 50 | "width": 220, 51 | "height": 24 52 | }, 53 | "content": "Type something red", 54 | "borders": [], 55 | "fills": [], 56 | "shadows": [], 57 | "textStyles": [ 58 | { 59 | "range": { 60 | "location": 0, 61 | "length": 4 62 | }, 63 | "style": { 64 | "fontFace": "SFProText-Medium", 65 | "fontSize": 20, 66 | "fontWeight": 500, 67 | "fontStyle": "normal", 68 | "fontFamily": "SFProText", 69 | "fontStretch": "normal", 70 | "textAlign": "left", 71 | "weightText": "medium", 72 | "color": { 73 | "r": 0, 74 | "g": 0, 75 | "b": 0, 76 | "a": 1 77 | } 78 | } 79 | }, 80 | { 81 | "range": { 82 | "location": 5, 83 | "length": 9 84 | }, 85 | "style": { 86 | "fontFace": "SFProText-Regular", 87 | "fontSize": 20, 88 | "fontWeight": 400, 89 | "fontStyle": "normal", 90 | "fontFamily": "SFProText", 91 | "fontStretch": "normal", 92 | "textAlign": "left", 93 | "weightText": "regular", 94 | "color": { 95 | "r": 0, 96 | "g": 0, 97 | "b": 0, 98 | "a": 1 99 | } 100 | } 101 | }, 102 | { 103 | "range": { 104 | "location": 15, 105 | "length": 3 106 | }, 107 | "style": { 108 | "fontFace": "SFProText-Regular", 109 | "fontSize": 20, 110 | "fontWeight": 400, 111 | "fontStyle": "normal", 112 | "fontFamily": "SFProText", 113 | "fontStretch": "normal", 114 | "textAlign": "left", 115 | "weightText": "regular", 116 | "color": { 117 | "r": 255, 118 | "g": 0, 119 | "b": 0, 120 | "a": 1 121 | } 122 | } 123 | } 124 | ], 125 | "assets": [] 126 | }, 127 | { 128 | "type": "text", 129 | "name": "Text layer", 130 | "sourceId": "01c469df-4ff5-449d-8d2a-246c0e87f476", 131 | "exportable": false, 132 | "rotation": 0, 133 | "opacity": 1, 134 | "blendMode": "normal", 135 | "borderRadius": 0, 136 | "rect": { 137 | "y": 0, 138 | "x": 0, 139 | "width": 220, 140 | "height": 24 141 | }, 142 | "content": "Type something", 143 | "borders": [], 144 | "fills": [], 145 | "shadows": [], 146 | "textStyles": [ 147 | { 148 | "range": { 149 | "location": 0, 150 | "length": 14 151 | }, 152 | "style": { 153 | "fontFace": "SFProText-Regular", 154 | "fontSize": 20, 155 | "fontWeight": 400, 156 | "fontStyle": "normal", 157 | "fontFamily": "SFProText", 158 | "fontStretch": "normal", 159 | "textAlign": "left", 160 | "weightText": "regular", 161 | "color": { 162 | "r": 0, 163 | "g": 0, 164 | "b": 0, 165 | "a": 1 166 | } 167 | } 168 | } 169 | ], 170 | "assets": [] 171 | }, 172 | { 173 | "type": "shape", 174 | "name": "Layer with blur", 175 | "sourceId": "49ddae53-1bf3-48e4-ae70-b0ff0abcd1cf", 176 | "exportable": false, 177 | "rotation": 0, 178 | "opacity": 1, 179 | "blendMode": "normal", 180 | "borderRadius": 0, 181 | "rect": { 182 | "y": 530, 183 | "x": 170, 184 | "width": 100, 185 | "height": 100 186 | }, 187 | "content": "", 188 | "borders": [], 189 | "fills": [ 190 | { 191 | "fillType": "color", 192 | "blendMode": "normal", 193 | "fill": { 194 | "r": 0, 195 | "g": 255, 196 | "b": 255, 197 | "a": 1 198 | }, 199 | "color": { 200 | "r": 0, 201 | "g": 255, 202 | "b": 255, 203 | "a": 1 204 | } 205 | } 206 | ], 207 | "shadows": [], 208 | "textStyles": [], 209 | "assets": [], 210 | "blur": { 211 | "type": "gaussian", 212 | "radius": 10 213 | } 214 | }, 215 | { 216 | "type": "shape", 217 | "name": "Exportable layer", 218 | "sourceId": "f02c0baf-0353-4080-95a7-3448314b6084", 219 | "exportable": true, 220 | "rotation": 0, 221 | "opacity": 1, 222 | "blendMode": "normal", 223 | "borderRadius": 0, 224 | "rect": { 225 | "y": 530, 226 | "x": 50, 227 | "width": 100, 228 | "height": 100 229 | }, 230 | "content": "", 231 | "borders": [], 232 | "fills": [ 233 | { 234 | "fillType": "color", 235 | "blendMode": "normal", 236 | "fill": { 237 | "r": 255, 238 | "g": 255, 239 | "b": 0, 240 | "a": 1 241 | }, 242 | "color": { 243 | "r": 255, 244 | "g": 255, 245 | "b": 0, 246 | "a": 1 247 | } 248 | } 249 | ], 250 | "shadows": [], 251 | "textStyles": [], 252 | "assets": [] 253 | }, 254 | { 255 | "type": "shape", 256 | "name": "Layer with border radius", 257 | "sourceId": "66d0761e-246a-4151-87f9-33feb47c6401", 258 | "exportable": false, 259 | "rotation": 0, 260 | "opacity": 1, 261 | "blendMode": "normal", 262 | "borderRadius": 20, 263 | "rect": { 264 | "y": 410, 265 | "x": 50, 266 | "width": 100, 267 | "height": 100 268 | }, 269 | "content": "", 270 | "borders": [], 271 | "fills": [ 272 | { 273 | "fillType": "color", 274 | "blendMode": "normal", 275 | "fill": { 276 | "r": 255, 277 | "g": 0, 278 | "b": 0, 279 | "a": 1 280 | }, 281 | "color": { 282 | "r": 255, 283 | "g": 0, 284 | "b": 0, 285 | "a": 1 286 | } 287 | } 288 | ], 289 | "shadows": [], 290 | "textStyles": [], 291 | "assets": [] 292 | }, 293 | { 294 | "type": "shape", 295 | "name": "Rotated layer", 296 | "sourceId": "76ef71eb-04df-44fb-8ab8-c4214c2144e4", 297 | "exportable": false, 298 | "rotation": -45, 299 | "opacity": 1, 300 | "blendMode": "normal", 301 | "borderRadius": 0, 302 | "rect": { 303 | "y": 410, 304 | "x": 170, 305 | "width": 103, 306 | "height": 100 307 | }, 308 | "content": "", 309 | "borders": [], 310 | "fills": [ 311 | { 312 | "fillType": "color", 313 | "blendMode": "normal", 314 | "fill": { 315 | "r": 0, 316 | "g": 255, 317 | "b": 0, 318 | "a": 1 319 | }, 320 | "color": { 321 | "r": 0, 322 | "g": 255, 323 | "b": 0, 324 | "a": 1 325 | } 326 | } 327 | ], 328 | "shadows": [], 329 | "textStyles": [], 330 | "assets": [] 331 | }, 332 | { 333 | "type": "shape", 334 | "name": "Transparent layer with blend mode", 335 | "sourceId": "5267442e-a184-4c0c-b9c1-24eaf2924d51", 336 | "exportable": false, 337 | "rotation": 0, 338 | "opacity": 0.3, 339 | "blendMode": "multiply", 340 | "borderRadius": 0, 341 | "rect": { 342 | "y": 290, 343 | "x": 170, 344 | "width": 103, 345 | "height": 100 346 | }, 347 | "content": "", 348 | "borders": [], 349 | "fills": [ 350 | { 351 | "fillType": "color", 352 | "blendMode": "normal", 353 | "fill": { 354 | "r": 0, 355 | "g": 255, 356 | "b": 0, 357 | "a": 1 358 | }, 359 | "color": { 360 | "r": 0, 361 | "g": 255, 362 | "b": 0, 363 | "a": 1 364 | } 365 | } 366 | ], 367 | "shadows": [], 368 | "textStyles": [], 369 | "assets": [] 370 | }, 371 | { 372 | "type": "shape", 373 | "name": "Layer with shadow", 374 | "sourceId": "e1ca8514-3755-4305-a854-230d25d7af28", 375 | "exportable": false, 376 | "rotation": 0, 377 | "opacity": 1, 378 | "blendMode": "normal", 379 | "borderRadius": 0, 380 | "rect": { 381 | "y": 290, 382 | "x": 50, 383 | "width": 100, 384 | "height": 100 385 | }, 386 | "content": "", 387 | "borders": [], 388 | "fills": [], 389 | "shadows": [ 390 | { 391 | "type": "outer", 392 | "offsetX": 0, 393 | "offsetY": 2, 394 | "blurRadius": 4, 395 | "spread": 6, 396 | "color": { 397 | "r": 0, 398 | "g": 0, 399 | "b": 0, 400 | "a": 0.5 401 | } 402 | } 403 | ], 404 | "textStyles": [], 405 | "assets": [] 406 | }, 407 | { 408 | "type": "shape", 409 | "name": "Layer with gradient fill", 410 | "sourceId": "e156c599-4813-4a96-8516-0173799905c7", 411 | "exportable": false, 412 | "rotation": 0, 413 | "opacity": 1, 414 | "blendMode": "normal", 415 | "borderRadius": 0, 416 | "rect": { 417 | "y": 120, 418 | "x": 0, 419 | "width": 100, 420 | "height": 100 421 | }, 422 | "content": "", 423 | "borders": [], 424 | "fills": [ 425 | { 426 | "fillType": "gradient", 427 | "blendMode": "normal", 428 | "fill": { 429 | "type": "linear", 430 | "from": { 431 | "x": 0.5, 432 | "y": 0 433 | }, 434 | "to": { 435 | "x": 0.5, 436 | "y": 1 437 | }, 438 | "colorStops": [ 439 | { 440 | "color": { 441 | "r": 255, 442 | "g": 255, 443 | "b": 255, 444 | "a": 0.5 445 | }, 446 | "position": 0 447 | }, 448 | { 449 | "color": { 450 | "r": 0, 451 | "g": 0, 452 | "b": 0, 453 | "a": 0.5 454 | }, 455 | "position": 1 456 | } 457 | ] 458 | }, 459 | "gradient": { 460 | "type": "linear", 461 | "from": { 462 | "x": 0.5, 463 | "y": 0 464 | }, 465 | "to": { 466 | "x": 0.5, 467 | "y": 1 468 | }, 469 | "colorStops": [ 470 | { 471 | "color": { 472 | "r": 255, 473 | "g": 255, 474 | "b": 255, 475 | "a": 0.5 476 | }, 477 | "position": 0 478 | }, 479 | { 480 | "color": { 481 | "r": 0, 482 | "g": 0, 483 | "b": 0, 484 | "a": 0.5 485 | }, 486 | "position": 1 487 | } 488 | ] 489 | } 490 | } 491 | ], 492 | "shadows": [], 493 | "textStyles": [], 494 | "assets": [] 495 | }, 496 | { 497 | "type": "shape", 498 | "name": "Layer with fill", 499 | "sourceId": "fd1c3674-bfca-4b6c-81d6-5033703e8891", 500 | "exportable": false, 501 | "rotation": 0, 502 | "opacity": 1, 503 | "blendMode": "normal", 504 | "borderRadius": 0, 505 | "rect": { 506 | "y": 0, 507 | "x": 0, 508 | "width": 100, 509 | "height": 100 510 | }, 511 | "content": "", 512 | "borders": [], 513 | "fills": [ 514 | { 515 | "fillType": "color", 516 | "blendMode": "normal", 517 | "fill": { 518 | "r": 0, 519 | "g": 0, 520 | "b": 255, 521 | "a": 1, 522 | "sourceId": "e719f81a-9ded-4ff1-84f6-e5d0b8cd950c" 523 | }, 524 | "color": { 525 | "r": 0, 526 | "g": 0, 527 | "b": 255, 528 | "a": 1, 529 | "sourceId": "e719f81a-9ded-4ff1-84f6-e5d0b8cd950c" 530 | } 531 | } 532 | ], 533 | "shadows": [], 534 | "textStyles": [], 535 | "assets": [] 536 | }, 537 | { 538 | "type": "shape", 539 | "name": "Layer with gradient border", 540 | "sourceId": "e308b2cb-aa94-4294-a5e3-5e37ee0af33c", 541 | "exportable": false, 542 | "rotation": 0, 543 | "opacity": 1, 544 | "blendMode": "normal", 545 | "borderRadius": 0, 546 | "rect": { 547 | "y": 120, 548 | "x": 0, 549 | "width": 100, 550 | "height": 100 551 | }, 552 | "content": "", 553 | "borders": [ 554 | { 555 | "position": "outside", 556 | "thickness": 6, 557 | "fill": { 558 | "type": "gradient", 559 | "blendMode": "normal", 560 | "fill": { 561 | "type": "radial", 562 | "from": { 563 | "x": 0.5, 564 | "y": 0 565 | }, 566 | "to": { 567 | "x": 0.5, 568 | "y": 1 569 | }, 570 | "colorStops": [ 571 | { 572 | "color": { 573 | "r": 255, 574 | "g": 0, 575 | "b": 0, 576 | "a": 1 577 | }, 578 | "position": 0 579 | }, 580 | { 581 | "color": { 582 | "r": 255, 583 | "g": 0, 584 | "b": 0, 585 | "a": 0 586 | }, 587 | "position": 1 588 | } 589 | ] 590 | }, 591 | "gradient": { 592 | "type": "radial", 593 | "from": { 594 | "x": 0.5, 595 | "y": 0 596 | }, 597 | "to": { 598 | "x": 0.5, 599 | "y": 1 600 | }, 601 | "colorStops": [ 602 | { 603 | "color": { 604 | "r": 255, 605 | "g": 0, 606 | "b": 0, 607 | "a": 1 608 | }, 609 | "position": 0 610 | }, 611 | { 612 | "color": { 613 | "r": 255, 614 | "g": 0, 615 | "b": 0, 616 | "a": 0 617 | }, 618 | "position": 1 619 | } 620 | ] 621 | } 622 | } 623 | } 624 | ], 625 | "fills": [], 626 | "shadows": [], 627 | "textStyles": [], 628 | "assets": [] 629 | }, 630 | { 631 | "type": "shape", 632 | "name": "Layer with border", 633 | "sourceId": "8713de27-bf05-47d8-bc0f-90e1ce1cdbdc", 634 | "exportable": false, 635 | "rotation": 0, 636 | "opacity": 1, 637 | "blendMode": "normal", 638 | "borderRadius": 0, 639 | "rect": { 640 | "y": 0, 641 | "x": 0, 642 | "width": 100, 643 | "height": 100 644 | }, 645 | "content": "", 646 | "borders": [ 647 | { 648 | "position": "inside", 649 | "thickness": 2, 650 | "fill": { 651 | "type": "color", 652 | "blendMode": "normal", 653 | "fill": { 654 | "r": 0, 655 | "g": 0, 656 | "b": 0, 657 | "a": 1 658 | }, 659 | "color": { 660 | "r": 0, 661 | "g": 0, 662 | "b": 0, 663 | "a": 1 664 | } 665 | } 666 | } 667 | ], 668 | "fills": [], 669 | "shadows": [], 670 | "textStyles": [], 671 | "assets": [] 672 | }, 673 | { 674 | "type": "group", 675 | "name": "layer with layout", 676 | "sourceId": "49478193-d2c9-4aa4-90b1-b06d70dfa9f5", 677 | "layout": { 678 | "alignment": "min", 679 | "direction": "row", 680 | "gap": 30, 681 | "padding": { 682 | "horizontal": 40, 683 | "vertical": 20 684 | }, 685 | "sizingMode": "auto" 686 | }, 687 | "opacity": 1, 688 | "blendMode": "normal", 689 | "borderRadius": 0, 690 | "rect": { 691 | "x": 0, 692 | "y": 0, 693 | "width": 310, 694 | "height": 140 695 | }, 696 | "content": "", 697 | "layers": [ 698 | { 699 | "type": "shape", 700 | "name": "first child", 701 | "sourceId": "51081349-b540-4f27-997b-5fb405d286ff", 702 | "opacity": 1, 703 | "blendMode": "normal", 704 | "borderRadius": 0, 705 | "rect": { 706 | "x": 40, 707 | "y": 20, 708 | "width": 100, 709 | "height": 100 710 | }, 711 | "content": "", 712 | "borders": [], 713 | "fills": [], 714 | "shadows": [], 715 | "textStyles": [], 716 | "assets": [] 717 | }, 718 | { 719 | "type": "shape", 720 | "name": "second child", 721 | "sourceId": "98535715-30ee-4ffd-acbf-04a0be34d73d", 722 | "opacity": 1, 723 | "blendMode": "normal", 724 | "borderRadius": 0, 725 | "rect": { 726 | "x": 170, 727 | "y": 20, 728 | "width": 100, 729 | "height": 100 730 | }, 731 | 732 | "content": "", 733 | "borders": [], 734 | "fills": [], 735 | "shadows": [], 736 | "textStyles": [], 737 | "assets": [] 738 | } 739 | ], 740 | "borders": [], 741 | "fills": [], 742 | "shadows": [], 743 | "textStyles": [], 744 | "assets": [] 745 | } 746 | ] 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /src/sample-data/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Header", 4 | "description": "", 5 | "latestVersion": { 6 | "snapshot": { 7 | "assets": [ 8 | { 9 | "layerId": "7E3861F2-458C-4917-A698-962F2D05F508", 10 | "layerName": " Logo", 11 | "displayName": "logo", 12 | "contents": [ 13 | { 14 | "url": "https://placekitten.com/200/299", 15 | "density": "1x", 16 | "format": "png" 17 | }, 18 | { 19 | "url": "https://placekitten.com/200/299", 20 | "density": "1x", 21 | "format": "svg", 22 | "optimized": { 23 | "url": "https://placekitten.com/200/299", 24 | "density": "1x", 25 | "format": "svg", 26 | "percent": 68 27 | } 28 | }, 29 | { 30 | "url": "https://placekitten.com/200/299", 31 | "density": "2x", 32 | "format": "png" 33 | }, 34 | { 35 | "url": "https://placekitten.com/200/299", 36 | "density": "3x", 37 | "format": "png" 38 | } 39 | ], 40 | "_id": "5cb69a783fdf9e4002334764", 41 | "size": { 42 | "width": 32, 43 | "height": 32 44 | }, 45 | "lightness": "dark" 46 | } 47 | ], 48 | "source": "sketch", 49 | "height": 84, 50 | "layers": [ 51 | { 52 | "borderRadius": 0, 53 | "componentName": "Header bg / Orange", 54 | "rotation": 0, 55 | "layers": [], 56 | "blendMode": "normal", 57 | "interactionLevel": "bottom", 58 | "rect": { 59 | "y": 0, 60 | "x": 0, 61 | "width": 1024, 62 | "height": 84 63 | }, 64 | "type": "group", 65 | "opacity": 1, 66 | "sourceId": "9B5912B0-9043-450E-9830-DFB71382B34B", 67 | "fills": [ 68 | { 69 | "color": { 70 | "r": 253, 71 | "b": 57, 72 | "g": 189, 73 | "a": 1 74 | }, 75 | "fillType": "color", 76 | "blendMode": "normal", 77 | "opacity": 1 78 | } 79 | ], 80 | "exportable": false, 81 | "borders": [], 82 | "name": " Orange", 83 | "shadows": [], 84 | "_id": "5cb69a783fdf9e4002334753", 85 | "absoluteRect": { 86 | "x": 0, 87 | "y": 0, 88 | "width": 1024, 89 | "height": 84 90 | } 91 | }, 92 | { 93 | "borderRadius": 0, 94 | "componentName": "Icons / Logo", 95 | "rotation": 0, 96 | "layers": [ 97 | { 98 | "borders": [], 99 | "rect": { 100 | "y": 0, 101 | "x": 0, 102 | "width": 32, 103 | "height": 32 104 | }, 105 | "rotation": 0, 106 | "layers": [ 107 | { 108 | "borderRadius": 0, 109 | "shadows": [], 110 | "rotation": 0, 111 | "blendMode": "normal", 112 | "type": "shape", 113 | "opacity": 1, 114 | "sourceId": "25B1FD06-178F-4901-B533-49D6D602D97F", 115 | "fills": [ 116 | { 117 | "color": { 118 | "r": 255, 119 | "b": 255, 120 | "g": 255, 121 | "a": 1 122 | }, 123 | "opacity": 1, 124 | "fillType": "color" 125 | } 126 | ], 127 | "exportable": false, 128 | "borders": [], 129 | "name": "Oval", 130 | "rect": { 131 | "y": 0, 132 | "x": 0, 133 | "width": 32, 134 | "height": 32 135 | }, 136 | "_id": "5cb69a783fdf9e4002334756", 137 | "absoluteRect": { 138 | "x": 12, 139 | "y": 40, 140 | "width": 32, 141 | "height": 32 142 | } 143 | }, 144 | { 145 | "borders": [], 146 | "rect": { 147 | "y": 1.038961038961039, 148 | "x": 1.038961038961039, 149 | "width": 29.92207792207792, 150 | "height": 29.92207792207792 151 | }, 152 | "rotation": 0, 153 | "layers": [ 154 | { 155 | "borderRadius": 0, 156 | "shadows": [], 157 | "rotation": 0, 158 | "blendMode": "normal", 159 | "type": "shape", 160 | "opacity": 1, 161 | "sourceId": "090685FD-58E7-4771-AA1C-CC95CB5A51C6", 162 | "fills": [ 163 | { 164 | "color": { 165 | "r": 238, 166 | "b": 35, 167 | "g": 103, 168 | "a": 1 169 | }, 170 | "opacity": 1, 171 | "fillType": "color" 172 | } 173 | ], 174 | "exportable": false, 175 | "borders": [], 176 | "name": "Oval 1", 177 | "rect": { 178 | "y": 14.96103896103896, 179 | "x": 0, 180 | "width": 29.9220779220779, 181 | "height": 14.96103896103896 182 | }, 183 | "_id": "5cb69a783fdf9e4002334758", 184 | "absoluteRect": { 185 | "x": 13.03896103896104, 186 | "y": 56, 187 | "width": 29.9220779220779, 188 | "height": 14.96103896103896 189 | } 190 | }, 191 | { 192 | "borderRadius": 0, 193 | "shadows": [], 194 | "rotation": 0, 195 | "blendMode": "normal", 196 | "type": "shape", 197 | "opacity": 1, 198 | "sourceId": "046464D9-46C9-435D-829F-11A89045F225", 199 | "fills": [ 200 | { 201 | "color": { 202 | "r": 253, 203 | "b": 57, 204 | "g": 189, 205 | "a": 1 206 | }, 207 | "opacity": 1, 208 | "fillType": "color" 209 | } 210 | ], 211 | "exportable": false, 212 | "borders": [], 213 | "name": "Oval 1 Copy", 214 | "rect": { 215 | "y": 0, 216 | "x": 0, 217 | "width": 29.92207792207792, 218 | "height": 14.96103896103896 219 | }, 220 | "_id": "5cb69a783fdf9e4002334759", 221 | "absoluteRect": { 222 | "x": 13.03896103896104, 223 | "y": 41.03896103896104, 224 | "width": 29.92207792207792, 225 | "height": 14.96103896103896 226 | } 227 | }, 228 | { 229 | "borderRadius": 0, 230 | "shadows": [], 231 | "rotation": 0, 232 | "blendMode": "normal", 233 | "type": "shape", 234 | "opacity": 1, 235 | "sourceId": "FD5F8BED-E602-4876-AE7A-7D4073DFBAD0", 236 | "fills": [ 237 | { 238 | "color": { 239 | "r": 254, 240 | "b": 51, 241 | "g": 207, 242 | "a": 1 243 | }, 244 | "opacity": 1, 245 | "fillType": "color" 246 | } 247 | ], 248 | "exportable": false, 249 | "borders": [], 250 | "name": "Oval 2 Copy", 251 | "rect": { 252 | "y": 14.96103896103896, 253 | "x": 0, 254 | "width": 29.92207792207792, 255 | "height": 9.044243891701518 256 | }, 257 | "_id": "5cb69a783fdf9e400233475a", 258 | "absoluteRect": { 259 | "x": 13.03896103896104, 260 | "y": 56, 261 | "width": 29.92207792207792, 262 | "height": 9.044243891701518 263 | } 264 | }, 265 | { 266 | "borderRadius": 0, 267 | "shadows": [], 268 | "rotation": 0, 269 | "blendMode": "normal", 270 | "type": "shape", 271 | "opacity": 1, 272 | "sourceId": "4048E04C-29EC-4D04-AC15-8F9C0F386442", 273 | "fills": [ 274 | { 275 | "color": { 276 | "r": 246, 277 | "b": 51, 278 | "g": 152, 279 | "a": 1 280 | }, 281 | "opacity": 1, 282 | "fillType": "color" 283 | } 284 | ], 285 | "exportable": false, 286 | "borders": [], 287 | "name": "Oval 2", 288 | "rect": { 289 | "y": 6.170372000880511, 290 | "x": 0, 291 | "width": 29.92207792207792, 292 | "height": 8.790666960158482 293 | }, 294 | "_id": "5cb69a783fdf9e400233475b", 295 | "absoluteRect": { 296 | "x": 13.03896103896104, 297 | "y": 47.20933303984155, 298 | "width": 29.92207792207792, 299 | "height": 8.790666960158482 300 | } 301 | } 302 | ], 303 | "blendMode": "normal", 304 | "type": "group", 305 | "opacity": 1, 306 | "sourceId": "F1DD6B18-4003-41ED-BC03-23F0034A60F9", 307 | "fills": [], 308 | "exportable": false, 309 | "borderRadius": 0, 310 | "name": "Group", 311 | "shadows": [], 312 | "_id": "5cb69a783fdf9e4002334757", 313 | "absoluteRect": { 314 | "x": 13.03896103896104, 315 | "y": 41.03896103896104, 316 | "width": 29.92207792207792, 317 | "height": 29.92207792207792 318 | } 319 | } 320 | ], 321 | "blendMode": "normal", 322 | "type": "group", 323 | "opacity": 1, 324 | "sourceId": "D45E7D12-7EAA-4846-8A81-89EB701B56A2", 325 | "fills": [], 326 | "exportable": false, 327 | "borderRadius": 0, 328 | "name": "Group 13", 329 | "shadows": [], 330 | "_id": "5cb69a783fdf9e4002334755", 331 | "absoluteRect": { 332 | "x": 12, 333 | "y": 40, 334 | "width": 32, 335 | "height": 32 336 | } 337 | } 338 | ], 339 | "blendMode": "normal", 340 | "interactionLevel": "top", 341 | "rect": { 342 | "y": 40, 343 | "x": 12, 344 | "width": 32, 345 | "height": 32 346 | }, 347 | "type": "group", 348 | "opacity": 1, 349 | "sourceId": "7E3861F2-458C-4917-A698-962F2D05F508", 350 | "fills": [], 351 | "exportable": true, 352 | "borders": [], 353 | "name": " Logo", 354 | "shadows": [], 355 | "_id": "5cb69a783fdf9e4002334754", 356 | "absoluteRect": { 357 | "x": 12, 358 | "y": 40, 359 | "width": 32, 360 | "height": 32 361 | } 362 | }, 363 | { 364 | "borderRadius": 0, 365 | "componentName": "Header / Title / Small", 366 | "rotation": 0, 367 | "layers": [ 368 | { 369 | "shadows": [], 370 | "name": "Title", 371 | "rotation": 0, 372 | "blendMode": "normal", 373 | "content": "Title", 374 | "type": "text", 375 | "opacity": 1, 376 | "sourceId": "816DF671-F304-45AD-B457-94D65357A553", 377 | "fills": [], 378 | "textStyles": [ 379 | { 380 | "range": { 381 | "location": 0, 382 | "length": 5 383 | }, 384 | "style": { 385 | "fontSize": 14, 386 | "fontFace": "OpenSans-Bold", 387 | "textAlign": "center", 388 | "color": { 389 | "r": 255, 390 | "b": 255, 391 | "g": 255, 392 | "a": 1 393 | } 394 | } 395 | } 396 | ], 397 | "exportable": false, 398 | "borders": [], 399 | "borderRadius": 0, 400 | "rect": { 401 | "y": 0, 402 | "x": 6, 403 | "width": 72, 404 | "height": 19 405 | }, 406 | "_id": "5cb69a783fdf9e400233475d", 407 | "absoluteRect": { 408 | "x": 838, 409 | "y": 53, 410 | "width": 72, 411 | "height": 19 412 | } 413 | } 414 | ], 415 | "blendMode": "normal", 416 | "interactionLevel": "bottom", 417 | "rect": { 418 | "y": 53, 419 | "x": 832, 420 | "width": 84, 421 | "height": 31 422 | }, 423 | "type": "group", 424 | "opacity": 1, 425 | "sourceId": "5CA70284-DF70-4553-9FEA-19FD728C0278", 426 | "fills": [ 427 | { 428 | "color": { 429 | "r": 151, 430 | "b": 151, 431 | "g": 145, 432 | "a": 1 433 | }, 434 | "fillType": "color", 435 | "blendMode": "normal", 436 | "opacity": 1 437 | } 438 | ], 439 | "exportable": false, 440 | "borders": [], 441 | "name": " Small", 442 | "shadows": [], 443 | "_id": "5cb69a783fdf9e400233475c", 444 | "absoluteRect": { 445 | "x": 832, 446 | "y": 53, 447 | "width": 84, 448 | "height": 31 449 | } 450 | }, 451 | { 452 | "borderRadius": 0, 453 | "componentName": "Header / Title / Small", 454 | "rotation": 0, 455 | "layers": [ 456 | { 457 | "shadows": [], 458 | "name": "Title", 459 | "rotation": 0, 460 | "blendMode": "normal", 461 | "content": "Title", 462 | "type": "text", 463 | "opacity": 1, 464 | "sourceId": "816DF671-F304-45AD-B457-94D65357A553", 465 | "fills": [], 466 | "textStyles": [ 467 | { 468 | "range": { 469 | "location": 0, 470 | "length": 5 471 | }, 472 | "style": { 473 | "fontSize": 14, 474 | "fontFace": "OpenSans-Bold", 475 | "textAlign": "center", 476 | "color": { 477 | "r": 255, 478 | "b": 255, 479 | "g": 255, 480 | "a": 1 481 | } 482 | } 483 | } 484 | ], 485 | "exportable": false, 486 | "borders": [], 487 | "borderRadius": 0, 488 | "rect": { 489 | "y": 0, 490 | "x": 6, 491 | "width": 72, 492 | "height": 19 493 | }, 494 | "_id": "5cb69a783fdf9e400233475f", 495 | "absoluteRect": { 496 | "x": 922, 497 | "y": 53, 498 | "width": 72, 499 | "height": 19 500 | } 501 | } 502 | ], 503 | "blendMode": "normal", 504 | "interactionLevel": "bottom", 505 | "rect": { 506 | "y": 53, 507 | "x": 916, 508 | "width": 84, 509 | "height": 31 510 | }, 511 | "type": "group", 512 | "opacity": 1, 513 | "sourceId": "5CA70284-DF70-4553-9FEA-19FD728C0278", 514 | "fills": [ 515 | { 516 | "color": { 517 | "r": 151, 518 | "b": 151, 519 | "g": 145, 520 | "a": 1 521 | }, 522 | "fillType": "color", 523 | "blendMode": "normal", 524 | "opacity": 1 525 | } 526 | ], 527 | "exportable": false, 528 | "borders": [], 529 | "name": " Small", 530 | "shadows": [], 531 | "_id": "5cb69a783fdf9e400233475e", 532 | "absoluteRect": { 533 | "x": 916, 534 | "y": 53, 535 | "width": 84, 536 | "height": 31 537 | } 538 | }, 539 | { 540 | "borderRadius": 0, 541 | "componentName": "Header title / ", 542 | "rotation": 0, 543 | "layers": [ 544 | { 545 | "shadows": [], 546 | "name": "Title", 547 | "rotation": 0, 548 | "blendMode": "normal", 549 | "content": "Places", 550 | "type": "text", 551 | "opacity": 0.6, 552 | "sourceId": "4ED300F5-22D4-4466-8334-F45DEA93ABB4", 553 | "fills": [], 554 | "textStyles": [ 555 | { 556 | "range": { 557 | "location": 0, 558 | "length": 6 559 | }, 560 | "style": { 561 | "fontSize": 24, 562 | "color": { 563 | "r": 255, 564 | "b": 255, 565 | "g": 255, 566 | "a": 1 567 | }, 568 | "fontFace": "OpenSans-Bold" 569 | } 570 | } 571 | ], 572 | "exportable": false, 573 | "borders": [], 574 | "borderRadius": 0, 575 | "rect": { 576 | "y": 0, 577 | "x": 6, 578 | "width": 93, 579 | "height": 33 580 | }, 581 | "_id": "5cb69a783fdf9e4002334761", 582 | "absoluteRect": { 583 | "x": 166, 584 | "y": 39, 585 | "width": 93, 586 | "height": 33 587 | } 588 | } 589 | ], 590 | "blendMode": "normal", 591 | "interactionLevel": "bottom", 592 | "rect": { 593 | "y": 39, 594 | "x": 160, 595 | "width": 108, 596 | "height": 45 597 | }, 598 | "type": "group", 599 | "opacity": 1, 600 | "sourceId": "07BBA166-2A0D-44C3-A375-4F097D32540C", 601 | "fills": [ 602 | { 603 | "color": { 604 | "r": 151, 605 | "b": 151, 606 | "g": 145, 607 | "a": 1 608 | }, 609 | "fillType": "color", 610 | "blendMode": "normal", 611 | "opacity": 1 612 | } 613 | ], 614 | "exportable": false, 615 | "borders": [], 616 | "name": " ", 617 | "shadows": [], 618 | "_id": "5cb69a783fdf9e4002334760", 619 | "absoluteRect": { 620 | "x": 160, 621 | "y": 39, 622 | "width": 108, 623 | "height": 45 624 | } 625 | }, 626 | { 627 | "borderRadius": 0, 628 | "componentName": "Header title / Selected", 629 | "rotation": 0, 630 | "layers": [ 631 | { 632 | "shadows": [], 633 | "name": "Title", 634 | "rotation": 0, 635 | "blendMode": "normal", 636 | "content": "Guides", 637 | "type": "text", 638 | "opacity": 1, 639 | "sourceId": "6DC1D1B0-A8E6-4F42-A34A-7C5D938A1107", 640 | "fills": [], 641 | "textStyles": [ 642 | { 643 | "range": { 644 | "location": 0, 645 | "length": 6 646 | }, 647 | "style": { 648 | "fontSize": 24, 649 | "color": { 650 | "r": 255, 651 | "b": 255, 652 | "g": 255, 653 | "a": 1 654 | }, 655 | "fontFace": "OpenSans-Bold" 656 | } 657 | } 658 | ], 659 | "exportable": false, 660 | "borders": [], 661 | "borderRadius": 0, 662 | "rect": { 663 | "y": 0, 664 | "x": 6, 665 | "width": 93, 666 | "height": 33 667 | }, 668 | "_id": "5cb69a783fdf9e4002334763", 669 | "absoluteRect": { 670 | "x": 58, 671 | "y": 39, 672 | "width": 93, 673 | "height": 33 674 | } 675 | } 676 | ], 677 | "blendMode": "normal", 678 | "interactionLevel": "bottom", 679 | "rect": { 680 | "y": 39, 681 | "x": 52, 682 | "width": 108, 683 | "height": 45 684 | }, 685 | "type": "group", 686 | "opacity": 1, 687 | "sourceId": "02EFBB68-FE0B-420B-8E25-A0C039C441D0", 688 | "fills": [ 689 | { 690 | "color": { 691 | "r": 151, 692 | "b": 151, 693 | "g": 145, 694 | "a": 1 695 | }, 696 | "fillType": "color", 697 | "blendMode": "normal", 698 | "opacity": 1 699 | } 700 | ], 701 | "exportable": false, 702 | "borders": [], 703 | "name": " Selected", 704 | "shadows": [], 705 | "_id": "5cb69a783fdf9e4002334762", 706 | "absoluteRect": { 707 | "x": 52, 708 | "y": 39, 709 | "width": 108, 710 | "height": 45 711 | } 712 | } 713 | ], 714 | "componentNames": [ 715 | "Header / Title / Small", 716 | "Header bg / Orange", 717 | "Header title / ", 718 | "Header title / Selected", 719 | "Icons / Logo" 720 | ], 721 | "width": 1024, 722 | "links": [], 723 | "url": "https://placekitten.com/1024/84" 724 | } 725 | } 726 | }, 727 | { 728 | "name": "Search field", 729 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lacus magna, elementum vel orci a, ornare molestie augue. Fusce semper pharetra augue ac condimentum. Suspendisse eget varius nulla. Vivamus feugiat ligula nec volutpat tincidunt. Curabitur iaculis purus convallis, scelerisque nulla vitae, condimentum ante. Curabitur vehicula nunc in massa congue, quis varius metus placerat. Integer feugiat, orci eu sagittis interdum, tortor nulla pulvinar mauris, egestas suscipit eros orci sit amet sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", 730 | "latestVersion": { 731 | "snapshot": { 732 | "assets": [ 733 | { 734 | "layerId": "B1168488-6DDF-4E56-B8F7-00ABC3FCCEDF", 735 | "layerName": "icSearch", 736 | "displayName": "ic-search", 737 | "contents": [ 738 | { 739 | "url": "https://placekitten.com/200/299", 740 | "density": "3x", 741 | "format": "png" 742 | }, 743 | { 744 | "url": "https://placekitten.com/200/299", 745 | "density": "2x", 746 | "format": "png" 747 | }, 748 | { 749 | "url": "https://placekitten.com/200/299", 750 | "density": "1x", 751 | "format": "png" 752 | }, 753 | { 754 | "url": "https://placekitten.com/200/299", 755 | "density": "1x", 756 | "format": "svg", 757 | "optimized": { 758 | "url": "https://placekitten.com/200/299", 759 | "density": "1x", 760 | "format": "svg", 761 | "percent": 76 762 | } 763 | } 764 | ], 765 | "_id": "5cb69aad3fdf9e400233476d", 766 | "size": { 767 | "width": 16, 768 | "height": 16 769 | }, 770 | "lightness": "dark" 771 | } 772 | ], 773 | "source": "sketch", 774 | "height": 36, 775 | "layers": [ 776 | { 777 | "borderRadius": 6, 778 | "shadows": [], 779 | "rotation": 0, 780 | "blendMode": "normal", 781 | "type": "shape", 782 | "opacity": 1, 783 | "sourceId": "C7734431-6C95-466F-9FED-75E060D0A2E5", 784 | "fills": [ 785 | { 786 | "color": { 787 | "r": 255, 788 | "b": 255, 789 | "g": 255, 790 | "a": 1 791 | }, 792 | "opacity": 1, 793 | "fillType": "color" 794 | } 795 | ], 796 | "exportable": false, 797 | "borders": [ 798 | { 799 | "position": "inside", 800 | "fillType": "color", 801 | "thickness": 1, 802 | "opacity": 1, 803 | "color": { 804 | "r": 237, 805 | "b": 237, 806 | "g": 236, 807 | "a": 1 808 | } 809 | } 810 | ], 811 | "name": "Search bar bg", 812 | "rect": { 813 | "y": 0, 814 | "x": 0, 815 | "width": 324, 816 | "height": 36 817 | }, 818 | "_id": "5cb69aad3fdf9e4002334769", 819 | "absoluteRect": { 820 | "x": 0, 821 | "y": 0, 822 | "width": 324, 823 | "height": 36 824 | } 825 | }, 826 | { 827 | "borderRadius": 0, 828 | "componentName": "icSearch", 829 | "rotation": 0, 830 | "layers": [ 831 | { 832 | "borderRadius": 0, 833 | "shadows": [], 834 | "rotation": 0, 835 | "blendMode": "normal", 836 | "type": "shape", 837 | "opacity": 1, 838 | "sourceId": "67839DE5-4907-4CF4-99C9-F84BDFF7359B", 839 | "fills": [ 840 | { 841 | "color": { 842 | "r": 193, 843 | "b": 193, 844 | "g": 190, 845 | "a": 1 846 | }, 847 | "opacity": 1, 848 | "fillType": "color" 849 | } 850 | ], 851 | "exportable": false, 852 | "borders": [], 853 | "name": "icSearch", 854 | "rect": { 855 | "y": 0, 856 | "x": 0, 857 | "width": 16, 858 | "height": 16 859 | }, 860 | "_id": "5cb69aad3fdf9e400233476b", 861 | "absoluteRect": { 862 | "x": 10, 863 | "y": 10, 864 | "width": 16, 865 | "height": 16 866 | } 867 | } 868 | ], 869 | "blendMode": "normal", 870 | "interactionLevel": "top", 871 | "rect": { 872 | "y": 10, 873 | "x": 10, 874 | "width": 16, 875 | "height": 16 876 | }, 877 | "type": "group", 878 | "opacity": 1, 879 | "sourceId": "B1168488-6DDF-4E56-B8F7-00ABC3FCCEDF", 880 | "fills": [], 881 | "exportable": true, 882 | "borders": [], 883 | "name": "icSearch", 884 | "shadows": [], 885 | "_id": "5cb69aad3fdf9e400233476a", 886 | "absoluteRect": { 887 | "x": 10, 888 | "y": 10, 889 | "width": 16, 890 | "height": 16 891 | } 892 | }, 893 | { 894 | "shadows": [], 895 | "name": "Search for a…", 896 | "rotation": 0, 897 | "blendMode": "normal", 898 | "content": "Search for a…", 899 | "type": "text", 900 | "opacity": 1, 901 | "sourceId": "B89E4C60-77F1-48CE-8562-135D690E886A", 902 | "fills": [], 903 | "textStyles": [ 904 | { 905 | "range": { 906 | "location": 0, 907 | "length": 13 908 | }, 909 | "style": { 910 | "fontSize": 18, 911 | "color": { 912 | "r": 193, 913 | "b": 193, 914 | "g": 190, 915 | "a": 1 916 | }, 917 | "fontFace": "OpenSans" 918 | } 919 | } 920 | ], 921 | "exportable": false, 922 | "borders": [], 923 | "borderRadius": 0, 924 | "rect": { 925 | "y": 6, 926 | "x": 32, 927 | "width": 268, 928 | "height": 24 929 | }, 930 | "_id": "5cb69aad3fdf9e400233476c", 931 | "absoluteRect": { 932 | "x": 32, 933 | "y": 6, 934 | "width": 268, 935 | "height": 24 936 | } 937 | } 938 | ], 939 | "componentNames": [ 940 | "icSearch" 941 | ], 942 | "width": 324, 943 | "links": [], 944 | "url": "https://placekitten.com/324/36" 945 | } 946 | } 947 | } 948 | ] -------------------------------------------------------------------------------- /src/template/tests/fixtures/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Header", 4 | "description": "", 5 | "sourceId": "95b6fa48-decd-44f2-b878-ba3d2fbf25df", 6 | "latestVersion": { 7 | "snapshot": { 8 | "assets": [ 9 | { 10 | "layerId": "7E3861F2-458C-4917-A698-962F2D05F508", 11 | "layerName": " Logo", 12 | "displayName": "logo", 13 | "contents": [ 14 | { 15 | "url": "https://placekitten.com/200/299", 16 | "density": "1x", 17 | "format": "png" 18 | }, 19 | { 20 | "url": "https://placekitten.com/200/299", 21 | "density": "1x", 22 | "format": "svg", 23 | "optimized": { 24 | "url": "https://placekitten.com/200/299", 25 | "density": "1x", 26 | "format": "svg", 27 | "percent": 68 28 | } 29 | }, 30 | { 31 | "url": "https://placekitten.com/200/299", 32 | "density": "2x", 33 | "format": "png" 34 | }, 35 | { 36 | "url": "https://placekitten.com/200/299", 37 | "density": "3x", 38 | "format": "png" 39 | } 40 | ], 41 | "_id": "5cb69a783fdf9e4002334764", 42 | "size": { 43 | "width": 32, 44 | "height": 32 45 | }, 46 | "lightness": "dark" 47 | } 48 | ], 49 | "source": "sketch", 50 | "height": 84, 51 | "layers": [ 52 | { 53 | "borderRadius": 0, 54 | "componentName": "Header bg / Orange", 55 | "rotation": 0, 56 | "layers": [], 57 | "blendMode": "normal", 58 | "interactionLevel": "bottom", 59 | "rect": { 60 | "y": 0, 61 | "x": 0, 62 | "width": 1024, 63 | "height": 84 64 | }, 65 | "type": "group", 66 | "opacity": 1, 67 | "sourceId": "9B5912B0-9043-450E-9830-DFB71382B34B", 68 | "fills": [ 69 | { 70 | "color": { 71 | "r": 253, 72 | "b": 57, 73 | "g": 189, 74 | "a": 1 75 | }, 76 | "fillType": "color", 77 | "blendMode": "normal", 78 | "opacity": 1 79 | } 80 | ], 81 | "exportable": false, 82 | "borders": [], 83 | "name": " Orange", 84 | "shadows": [], 85 | "_id": "5cb69a783fdf9e4002334753", 86 | "absoluteRect": { 87 | "x": 0, 88 | "y": 0, 89 | "width": 1024, 90 | "height": 84 91 | } 92 | }, 93 | { 94 | "borderRadius": 0, 95 | "componentName": "Icons / Logo", 96 | "rotation": 0, 97 | "layers": [ 98 | { 99 | "borders": [], 100 | "rect": { 101 | "y": 0, 102 | "x": 0, 103 | "width": 32, 104 | "height": 32 105 | }, 106 | "rotation": 0, 107 | "layers": [ 108 | { 109 | "borderRadius": 0, 110 | "shadows": [], 111 | "rotation": 0, 112 | "blendMode": "normal", 113 | "type": "shape", 114 | "opacity": 1, 115 | "sourceId": "25B1FD06-178F-4901-B533-49D6D602D97F", 116 | "fills": [ 117 | { 118 | "color": { 119 | "r": 255, 120 | "b": 255, 121 | "g": 255, 122 | "a": 1 123 | }, 124 | "opacity": 1, 125 | "fillType": "color" 126 | } 127 | ], 128 | "exportable": false, 129 | "borders": [], 130 | "name": "Oval", 131 | "rect": { 132 | "y": 0, 133 | "x": 0, 134 | "width": 32, 135 | "height": 32 136 | }, 137 | "_id": "5cb69a783fdf9e4002334756", 138 | "absoluteRect": { 139 | "x": 12, 140 | "y": 40, 141 | "width": 32, 142 | "height": 32 143 | } 144 | }, 145 | { 146 | "borders": [], 147 | "rect": { 148 | "y": 1.038961038961039, 149 | "x": 1.038961038961039, 150 | "width": 29.92207792207792, 151 | "height": 29.92207792207792 152 | }, 153 | "rotation": 0, 154 | "layers": [ 155 | { 156 | "borderRadius": 0, 157 | "shadows": [], 158 | "rotation": 0, 159 | "blendMode": "normal", 160 | "type": "shape", 161 | "opacity": 1, 162 | "sourceId": "090685FD-58E7-4771-AA1C-CC95CB5A51C6", 163 | "fills": [ 164 | { 165 | "color": { 166 | "r": 238, 167 | "b": 35, 168 | "g": 103, 169 | "a": 1 170 | }, 171 | "opacity": 1, 172 | "fillType": "color" 173 | } 174 | ], 175 | "exportable": false, 176 | "borders": [], 177 | "name": "Oval 1", 178 | "rect": { 179 | "y": 14.96103896103896, 180 | "x": 0, 181 | "width": 29.9220779220779, 182 | "height": 14.96103896103896 183 | }, 184 | "_id": "5cb69a783fdf9e4002334758", 185 | "absoluteRect": { 186 | "x": 13.03896103896104, 187 | "y": 56, 188 | "width": 29.9220779220779, 189 | "height": 14.96103896103896 190 | } 191 | }, 192 | { 193 | "borderRadius": 0, 194 | "shadows": [], 195 | "rotation": 0, 196 | "blendMode": "normal", 197 | "type": "shape", 198 | "opacity": 1, 199 | "sourceId": "046464D9-46C9-435D-829F-11A89045F225", 200 | "fills": [ 201 | { 202 | "color": { 203 | "r": 253, 204 | "b": 57, 205 | "g": 189, 206 | "a": 1 207 | }, 208 | "opacity": 1, 209 | "fillType": "color" 210 | } 211 | ], 212 | "exportable": false, 213 | "borders": [], 214 | "name": "Oval 1 Copy", 215 | "rect": { 216 | "y": 0, 217 | "x": 0, 218 | "width": 29.92207792207792, 219 | "height": 14.96103896103896 220 | }, 221 | "_id": "5cb69a783fdf9e4002334759", 222 | "absoluteRect": { 223 | "x": 13.03896103896104, 224 | "y": 41.03896103896104, 225 | "width": 29.92207792207792, 226 | "height": 14.96103896103896 227 | } 228 | }, 229 | { 230 | "borderRadius": 0, 231 | "shadows": [], 232 | "rotation": 0, 233 | "blendMode": "normal", 234 | "type": "shape", 235 | "opacity": 1, 236 | "sourceId": "FD5F8BED-E602-4876-AE7A-7D4073DFBAD0", 237 | "fills": [ 238 | { 239 | "color": { 240 | "r": 254, 241 | "b": 51, 242 | "g": 207, 243 | "a": 1 244 | }, 245 | "opacity": 1, 246 | "fillType": "color" 247 | } 248 | ], 249 | "exportable": false, 250 | "borders": [], 251 | "name": "Oval 2 Copy", 252 | "rect": { 253 | "y": 14.96103896103896, 254 | "x": 0, 255 | "width": 29.92207792207792, 256 | "height": 9.044243891701518 257 | }, 258 | "_id": "5cb69a783fdf9e400233475a", 259 | "absoluteRect": { 260 | "x": 13.03896103896104, 261 | "y": 56, 262 | "width": 29.92207792207792, 263 | "height": 9.044243891701518 264 | } 265 | }, 266 | { 267 | "borderRadius": 0, 268 | "shadows": [], 269 | "rotation": 0, 270 | "blendMode": "normal", 271 | "type": "shape", 272 | "opacity": 1, 273 | "sourceId": "4048E04C-29EC-4D04-AC15-8F9C0F386442", 274 | "fills": [ 275 | { 276 | "color": { 277 | "r": 246, 278 | "b": 51, 279 | "g": 152, 280 | "a": 1 281 | }, 282 | "opacity": 1, 283 | "fillType": "color" 284 | } 285 | ], 286 | "exportable": false, 287 | "borders": [], 288 | "name": "Oval 2", 289 | "rect": { 290 | "y": 6.170372000880511, 291 | "x": 0, 292 | "width": 29.92207792207792, 293 | "height": 8.790666960158482 294 | }, 295 | "_id": "5cb69a783fdf9e400233475b", 296 | "absoluteRect": { 297 | "x": 13.03896103896104, 298 | "y": 47.20933303984155, 299 | "width": 29.92207792207792, 300 | "height": 8.790666960158482 301 | } 302 | } 303 | ], 304 | "blendMode": "normal", 305 | "type": "group", 306 | "opacity": 1, 307 | "sourceId": "F1DD6B18-4003-41ED-BC03-23F0034A60F9", 308 | "fills": [], 309 | "exportable": false, 310 | "borderRadius": 0, 311 | "name": "Group", 312 | "shadows": [], 313 | "_id": "5cb69a783fdf9e4002334757", 314 | "absoluteRect": { 315 | "x": 13.03896103896104, 316 | "y": 41.03896103896104, 317 | "width": 29.92207792207792, 318 | "height": 29.92207792207792 319 | } 320 | } 321 | ], 322 | "blendMode": "normal", 323 | "type": "group", 324 | "opacity": 1, 325 | "sourceId": "D45E7D12-7EAA-4846-8A81-89EB701B56A2", 326 | "fills": [], 327 | "exportable": false, 328 | "borderRadius": 0, 329 | "name": "Group 13", 330 | "shadows": [], 331 | "_id": "5cb69a783fdf9e4002334755", 332 | "absoluteRect": { 333 | "x": 12, 334 | "y": 40, 335 | "width": 32, 336 | "height": 32 337 | } 338 | } 339 | ], 340 | "blendMode": "normal", 341 | "interactionLevel": "top", 342 | "rect": { 343 | "y": 40, 344 | "x": 12, 345 | "width": 32, 346 | "height": 32 347 | }, 348 | "type": "group", 349 | "opacity": 1, 350 | "sourceId": "7E3861F2-458C-4917-A698-962F2D05F508", 351 | "fills": [], 352 | "exportable": true, 353 | "borders": [], 354 | "name": " Logo", 355 | "shadows": [], 356 | "_id": "5cb69a783fdf9e4002334754", 357 | "absoluteRect": { 358 | "x": 12, 359 | "y": 40, 360 | "width": 32, 361 | "height": 32 362 | } 363 | }, 364 | { 365 | "borderRadius": 0, 366 | "componentName": "Header / Title / Small", 367 | "rotation": 0, 368 | "layers": [ 369 | { 370 | "shadows": [], 371 | "name": "Title", 372 | "rotation": 0, 373 | "blendMode": "normal", 374 | "content": "Title", 375 | "type": "text", 376 | "opacity": 1, 377 | "sourceId": "816DF671-F304-45AD-B457-94D65357A553", 378 | "fills": [], 379 | "textStyles": [ 380 | { 381 | "range": { 382 | "location": 0, 383 | "length": 5 384 | }, 385 | "style": { 386 | "fontSize": 14, 387 | "fontFace": "OpenSans-Bold", 388 | "textAlign": "center", 389 | "color": { 390 | "r": 255, 391 | "b": 255, 392 | "g": 255, 393 | "a": 1 394 | } 395 | } 396 | } 397 | ], 398 | "exportable": false, 399 | "borders": [], 400 | "borderRadius": 0, 401 | "rect": { 402 | "y": 0, 403 | "x": 6, 404 | "width": 72, 405 | "height": 19 406 | }, 407 | "_id": "5cb69a783fdf9e400233475d", 408 | "absoluteRect": { 409 | "x": 838, 410 | "y": 53, 411 | "width": 72, 412 | "height": 19 413 | } 414 | } 415 | ], 416 | "blendMode": "normal", 417 | "interactionLevel": "bottom", 418 | "rect": { 419 | "y": 53, 420 | "x": 832, 421 | "width": 84, 422 | "height": 31 423 | }, 424 | "type": "group", 425 | "opacity": 1, 426 | "sourceId": "5CA70284-DF70-4553-9FEA-19FD728C0278", 427 | "fills": [ 428 | { 429 | "color": { 430 | "r": 151, 431 | "b": 151, 432 | "g": 145, 433 | "a": 1 434 | }, 435 | "fillType": "color", 436 | "blendMode": "normal", 437 | "opacity": 1 438 | } 439 | ], 440 | "exportable": false, 441 | "borders": [], 442 | "name": " Small", 443 | "shadows": [], 444 | "_id": "5cb69a783fdf9e400233475c", 445 | "absoluteRect": { 446 | "x": 832, 447 | "y": 53, 448 | "width": 84, 449 | "height": 31 450 | } 451 | }, 452 | { 453 | "borderRadius": 0, 454 | "componentName": "Header / Title / Small", 455 | "rotation": 0, 456 | "layers": [ 457 | { 458 | "shadows": [], 459 | "name": "Title", 460 | "rotation": 0, 461 | "blendMode": "normal", 462 | "content": "Title", 463 | "type": "text", 464 | "opacity": 1, 465 | "sourceId": "816DF671-F304-45AD-B457-94D65357A553", 466 | "fills": [], 467 | "textStyles": [ 468 | { 469 | "range": { 470 | "location": 0, 471 | "length": 5 472 | }, 473 | "style": { 474 | "fontSize": 14, 475 | "fontFace": "OpenSans-Bold", 476 | "textAlign": "center", 477 | "color": { 478 | "r": 255, 479 | "b": 255, 480 | "g": 255, 481 | "a": 1 482 | } 483 | } 484 | } 485 | ], 486 | "exportable": false, 487 | "borders": [], 488 | "borderRadius": 0, 489 | "rect": { 490 | "y": 0, 491 | "x": 6, 492 | "width": 72, 493 | "height": 19 494 | }, 495 | "_id": "5cb69a783fdf9e400233475f", 496 | "absoluteRect": { 497 | "x": 922, 498 | "y": 53, 499 | "width": 72, 500 | "height": 19 501 | } 502 | } 503 | ], 504 | "blendMode": "normal", 505 | "interactionLevel": "bottom", 506 | "rect": { 507 | "y": 53, 508 | "x": 916, 509 | "width": 84, 510 | "height": 31 511 | }, 512 | "type": "group", 513 | "opacity": 1, 514 | "sourceId": "5CA70284-DF70-4553-9FEA-19FD728C0278", 515 | "fills": [ 516 | { 517 | "color": { 518 | "r": 151, 519 | "b": 151, 520 | "g": 145, 521 | "a": 1 522 | }, 523 | "fillType": "color", 524 | "blendMode": "normal", 525 | "opacity": 1 526 | } 527 | ], 528 | "exportable": false, 529 | "borders": [], 530 | "name": " Small", 531 | "shadows": [], 532 | "_id": "5cb69a783fdf9e400233475e", 533 | "absoluteRect": { 534 | "x": 916, 535 | "y": 53, 536 | "width": 84, 537 | "height": 31 538 | } 539 | }, 540 | { 541 | "borderRadius": 0, 542 | "componentName": "Header title / ", 543 | "rotation": 0, 544 | "layers": [ 545 | { 546 | "shadows": [], 547 | "name": "Title", 548 | "rotation": 0, 549 | "blendMode": "normal", 550 | "content": "Places", 551 | "type": "text", 552 | "opacity": 0.6, 553 | "sourceId": "4ED300F5-22D4-4466-8334-F45DEA93ABB4", 554 | "fills": [], 555 | "textStyles": [ 556 | { 557 | "range": { 558 | "location": 0, 559 | "length": 6 560 | }, 561 | "style": { 562 | "fontSize": 24, 563 | "color": { 564 | "r": 255, 565 | "b": 255, 566 | "g": 255, 567 | "a": 1 568 | }, 569 | "fontFace": "OpenSans-Bold" 570 | } 571 | } 572 | ], 573 | "exportable": false, 574 | "borders": [], 575 | "borderRadius": 0, 576 | "rect": { 577 | "y": 0, 578 | "x": 6, 579 | "width": 93, 580 | "height": 33 581 | }, 582 | "_id": "5cb69a783fdf9e4002334761", 583 | "absoluteRect": { 584 | "x": 166, 585 | "y": 39, 586 | "width": 93, 587 | "height": 33 588 | } 589 | } 590 | ], 591 | "blendMode": "normal", 592 | "interactionLevel": "bottom", 593 | "rect": { 594 | "y": 39, 595 | "x": 160, 596 | "width": 108, 597 | "height": 45 598 | }, 599 | "type": "group", 600 | "opacity": 1, 601 | "sourceId": "07BBA166-2A0D-44C3-A375-4F097D32540C", 602 | "fills": [ 603 | { 604 | "color": { 605 | "r": 151, 606 | "b": 151, 607 | "g": 145, 608 | "a": 1 609 | }, 610 | "fillType": "color", 611 | "blendMode": "normal", 612 | "opacity": 1 613 | } 614 | ], 615 | "exportable": false, 616 | "borders": [], 617 | "name": " ", 618 | "shadows": [], 619 | "_id": "5cb69a783fdf9e4002334760", 620 | "absoluteRect": { 621 | "x": 160, 622 | "y": 39, 623 | "width": 108, 624 | "height": 45 625 | } 626 | }, 627 | { 628 | "borderRadius": 0, 629 | "componentName": "Header title / Selected", 630 | "rotation": 0, 631 | "layers": [ 632 | { 633 | "shadows": [], 634 | "name": "Title", 635 | "rotation": 0, 636 | "blendMode": "normal", 637 | "content": "Guides", 638 | "type": "text", 639 | "opacity": 1, 640 | "sourceId": "6DC1D1B0-A8E6-4F42-A34A-7C5D938A1107", 641 | "fills": [], 642 | "textStyles": [ 643 | { 644 | "range": { 645 | "location": 0, 646 | "length": 6 647 | }, 648 | "style": { 649 | "fontSize": 24, 650 | "color": { 651 | "r": 255, 652 | "b": 255, 653 | "g": 255, 654 | "a": 1 655 | }, 656 | "fontFace": "OpenSans-Bold" 657 | } 658 | } 659 | ], 660 | "exportable": false, 661 | "borders": [], 662 | "borderRadius": 0, 663 | "rect": { 664 | "y": 0, 665 | "x": 6, 666 | "width": 93, 667 | "height": 33 668 | }, 669 | "_id": "5cb69a783fdf9e4002334763", 670 | "absoluteRect": { 671 | "x": 58, 672 | "y": 39, 673 | "width": 93, 674 | "height": 33 675 | } 676 | } 677 | ], 678 | "blendMode": "normal", 679 | "interactionLevel": "bottom", 680 | "rect": { 681 | "y": 39, 682 | "x": 52, 683 | "width": 108, 684 | "height": 45 685 | }, 686 | "type": "group", 687 | "opacity": 1, 688 | "sourceId": "02EFBB68-FE0B-420B-8E25-A0C039C441D0", 689 | "fills": [ 690 | { 691 | "color": { 692 | "r": 151, 693 | "b": 151, 694 | "g": 145, 695 | "a": 1 696 | }, 697 | "fillType": "color", 698 | "blendMode": "normal", 699 | "opacity": 1 700 | } 701 | ], 702 | "exportable": false, 703 | "borders": [], 704 | "name": " Selected", 705 | "shadows": [], 706 | "_id": "5cb69a783fdf9e4002334762", 707 | "absoluteRect": { 708 | "x": 52, 709 | "y": 39, 710 | "width": 108, 711 | "height": 45 712 | } 713 | } 714 | ], 715 | "componentNames": [ 716 | "Header / Title / Small", 717 | "Header bg / Orange", 718 | "Header title / ", 719 | "Header title / Selected", 720 | "Icons / Logo" 721 | ], 722 | "width": 1024, 723 | "links": [], 724 | "url": "https://placekitten.com/1024/84" 725 | } 726 | } 727 | }, 728 | { 729 | "name": "Search field", 730 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lacus magna, elementum vel orci a, ornare molestie augue. Fusce semper pharetra augue ac condimentum. Suspendisse eget varius nulla. Vivamus feugiat ligula nec volutpat tincidunt. Curabitur iaculis purus convallis, scelerisque nulla vitae, condimentum ante. Curabitur vehicula nunc in massa congue, quis varius metus placerat. Integer feugiat, orci eu sagittis interdum, tortor nulla pulvinar mauris, egestas suscipit eros orci sit amet sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", 731 | "sourceId": "95b6fa48-decd-44f2-b878-ba3d2fbf25df", 732 | "latestVersion": { 733 | "snapshot": { 734 | "assets": [ 735 | { 736 | "layerId": "B1168488-6DDF-4E56-B8F7-00ABC3FCCEDF", 737 | "layerName": "icSearch", 738 | "displayName": "ic-search", 739 | "contents": [ 740 | { 741 | "url": "https://placekitten.com/200/299", 742 | "density": "3x", 743 | "format": "png" 744 | }, 745 | { 746 | "url": "https://placekitten.com/200/299", 747 | "density": "2x", 748 | "format": "png" 749 | }, 750 | { 751 | "url": "https://placekitten.com/200/299", 752 | "density": "1x", 753 | "format": "png" 754 | }, 755 | { 756 | "url": "https://placekitten.com/200/299", 757 | "density": "1x", 758 | "format": "svg", 759 | "optimized": { 760 | "url": "https://placekitten.com/200/299", 761 | "density": "1x", 762 | "format": "svg", 763 | "percent": 76 764 | } 765 | } 766 | ], 767 | "_id": "5cb69aad3fdf9e400233476d", 768 | "size": { 769 | "width": 16, 770 | "height": 16 771 | }, 772 | "lightness": "dark" 773 | } 774 | ], 775 | "source": "sketch", 776 | "height": 36, 777 | "layers": [ 778 | { 779 | "borderRadius": 6, 780 | "shadows": [], 781 | "rotation": 0, 782 | "blendMode": "normal", 783 | "type": "shape", 784 | "opacity": 1, 785 | "sourceId": "C7734431-6C95-466F-9FED-75E060D0A2E5", 786 | "fills": [ 787 | { 788 | "color": { 789 | "r": 255, 790 | "b": 255, 791 | "g": 255, 792 | "a": 1 793 | }, 794 | "opacity": 1, 795 | "fillType": "color" 796 | } 797 | ], 798 | "exportable": false, 799 | "borders": [ 800 | { 801 | "position": "inside", 802 | "fillType": "color", 803 | "thickness": 1, 804 | "opacity": 1, 805 | "color": { 806 | "r": 237, 807 | "b": 237, 808 | "g": 236, 809 | "a": 1 810 | } 811 | } 812 | ], 813 | "name": "Search bar bg", 814 | "rect": { 815 | "y": 0, 816 | "x": 0, 817 | "width": 324, 818 | "height": 36 819 | }, 820 | "_id": "5cb69aad3fdf9e4002334769", 821 | "absoluteRect": { 822 | "x": 0, 823 | "y": 0, 824 | "width": 324, 825 | "height": 36 826 | } 827 | }, 828 | { 829 | "borderRadius": 0, 830 | "componentName": "icSearch", 831 | "rotation": 0, 832 | "layers": [ 833 | { 834 | "borderRadius": 0, 835 | "shadows": [], 836 | "rotation": 0, 837 | "blendMode": "normal", 838 | "type": "shape", 839 | "opacity": 1, 840 | "sourceId": "67839DE5-4907-4CF4-99C9-F84BDFF7359B", 841 | "fills": [ 842 | { 843 | "color": { 844 | "r": 193, 845 | "b": 193, 846 | "g": 190, 847 | "a": 1 848 | }, 849 | "opacity": 1, 850 | "fillType": "color" 851 | } 852 | ], 853 | "exportable": false, 854 | "borders": [], 855 | "name": "icSearch", 856 | "rect": { 857 | "y": 0, 858 | "x": 0, 859 | "width": 16, 860 | "height": 16 861 | }, 862 | "_id": "5cb69aad3fdf9e400233476b", 863 | "absoluteRect": { 864 | "x": 10, 865 | "y": 10, 866 | "width": 16, 867 | "height": 16 868 | } 869 | } 870 | ], 871 | "blendMode": "normal", 872 | "interactionLevel": "top", 873 | "rect": { 874 | "y": 10, 875 | "x": 10, 876 | "width": 16, 877 | "height": 16 878 | }, 879 | "type": "group", 880 | "opacity": 1, 881 | "sourceId": "B1168488-6DDF-4E56-B8F7-00ABC3FCCEDF", 882 | "fills": [], 883 | "exportable": true, 884 | "borders": [], 885 | "name": "icSearch", 886 | "shadows": [], 887 | "_id": "5cb69aad3fdf9e400233476a", 888 | "absoluteRect": { 889 | "x": 10, 890 | "y": 10, 891 | "width": 16, 892 | "height": 16 893 | } 894 | }, 895 | { 896 | "shadows": [], 897 | "name": "Search for a…", 898 | "rotation": 0, 899 | "blendMode": "normal", 900 | "content": "Search for a…", 901 | "type": "text", 902 | "opacity": 1, 903 | "sourceId": "B89E4C60-77F1-48CE-8562-135D690E886A", 904 | "fills": [], 905 | "textStyles": [ 906 | { 907 | "range": { 908 | "location": 0, 909 | "length": 13 910 | }, 911 | "style": { 912 | "fontSize": 18, 913 | "color": { 914 | "r": 193, 915 | "b": 193, 916 | "g": 190, 917 | "a": 1 918 | }, 919 | "fontFace": "OpenSans" 920 | } 921 | } 922 | ], 923 | "exportable": false, 924 | "borders": [], 925 | "borderRadius": 0, 926 | "rect": { 927 | "y": 6, 928 | "x": 32, 929 | "width": 268, 930 | "height": 24 931 | }, 932 | "_id": "5cb69aad3fdf9e400233476c", 933 | "absoluteRect": { 934 | "x": 32, 935 | "y": 6, 936 | "width": 268, 937 | "height": 24 938 | } 939 | } 940 | ], 941 | "componentNames": [ 942 | "icSearch" 943 | ], 944 | "width": 324, 945 | "links": [], 946 | "url": "https://placekitten.com/324/36" 947 | } 948 | } 949 | } 950 | ] --------------------------------------------------------------------------------