├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── banner.png ├── changelog.txt ├── description.txt └── icon.png ├── code.js ├── code.test.ts ├── code.ts ├── figma.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── tsconfig.json └── ui.html /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: npm i 17 | - run: npm run build 18 | - run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Leonard Thomas 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | 4 | 5 | ## Installation 6 | 7 | https://www.figma.com/community/plugin/820311256083321341 8 | 9 | ## Usage 10 | Use text to create a timer on a Figma page. 11 | 12 | There are two ways to start a timer: 13 | 1. Type the time you want to count down in **Timer: HH:MM:SS** format (e.g. Timer: 5:00), then press 'Start'. 14 | 2. Type the time you want to count down in **HH:MM:SS** format (e.g. 5:00). In this case you need to select the text layer before pressing 'Start' 15 | 16 | Additionally, you can now start a couple of timers on a page (for whatever reason). 17 | 18 | ## Development 19 | 20 | 1. This plugin uses Typescript & Visual Studio Code (https://code.visualstudio.com/). 21 | To install typescript execute: 22 | 23 | ``` 24 | $ sudo npm install -g typescript 25 | ``` 26 | 27 | 2. Open the directory in Visual Studio Code and perform your changes in the code.ts file 28 | 3. Run the "Terminal > Run Build Task..." menu item, then select "tsc: watch - tsconfig.json", to compile the JavaScript source. 29 | 30 | ___ 31 | 32 | Created by [Viktoriia Leontieva](https://twitter.com/killnicole), [Bernd Plontsch](https://twitter.com/berndplontsch), [Leonard Thomas](https://twitter.com/_leotho) & [Jannes Peters](https://twitter.com/jannespeters) 33 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/Figma-Timer/69980ada6f41fea0ec78231ee03acf3427e04c2b/assets/banner.png -------------------------------------------------------------------------------- /assets/changelog.txt: -------------------------------------------------------------------------------- 1 | Version 3: 2 | It is now possible to pause and reset timers. 3 | Thanks to Jannes (🐦@jannespeters) for the contribution! 4 | 5 | We are always happy about feedback and suggestions! https://github.com/lennet/Figma-Timer/issues/new/ -------------------------------------------------------------------------------- /assets/description.txt: -------------------------------------------------------------------------------- 1 | Use text to create a timer on a Figma page. 2 | 3 | There are two ways to start a timer: 4 | 5 | 1. Type the time you want to count down in Timer: HH:MM:SS format (e.g. Timer: 5:00), then press 'Start'. 6 | 2. Type the time you want to count down in HH:MM:SS format (e.g. 5:00). In this case you need to select the text layer before pressing 'Start' 7 | 8 | Additionally, you can now start a couple of timers on a page (for whatever reason). 9 | 10 | Checkout Timebox ⏱ for a collection of Timers that work great with this plugin: https://www.figma.com/community/file/824677652393376493. 11 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/Figma-Timer/69980ada6f41fea0ec78231ee03acf3427e04c2b/assets/icon.png -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | const delay = ms => new Promise(res => setTimeout(res, ms)); 11 | var totalTimers = 0; 12 | const secondsSet = [86400, 3600, 60, 1]; 13 | var pause = false; 14 | var reset = false; 15 | var userSetSeconds = 0; 16 | const uiWindow = { 17 | minHeight: 60, 18 | maxHeight: 300, 19 | helptextHeight: 200, 20 | width: 220, 21 | }; 22 | var timerUIHeight = 50; 23 | if (figma.command === 'timer') { 24 | figma.showUI(__html__, { width: uiWindow.width, height: uiWindow.minHeight }); 25 | } 26 | figma.showUI(__html__, { width: uiWindow.width, height: uiWindow.minHeight }); 27 | figma.ui.onmessage = msg => { 28 | switch (msg.type) { 29 | case 'start': 30 | pause = false; 31 | reset = false; 32 | checkAndStart(); 33 | break; 34 | case 'pause': 35 | pause = true; 36 | break; 37 | case 'continue': 38 | pause = false; 39 | break; 40 | case 'reset': 41 | reset = true; 42 | pause = true; 43 | totalTimers = 0; 44 | figma.ui.resize(uiWindow.width, uiWindow.minHeight); 45 | break; 46 | case 'helpon': 47 | figma.ui.resize(uiWindow.width, uiWindow.helptextHeight); 48 | break; 49 | case 'helpoff': 50 | figma.ui.resize(uiWindow.width, uiWindow.minHeight); 51 | break; 52 | default: 53 | console.log("no code for msg.type: " + msg.type); 54 | break; 55 | } 56 | }; 57 | function checkAndStart() { 58 | if (checkForSelectedNodes() == false) { 59 | if (checkForNodesThatBeginWithTimer() == false) { 60 | throw new Error("Type the time to start Timer"); 61 | } 62 | } 63 | } 64 | function checkForSelectedNodes() { 65 | const regex = new RegExp("[0-9]{1,2}(:[0-9]{1,2})*"); 66 | const selectedNodes = figma.currentPage.selection.filter(node => node.type == "TEXT" && regex.test(node.characters)); 67 | selectedNodes.forEach(start); 68 | return selectedNodes.length > 0; 69 | } 70 | function checkForNodesThatBeginWithTimer() { 71 | const nodes = figma.currentPage.findAll(node => node.type === "TEXT" && node.characters.startsWith("Timer:")); 72 | nodes.forEach(start); 73 | return nodes.length > 0; 74 | } 75 | function start(node) { 76 | var timeString = node.characters; 77 | var startsWithTimer = false; 78 | if (timeString.startsWith("Timer:")) { 79 | timeString = timeString.replace("Timer: ", ""); 80 | startsWithTimer = true; 81 | } 82 | var seconds = getRemainingSeconds(timeString); 83 | var template = getTemplateFromString(timeString); 84 | startTimer(node, seconds, template, startsWithTimer); 85 | //set plugin relaunch data for easy launching/installing of plugin 86 | node.setRelaunchData({ timer: '' }); 87 | } 88 | function getRemainingSeconds(timeString) { 89 | var seconds = 0; 90 | var components = timeString.split(":"); 91 | secondsSet.reverse(); 92 | components.reverse().forEach((element, index) => { 93 | var factor = secondsSet[index]; 94 | seconds += factor * Number(element); 95 | }); 96 | secondsSet.reverse(); 97 | return seconds; 98 | } 99 | /** 100 | * Generates a template string from a timeString 101 | * e.g. converts 5:00 into 0:00 102 | */ 103 | function getTemplateFromString(timeString) { 104 | var result = ""; 105 | for (const c of timeString) { 106 | if (c == ":") { 107 | result += c; 108 | } 109 | else { 110 | result += "0"; 111 | } 112 | } 113 | return result; 114 | } 115 | /** 116 | * Creates a new time string that conforms to the templates format 117 | * e.g. 5:00 (timeString) and 00:00:00 (template) will return 00:05:00 118 | */ 119 | function fillUpTimeStringWithTemplate(timeString, template) { 120 | const trimmedTemplate = template.substring(0, template.length - timeString.length); 121 | return trimmedTemplate + timeString; 122 | } 123 | function secondsToInterval(seconds) { 124 | var result = ""; 125 | var secondsToGo = seconds; 126 | secondsSet.forEach((element) => { 127 | var count = Math.floor(secondsToGo / element); 128 | if (count > 0 || result.length > 0) { 129 | secondsToGo -= count * element; 130 | if (result.length > 0) { 131 | result += ":"; 132 | if (count < 10) { 133 | result += "0"; 134 | } 135 | } 136 | result += String(count); 137 | } 138 | }); 139 | return result; 140 | } 141 | /** 142 | * Code that updates all timers on the Figma stage 143 | * will also send updates / messages to UI.html, so we can show timers there 144 | */ 145 | function startTimer(node, seconds, template, startsWithTimer) { 146 | return __awaiter(this, void 0, void 0, function* () { 147 | yield figma.loadFontAsync(node.fontName); 148 | totalTimers += 1; 149 | console.log("Timer started / became active"); 150 | var timerID = totalTimers; 151 | var keepItRunning = true; 152 | var secondsToGo = seconds; 153 | var eventType = "start timer"; 154 | var newText = ""; 155 | adjustUIWindowHeight(); 156 | postMessageToUIWindow(eventType, newText, timerID, secondsToGo, seconds); 157 | // this loop updates all timers every second 158 | while (keepItRunning) { 159 | // checking if reset was clicked by user and if so resetting all timers 160 | if (reset) { 161 | newText = fillUpTimeStringWithTemplate(secondsToInterval(seconds), template); 162 | keepItRunning = false; 163 | updateTimerText(startsWithTimer, newText, node); 164 | } 165 | else if (!pause) { 166 | if (secondsToGo > 0) { 167 | newText = fillUpTimeStringWithTemplate(secondsToInterval(secondsToGo), template); 168 | eventType = "counting"; 169 | } 170 | else { 171 | newText = "Done"; 172 | eventType = "timer done"; 173 | } 174 | postMessageToUIWindow(eventType, newText, timerID, secondsToGo, seconds); 175 | updateTimerText(startsWithTimer, newText, node); 176 | secondsToGo -= 1; 177 | } 178 | yield delay(1000); 179 | } 180 | console.log("Timer finished / became in-active"); 181 | }); 182 | } 183 | function postMessageToUIWindow(eventType, timerText, timerID, secondsToGo, secondsToStart) { 184 | figma.ui.postMessage([eventType, timerText, timerID, secondsToGo, secondsToStart]); 185 | } 186 | function updateTimerText(startsWithTimer, newText, node) { 187 | if (startsWithTimer) { 188 | newText = "Timer: " + newText; 189 | } 190 | node.characters = newText; 191 | } 192 | // adjusting height of UI windows depending on amount of timers 193 | function adjustUIWindowHeight() { 194 | var newUIHeight = 100 + totalTimers * 50; 195 | if (newUIHeight > uiWindow.maxHeight) { 196 | newUIHeight = uiWindow.maxHeight; 197 | } 198 | figma.ui.resize(uiWindow.width, newUIHeight); 199 | } 200 | -------------------------------------------------------------------------------- /code.test.ts: -------------------------------------------------------------------------------- 1 | test('Placeholder', () => { 2 | expect(true).toBe(true); 3 | }); -------------------------------------------------------------------------------- /code.ts: -------------------------------------------------------------------------------- 1 | const delay = ms => new Promise(res => setTimeout(res, ms)); 2 | var totalTimers = 0; 3 | const secondsSet = [86400, 3600, 60, 1]; 4 | var pause = false; 5 | var reset = false; 6 | var userSetSeconds = 0; 7 | 8 | const uiWindow = { 9 | minHeight: 60, 10 | maxHeight: 300, 11 | helptextHeight: 200, 12 | width: 220, 13 | } 14 | 15 | var timerUIHeight = 50; 16 | 17 | if (figma.command === 'timer') { 18 | figma.showUI(__html__, { width: uiWindow.width, height: uiWindow.minHeight }) 19 | } 20 | 21 | figma.showUI(__html__, { width: uiWindow.width, height: uiWindow.minHeight }) 22 | 23 | figma.ui.onmessage = msg => { 24 | 25 | switch (msg.type) { 26 | 27 | case 'start': 28 | pause = false; 29 | reset = false; 30 | checkAndStart(); 31 | break; 32 | 33 | case 'pause': 34 | pause = true; 35 | break; 36 | 37 | case 'continue': 38 | pause = false; 39 | break; 40 | 41 | case 'reset': 42 | reset = true; 43 | pause = true; 44 | totalTimers = 0; 45 | figma.ui.resize(uiWindow.width, uiWindow.minHeight); 46 | break; 47 | 48 | case 'helpon': 49 | figma.ui.resize(uiWindow.width, uiWindow.helptextHeight); 50 | break; 51 | 52 | case 'helpoff': 53 | figma.ui.resize(uiWindow.width, uiWindow.minHeight); 54 | break; 55 | 56 | default: 57 | console.log("no code for msg.type: " + msg.type); 58 | break; 59 | 60 | } 61 | }; 62 | 63 | function checkAndStart() { 64 | if (checkForSelectedNodes() == false) { 65 | if (checkForNodesThatBeginWithTimer() == false) { 66 | throw new Error("Type the time to start Timer"); 67 | } 68 | } 69 | } 70 | 71 | function checkForSelectedNodes(): boolean { 72 | const regex = new RegExp("[0-9]{1,2}(:[0-9]{1,2})*"); 73 | const selectedNodes = figma.currentPage.selection.filter(node => node.type == "TEXT" && regex.test(node.characters)); 74 | selectedNodes.forEach(start); 75 | return selectedNodes.length > 0; 76 | } 77 | 78 | function checkForNodesThatBeginWithTimer(): boolean { 79 | const nodes = figma.currentPage.findAll(node => node.type === "TEXT" && node.characters.startsWith("Timer:")); 80 | nodes.forEach(start); 81 | return nodes.length > 0; 82 | } 83 | 84 | function start(node: TextNode) { 85 | var timeString = node.characters; 86 | var startsWithTimer = false; 87 | if (timeString.startsWith("Timer:")) { 88 | timeString = timeString.replace("Timer: ", ""); 89 | startsWithTimer = true; 90 | } 91 | 92 | var seconds = getRemainingSeconds(timeString); 93 | var template = getTemplateFromString(timeString); 94 | 95 | startTimer(node, seconds, template, startsWithTimer); 96 | 97 | //set plugin relaunch data for easy launching/installing of plugin 98 | node.setRelaunchData({ timer: '' }); 99 | 100 | } 101 | 102 | function getRemainingSeconds(timeString: string): number { 103 | var seconds = 0; 104 | var components = timeString.split(":"); 105 | secondsSet.reverse(); 106 | 107 | components.reverse().forEach((element, index) => { 108 | var factor = secondsSet[index]; 109 | seconds += factor * Number(element); 110 | }); 111 | 112 | secondsSet.reverse(); 113 | 114 | return seconds; 115 | } 116 | 117 | /** 118 | * Generates a template string from a timeString 119 | * e.g. converts 5:00 into 0:00 120 | */ 121 | function getTemplateFromString(timeString: string): string { 122 | var result = ""; 123 | for (const c of timeString) { 124 | if (c == ":") { 125 | result += c; 126 | } else { 127 | result += "0"; 128 | } 129 | } 130 | return result; 131 | } 132 | 133 | /** 134 | * Creates a new time string that conforms to the templates format 135 | * e.g. 5:00 (timeString) and 00:00:00 (template) will return 00:05:00 136 | */ 137 | function fillUpTimeStringWithTemplate(timeString: string, template: string): string { 138 | const trimmedTemplate = template.substring(0, template.length - timeString.length) 139 | return trimmedTemplate + timeString; 140 | } 141 | 142 | function secondsToInterval(seconds: number): string { 143 | var result = ""; 144 | var secondsToGo = seconds; 145 | secondsSet.forEach((element) => { 146 | var count = Math.floor(secondsToGo / element); 147 | if (count > 0 || result.length > 0) { 148 | secondsToGo -= count * element; 149 | if (result.length > 0) { 150 | result += ":"; 151 | if (count < 10) { 152 | result += "0"; 153 | } 154 | } 155 | result += String(count); 156 | } 157 | }) 158 | return result; 159 | } 160 | 161 | 162 | /** 163 | * Code that updates all timers on the Figma stage 164 | * will also send updates / messages to UI.html, so we can show timers there 165 | */ 166 | 167 | async function startTimer(node: TextNode, seconds: number, template: string, startsWithTimer: boolean) { 168 | await figma.loadFontAsync(node.fontName as FontName); 169 | totalTimers += 1 170 | 171 | console.log("Timer started / became active"); 172 | 173 | var timerID = totalTimers; 174 | var keepItRunning = true; 175 | var secondsToGo = seconds; 176 | var eventType = "start timer"; 177 | var newText = ""; 178 | 179 | adjustUIWindowHeight(); 180 | postMessageToUIWindow (eventType, newText, timerID, secondsToGo, seconds); 181 | 182 | // this loop updates all timers every second 183 | while (keepItRunning) { 184 | // checking if reset was clicked by user and if so resetting all timers 185 | if (reset) { 186 | newText = fillUpTimeStringWithTemplate(secondsToInterval(seconds), template); 187 | keepItRunning = false; 188 | updateTimerText(startsWithTimer, newText, node); 189 | } else if (!pause) { 190 | if (secondsToGo > 0) { 191 | newText = fillUpTimeStringWithTemplate(secondsToInterval(secondsToGo), template); 192 | eventType = "counting"; 193 | } else { 194 | newText = "Done" 195 | eventType = "timer done"; 196 | } 197 | postMessageToUIWindow(eventType, newText, timerID, secondsToGo, seconds); 198 | updateTimerText(startsWithTimer, newText, node); 199 | secondsToGo -= 1; 200 | } 201 | await delay(1000); 202 | } 203 | console.log("Timer finished / became in-active"); 204 | } 205 | 206 | function postMessageToUIWindow(eventType: string, timerText: string, timerID: number, secondsToGo: number, secondsToStart: number) { 207 | figma.ui.postMessage([eventType, timerText, timerID, secondsToGo, secondsToStart]); 208 | } 209 | 210 | function updateTimerText(startsWithTimer: boolean, newText: string, node: TextNode) { 211 | if (startsWithTimer) { 212 | newText = "Timer: " + newText; 213 | } 214 | node.characters = newText; 215 | } 216 | 217 | // adjusting height of UI windows depending on amount of timers 218 | function adjustUIWindowHeight() { 219 | var newUIHeight = 100 + totalTimers * 50; 220 | if (newUIHeight > uiWindow.maxHeight) { 221 | newUIHeight = uiWindow.maxHeight; 222 | } 223 | figma.ui.resize(uiWindow.width, newUIHeight); 224 | } -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 14 2 | 3 | declare global { 4 | // Global variable with Figma's plugin API. 5 | const figma: PluginAPI 6 | const __html__: string 7 | 8 | interface PluginAPI { 9 | readonly apiVersion: "1.0.0" 10 | readonly command: string 11 | readonly viewport: ViewportAPI 12 | closePlugin(message?: string): void 13 | 14 | notify(message: string, options?: NotificationOptions): NotificationHandler 15 | 16 | showUI(html: string, options?: ShowUIOptions): void 17 | readonly ui: UIAPI 18 | 19 | readonly clientStorage: ClientStorageAPI 20 | 21 | getNodeById(id: string): BaseNode | null 22 | getStyleById(id: string): BaseStyle | null 23 | 24 | readonly root: DocumentNode 25 | currentPage: PageNode 26 | 27 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 28 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 29 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 30 | 31 | readonly mixed: unique symbol 32 | 33 | createRectangle(): RectangleNode 34 | createLine(): LineNode 35 | createEllipse(): EllipseNode 36 | createPolygon(): PolygonNode 37 | createStar(): StarNode 38 | createVector(): VectorNode 39 | createText(): TextNode 40 | createFrame(): FrameNode 41 | createComponent(): ComponentNode 42 | createPage(): PageNode 43 | createSlice(): SliceNode 44 | /** 45 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 46 | */ 47 | createBooleanOperation(): BooleanOperationNode 48 | 49 | createPaintStyle(): PaintStyle 50 | createTextStyle(): TextStyle 51 | createEffectStyle(): EffectStyle 52 | createGridStyle(): GridStyle 53 | 54 | // The styles are returned in the same order as displayed in the UI. Only 55 | // local styles are returned. Never styles from team library. 56 | getLocalPaintStyles(): PaintStyle[] 57 | getLocalTextStyles(): TextStyle[] 58 | getLocalEffectStyles(): EffectStyle[] 59 | getLocalGridStyles(): GridStyle[] 60 | 61 | importComponentByKeyAsync(key: string): Promise 62 | importStyleByKeyAsync(key: string): Promise 63 | 64 | listAvailableFontsAsync(): Promise 65 | loadFontAsync(fontName: FontName): Promise 66 | readonly hasMissingFont: boolean 67 | 68 | createNodeFromSvg(svg: string): FrameNode 69 | 70 | createImage(data: Uint8Array): Image 71 | getImageByHash(hash: string): Image 72 | 73 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): GroupNode 74 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 75 | 76 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 80 | } 81 | 82 | interface ClientStorageAPI { 83 | getAsync(key: string): Promise 84 | setAsync(key: string, value: any): Promise 85 | } 86 | 87 | interface NotificationOptions { 88 | timeout?: number 89 | } 90 | 91 | interface NotificationHandler { 92 | cancel: () => void 93 | } 94 | 95 | interface ShowUIOptions { 96 | visible?: boolean 97 | width?: number 98 | height?: number 99 | } 100 | 101 | interface UIPostMessageOptions { 102 | origin?: string 103 | } 104 | 105 | interface OnMessageProperties { 106 | origin: string 107 | } 108 | 109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void 110 | 111 | interface UIAPI { 112 | show(): void 113 | hide(): void 114 | resize(width: number, height: number): void 115 | close(): void 116 | 117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 118 | onmessage: MessageEventHandler | undefined 119 | on(type: "message", callback: MessageEventHandler): void 120 | once(type: "message", callback: MessageEventHandler): void 121 | off(type: "message", callback: MessageEventHandler): void 122 | } 123 | 124 | interface ViewportAPI { 125 | center: Vector 126 | zoom: number 127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void 128 | readonly bounds: Rect 129 | } 130 | 131 | //////////////////////////////////////////////////////////////////////////////// 132 | // Datatypes 133 | 134 | type Transform = [ 135 | [number, number, number], 136 | [number, number, number] 137 | ] 138 | 139 | interface Vector { 140 | readonly x: number 141 | readonly y: number 142 | } 143 | 144 | interface Rect { 145 | readonly x: number 146 | readonly y: number 147 | readonly width: number 148 | readonly height: number 149 | } 150 | 151 | interface RGB { 152 | readonly r: number 153 | readonly g: number 154 | readonly b: number 155 | } 156 | 157 | interface RGBA { 158 | readonly r: number 159 | readonly g: number 160 | readonly b: number 161 | readonly a: number 162 | } 163 | 164 | interface FontName { 165 | readonly family: string 166 | readonly style: string 167 | } 168 | 169 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 170 | 171 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 172 | 173 | interface ArcData { 174 | readonly startingAngle: number 175 | readonly endingAngle: number 176 | readonly innerRadius: number 177 | } 178 | 179 | interface ShadowEffect { 180 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 181 | readonly color: RGBA 182 | readonly offset: Vector 183 | readonly radius: number 184 | readonly visible: boolean 185 | readonly blendMode: BlendMode 186 | } 187 | 188 | interface BlurEffect { 189 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 190 | readonly radius: number 191 | readonly visible: boolean 192 | } 193 | 194 | type Effect = ShadowEffect | BlurEffect 195 | 196 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 197 | 198 | interface Constraints { 199 | readonly horizontal: ConstraintType 200 | readonly vertical: ConstraintType 201 | } 202 | 203 | interface ColorStop { 204 | readonly position: number 205 | readonly color: RGBA 206 | } 207 | 208 | interface ImageFilters { 209 | readonly exposure?: number 210 | readonly contrast?: number 211 | readonly saturation?: number 212 | readonly temperature?: number 213 | readonly tint?: number 214 | readonly highlights?: number 215 | readonly shadows?: number 216 | } 217 | 218 | interface SolidPaint { 219 | readonly type: "SOLID" 220 | readonly color: RGB 221 | 222 | readonly visible?: boolean 223 | readonly opacity?: number 224 | readonly blendMode?: BlendMode 225 | } 226 | 227 | interface GradientPaint { 228 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 229 | readonly gradientTransform: Transform 230 | readonly gradientStops: ReadonlyArray 231 | 232 | readonly visible?: boolean 233 | readonly opacity?: number 234 | readonly blendMode?: BlendMode 235 | } 236 | 237 | interface ImagePaint { 238 | readonly type: "IMAGE" 239 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 240 | readonly imageHash: string | null 241 | readonly imageTransform?: Transform // setting for "CROP" 242 | readonly scalingFactor?: number // setting for "TILE" 243 | readonly filters?: ImageFilters 244 | 245 | readonly visible?: boolean 246 | readonly opacity?: number 247 | readonly blendMode?: BlendMode 248 | } 249 | 250 | type Paint = SolidPaint | GradientPaint | ImagePaint 251 | 252 | interface Guide { 253 | readonly axis: "X" | "Y" 254 | readonly offset: number 255 | } 256 | 257 | interface RowsColsLayoutGrid { 258 | readonly pattern: "ROWS" | "COLUMNS" 259 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 260 | readonly gutterSize: number 261 | 262 | readonly count: number // Infinity when "Auto" is set in the UI 263 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 264 | readonly offset?: number // Not set for alignment: "CENTER" 265 | 266 | readonly visible?: boolean 267 | readonly color?: RGBA 268 | } 269 | 270 | interface GridLayoutGrid { 271 | readonly pattern: "GRID" 272 | readonly sectionSize: number 273 | 274 | readonly visible?: boolean 275 | readonly color?: RGBA 276 | } 277 | 278 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 279 | 280 | interface ExportSettingsConstraints { 281 | readonly type: "SCALE" | "WIDTH" | "HEIGHT" 282 | readonly value: number 283 | } 284 | 285 | interface ExportSettingsImage { 286 | readonly format: "JPG" | "PNG" 287 | readonly contentsOnly?: boolean // defaults to true 288 | readonly suffix?: string 289 | readonly constraint?: ExportSettingsConstraints 290 | } 291 | 292 | interface ExportSettingsSVG { 293 | readonly format: "SVG" 294 | readonly contentsOnly?: boolean // defaults to true 295 | readonly suffix?: string 296 | readonly svgOutlineText?: boolean // defaults to true 297 | readonly svgIdAttribute?: boolean // defaults to false 298 | readonly svgSimplifyStroke?: boolean // defaults to true 299 | } 300 | 301 | interface ExportSettingsPDF { 302 | readonly format: "PDF" 303 | readonly contentsOnly?: boolean // defaults to true 304 | readonly suffix?: string 305 | } 306 | 307 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 308 | 309 | type WindingRule = "NONZERO" | "EVENODD" 310 | 311 | interface VectorVertex { 312 | readonly x: number 313 | readonly y: number 314 | readonly strokeCap?: StrokeCap 315 | readonly strokeJoin?: StrokeJoin 316 | readonly cornerRadius?: number 317 | readonly handleMirroring?: HandleMirroring 318 | } 319 | 320 | interface VectorSegment { 321 | readonly start: number 322 | readonly end: number 323 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 324 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 325 | } 326 | 327 | interface VectorRegion { 328 | readonly windingRule: WindingRule 329 | readonly loops: ReadonlyArray> 330 | } 331 | 332 | interface VectorNetwork { 333 | readonly vertices: ReadonlyArray 334 | readonly segments: ReadonlyArray 335 | readonly regions?: ReadonlyArray // Defaults to [] 336 | } 337 | 338 | interface VectorPath { 339 | readonly windingRule: WindingRule | "NONE" 340 | readonly data: string 341 | } 342 | 343 | type VectorPaths = ReadonlyArray 344 | 345 | interface LetterSpacing { 346 | readonly value: number 347 | readonly unit: "PIXELS" | "PERCENT" 348 | } 349 | 350 | type LineHeight = { 351 | readonly value: number 352 | readonly unit: "PIXELS" | "PERCENT" 353 | } | { 354 | readonly unit: "AUTO" 355 | } 356 | 357 | type BlendMode = 358 | "PASS_THROUGH" | 359 | "NORMAL" | 360 | "DARKEN" | 361 | "MULTIPLY" | 362 | "LINEAR_BURN" | 363 | "COLOR_BURN" | 364 | "LIGHTEN" | 365 | "SCREEN" | 366 | "LINEAR_DODGE" | 367 | "COLOR_DODGE" | 368 | "OVERLAY" | 369 | "SOFT_LIGHT" | 370 | "HARD_LIGHT" | 371 | "DIFFERENCE" | 372 | "EXCLUSION" | 373 | "HUE" | 374 | "SATURATION" | 375 | "COLOR" | 376 | "LUMINOSITY" 377 | 378 | interface Font { 379 | fontName: FontName 380 | } 381 | 382 | type Reaction = { action: Action, trigger: Trigger } 383 | 384 | type Action = 385 | { readonly type: "BACK" | "CLOSE" } | 386 | { readonly type: "URL", url: string } | 387 | { readonly type: "NODE" 388 | readonly destinationId: string | null 389 | readonly navigation: Navigation 390 | readonly transition: Transition | null 391 | readonly preserveScrollPosition: boolean 392 | 393 | // Only present if navigation == "OVERLAY" and the destination uses 394 | // overlay position type "RELATIVE" 395 | readonly overlayRelativePosition?: Vector 396 | } 397 | 398 | interface SimpleTransition { 399 | readonly type: "DISSOLVE" | "SMART_ANIMATE" 400 | readonly easing: Easing 401 | readonly duration: number 402 | } 403 | 404 | interface DirectionalTransition { 405 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT" 406 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM" 407 | readonly matchLayers: boolean 408 | 409 | readonly easing: Easing 410 | readonly duration: number 411 | } 412 | 413 | type Transition = SimpleTransition | DirectionalTransition 414 | 415 | type Trigger = 416 | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } | 417 | { readonly type: "AFTER_TIMEOUT", readonly timeout: number } | 418 | { readonly type: "MOUSE_ENTER" | "MOUSE_LEAVE" | "MOUSE_UP" | "MOUSE_DOWN" 419 | readonly delay: number 420 | } 421 | 422 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY" 423 | 424 | interface Easing { 425 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR" 426 | } 427 | 428 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH" 429 | 430 | type OverlayPositionType = "CENTER" | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT" | "MANUAL" 431 | 432 | type OverlayBackground = 433 | { readonly type: "NONE" } | 434 | { readonly type: "SOLID_COLOR", readonly color: RGBA } 435 | 436 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE" 437 | 438 | //////////////////////////////////////////////////////////////////////////////// 439 | // Mixins 440 | 441 | interface BaseNodeMixin { 442 | readonly id: string 443 | readonly parent: (BaseNode & ChildrenMixin) | null 444 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 445 | readonly removed: boolean 446 | toString(): string 447 | remove(): void 448 | 449 | getPluginData(key: string): string 450 | setPluginData(key: string, value: string): void 451 | 452 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 453 | // be a name related to your plugin. Other plugins will be able to read this data. 454 | getSharedPluginData(namespace: string, key: string): string 455 | setSharedPluginData(namespace: string, key: string, value: string): void 456 | setRelaunchData(data: { [command: string]: /* description */ string }): void 457 | } 458 | 459 | interface SceneNodeMixin { 460 | visible: boolean 461 | locked: boolean 462 | } 463 | 464 | interface ChildrenMixin { 465 | readonly children: ReadonlyArray 466 | 467 | appendChild(child: SceneNode): void 468 | insertChild(index: number, child: SceneNode): void 469 | 470 | findChildren(callback?: (node: SceneNode) => boolean): SceneNode[] 471 | findChild(callback: (node: SceneNode) => boolean): SceneNode | null 472 | 473 | /** 474 | * If you only need to search immediate children, it is much faster 475 | * to call node.children.filter(callback) or node.findChildren(callback) 476 | */ 477 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 478 | 479 | /** 480 | * If you only need to search immediate children, it is much faster 481 | * to call node.children.find(callback) or node.findChild(callback) 482 | */ 483 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 484 | } 485 | 486 | interface ConstraintMixin { 487 | constraints: Constraints 488 | } 489 | 490 | interface LayoutMixin { 491 | readonly absoluteTransform: Transform 492 | relativeTransform: Transform 493 | x: number 494 | y: number 495 | rotation: number // In degrees 496 | 497 | readonly width: number 498 | readonly height: number 499 | constrainProportions: boolean 500 | 501 | layoutAlign: "MIN" | "CENTER" | "MAX" | "STRETCH" // applicable only inside auto-layout frames 502 | 503 | resize(width: number, height: number): void 504 | resizeWithoutConstraints(width: number, height: number): void 505 | } 506 | 507 | interface BlendMixin { 508 | opacity: number 509 | blendMode: BlendMode 510 | isMask: boolean 511 | effects: ReadonlyArray 512 | effectStyleId: string 513 | } 514 | 515 | interface ContainerMixin { 516 | expanded: boolean 517 | backgrounds: ReadonlyArray // DEPRECATED: use 'fills' instead 518 | backgroundStyleId: string // DEPRECATED: use 'fillStyleId' instead 519 | } 520 | 521 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 522 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 523 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 524 | 525 | interface GeometryMixin { 526 | fills: ReadonlyArray | PluginAPI['mixed'] 527 | strokes: ReadonlyArray 528 | strokeWeight: number 529 | strokeMiterLimit: number 530 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 531 | strokeCap: StrokeCap | PluginAPI['mixed'] 532 | strokeJoin: StrokeJoin | PluginAPI['mixed'] 533 | dashPattern: ReadonlyArray 534 | fillStyleId: string | PluginAPI['mixed'] 535 | strokeStyleId: string 536 | outlineStroke(): VectorNode | null 537 | } 538 | 539 | interface CornerMixin { 540 | cornerRadius: number | PluginAPI['mixed'] 541 | cornerSmoothing: number 542 | } 543 | 544 | interface RectangleCornerMixin { 545 | topLeftRadius: number 546 | topRightRadius: number 547 | bottomLeftRadius: number 548 | bottomRightRadius: number 549 | } 550 | 551 | interface ExportMixin { 552 | exportSettings: ReadonlyArray 553 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 554 | } 555 | 556 | interface ReactionMixin { 557 | readonly reactions: ReadonlyArray 558 | } 559 | 560 | interface DefaultShapeMixin extends 561 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 562 | BlendMixin, GeometryMixin, LayoutMixin, 563 | ExportMixin { 564 | } 565 | 566 | interface DefaultFrameMixin extends 567 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 568 | ChildrenMixin, ContainerMixin, 569 | GeometryMixin, CornerMixin, RectangleCornerMixin, 570 | BlendMixin, ConstraintMixin, LayoutMixin, 571 | ExportMixin { 572 | 573 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL" 574 | counterAxisSizingMode: "FIXED" | "AUTO" // applicable only if layoutMode != "NONE" 575 | horizontalPadding: number // applicable only if layoutMode != "NONE" 576 | verticalPadding: number // applicable only if layoutMode != "NONE" 577 | itemSpacing: number // applicable only if layoutMode != "NONE" 578 | 579 | layoutGrids: ReadonlyArray 580 | gridStyleId: string 581 | clipsContent: boolean 582 | guides: ReadonlyArray 583 | 584 | overflowDirection: OverflowDirection 585 | numberOfFixedChildren: number 586 | 587 | readonly overlayPositionType: OverlayPositionType 588 | readonly overlayBackground: OverlayBackground 589 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction 590 | } 591 | 592 | //////////////////////////////////////////////////////////////////////////////// 593 | // Nodes 594 | 595 | interface DocumentNode extends BaseNodeMixin { 596 | readonly type: "DOCUMENT" 597 | 598 | readonly children: ReadonlyArray 599 | 600 | appendChild(child: PageNode): void 601 | insertChild(index: number, child: PageNode): void 602 | findChildren(callback?: (node: PageNode) => boolean): Array 603 | findChild(callback: (node: PageNode) => boolean): PageNode | null 604 | 605 | /** 606 | * If you only need to search immediate children, it is much faster 607 | * to call node.children.filter(callback) or node.findChildren(callback) 608 | */ 609 | findAll(callback?: (node: PageNode | SceneNode) => boolean): Array 610 | 611 | /** 612 | * If you only need to search immediate children, it is much faster 613 | * to call node.children.find(callback) or node.findChild(callback) 614 | */ 615 | findOne(callback: (node: PageNode | SceneNode) => boolean): PageNode | SceneNode | null 616 | } 617 | 618 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 619 | 620 | readonly type: "PAGE" 621 | clone(): PageNode 622 | 623 | guides: ReadonlyArray 624 | selection: ReadonlyArray 625 | selectedTextRange: { node: TextNode, start: number, end: number } | null 626 | 627 | backgrounds: ReadonlyArray 628 | 629 | readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null 630 | } 631 | 632 | interface FrameNode extends DefaultFrameMixin { 633 | readonly type: "FRAME" 634 | clone(): FrameNode 635 | } 636 | 637 | interface GroupNode extends 638 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 639 | ChildrenMixin, ContainerMixin, BlendMixin, 640 | LayoutMixin, ExportMixin { 641 | 642 | readonly type: "GROUP" 643 | clone(): GroupNode 644 | } 645 | 646 | interface SliceNode extends 647 | BaseNodeMixin, SceneNodeMixin, LayoutMixin, 648 | ExportMixin { 649 | 650 | readonly type: "SLICE" 651 | clone(): SliceNode 652 | } 653 | 654 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin { 655 | readonly type: "RECTANGLE" 656 | clone(): RectangleNode 657 | } 658 | 659 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 660 | readonly type: "LINE" 661 | clone(): LineNode 662 | } 663 | 664 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 665 | readonly type: "ELLIPSE" 666 | clone(): EllipseNode 667 | arcData: ArcData 668 | } 669 | 670 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 671 | readonly type: "POLYGON" 672 | clone(): PolygonNode 673 | pointCount: number 674 | } 675 | 676 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 677 | readonly type: "STAR" 678 | clone(): StarNode 679 | pointCount: number 680 | innerRadius: number 681 | } 682 | 683 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 684 | readonly type: "VECTOR" 685 | clone(): VectorNode 686 | vectorNetwork: VectorNetwork 687 | vectorPaths: VectorPaths 688 | handleMirroring: HandleMirroring | PluginAPI['mixed'] 689 | } 690 | 691 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 692 | readonly type: "TEXT" 693 | clone(): TextNode 694 | readonly hasMissingFont: boolean 695 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 696 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 697 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 698 | paragraphIndent: number 699 | paragraphSpacing: number 700 | autoRename: boolean 701 | 702 | textStyleId: string | PluginAPI['mixed'] 703 | fontSize: number | PluginAPI['mixed'] 704 | fontName: FontName | PluginAPI['mixed'] 705 | textCase: TextCase | PluginAPI['mixed'] 706 | textDecoration: TextDecoration | PluginAPI['mixed'] 707 | letterSpacing: LetterSpacing | PluginAPI['mixed'] 708 | lineHeight: LineHeight | PluginAPI['mixed'] 709 | 710 | characters: string 711 | insertCharacters(start: number, characters: string, useStyle?: "BEFORE" | "AFTER"): void 712 | deleteCharacters(start: number, end: number): void 713 | 714 | getRangeFontSize(start: number, end: number): number | PluginAPI['mixed'] 715 | setRangeFontSize(start: number, end: number, value: number): void 716 | getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed'] 717 | setRangeFontName(start: number, end: number, value: FontName): void 718 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed'] 719 | setRangeTextCase(start: number, end: number, value: TextCase): void 720 | getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed'] 721 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 722 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed'] 723 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 724 | getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed'] 725 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 726 | getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed'] 727 | setRangeFills(start: number, end: number, value: Paint[]): void 728 | getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed'] 729 | setRangeTextStyleId(start: number, end: number, value: string): void 730 | getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed'] 731 | setRangeFillStyleId(start: number, end: number, value: string): void 732 | } 733 | 734 | interface ComponentNode extends DefaultFrameMixin { 735 | readonly type: "COMPONENT" 736 | clone(): ComponentNode 737 | 738 | createInstance(): InstanceNode 739 | description: string 740 | readonly remote: boolean 741 | readonly key: string // The key to use with "importComponentByKeyAsync" 742 | } 743 | 744 | interface InstanceNode extends DefaultFrameMixin { 745 | readonly type: "INSTANCE" 746 | clone(): InstanceNode 747 | masterComponent: ComponentNode 748 | scaleFactor: number 749 | } 750 | 751 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 752 | readonly type: "BOOLEAN_OPERATION" 753 | clone(): BooleanOperationNode 754 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 755 | 756 | expanded: boolean 757 | } 758 | 759 | type BaseNode = 760 | DocumentNode | 761 | PageNode | 762 | SceneNode 763 | 764 | type SceneNode = 765 | SliceNode | 766 | FrameNode | 767 | GroupNode | 768 | ComponentNode | 769 | InstanceNode | 770 | BooleanOperationNode | 771 | VectorNode | 772 | StarNode | 773 | LineNode | 774 | EllipseNode | 775 | PolygonNode | 776 | RectangleNode | 777 | TextNode 778 | 779 | type NodeType = 780 | "DOCUMENT" | 781 | "PAGE" | 782 | "SLICE" | 783 | "FRAME" | 784 | "GROUP" | 785 | "COMPONENT" | 786 | "INSTANCE" | 787 | "BOOLEAN_OPERATION" | 788 | "VECTOR" | 789 | "STAR" | 790 | "LINE" | 791 | "ELLIPSE" | 792 | "POLYGON" | 793 | "RECTANGLE" | 794 | "TEXT" 795 | 796 | //////////////////////////////////////////////////////////////////////////////// 797 | // Styles 798 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 799 | 800 | interface BaseStyle { 801 | readonly id: string 802 | readonly type: StyleType 803 | name: string 804 | description: string 805 | remote: boolean 806 | readonly key: string // The key to use with "importStyleByKeyAsync" 807 | remove(): void 808 | } 809 | 810 | interface PaintStyle extends BaseStyle { 811 | type: "PAINT" 812 | paints: ReadonlyArray 813 | } 814 | 815 | interface TextStyle extends BaseStyle { 816 | type: "TEXT" 817 | fontSize: number 818 | textDecoration: TextDecoration 819 | fontName: FontName 820 | letterSpacing: LetterSpacing 821 | lineHeight: LineHeight 822 | paragraphIndent: number 823 | paragraphSpacing: number 824 | textCase: TextCase 825 | } 826 | 827 | interface EffectStyle extends BaseStyle { 828 | type: "EFFECT" 829 | effects: ReadonlyArray 830 | } 831 | 832 | interface GridStyle extends BaseStyle { 833 | type: "GRID" 834 | layoutGrids: ReadonlyArray 835 | } 836 | 837 | //////////////////////////////////////////////////////////////////////////////// 838 | // Other 839 | 840 | interface Image { 841 | readonly hash: string 842 | getBytesAsync(): Promise 843 | } 844 | } // declare global 845 | 846 | export {} 847 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Timer", 3 | "id": "820311256083321341", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "relaunchButtons": [ 8 | {"command": "timer", "name": "Figma Timer", "multipleSelection": true} 9 | ] 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-timer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "code.ts", 6 | "types": "figma.d.ts", 7 | "scripts": { 8 | "build": "tsc --build tsconfig.json", 9 | "test": "jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://lennet@github.com/lennet/Figma-Timer.git" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/lennet/Figma-Timer/issues" 19 | }, 20 | "homepage": "https://github.com/lennet/Figma-Timer#readme", 21 | "devDependencies": { 22 | "@types/jest": "^25.2.1", 23 | "jest": "^25.4.0", 24 | "ts-jest": "^25.4.0", 25 | "typescript": "^3.8.3" 26 | }, 27 | "jest": { 28 | "transform": { 29 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 30 | }, 31 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 32 | "moduleFileExtensions": [ 33 | "ts", 34 | "tsx", 35 | "js" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "esModuleInterop": true, 5 | "resolveJsonModule": true, 6 | "moduleResolution": "node" 7 | }, 8 | "include": ["*"], 9 | "exclude": ["node_modules", "*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ui.html: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 | 33 | 34 | 39 | 44 | 45 | 46 | --------------------------------------------------------------------------------