├── .gitignore ├── README.md ├── fetch-image ├── README.md ├── manifest.json └── plugin.js ├── graphviz ├── README.md ├── artwork.png ├── manifest.json ├── package-lock.json ├── package.json ├── src │ ├── editor.ts │ ├── figma.d.ts │ ├── figplug.d.ts │ ├── plugin.ts │ ├── structs.ts │ ├── ui.css │ ├── ui.html │ ├── ui.ts │ └── util.ts └── tsconfig.json ├── minimap ├── README.md ├── artwork.png ├── figma.d.ts ├── figplug.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── src │ ├── figutil.ts │ ├── plugin.ts │ ├── structs.ts │ ├── ui.css │ ├── ui.html │ └── ui.ts └── tsconfig.json └── misc └── optimize-resources.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | *~ 4 | _local/ 5 | build/ 6 | node_modules 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collection of Figma plugins 2 | 3 | Most are small examples. 4 | 5 | All you need to try these is: 6 | - [Nodejs](https://nodejs.org/) 7 | - [figplug](https://rsms.me/figplug/) 8 | - [Figma desktop app](https://www.figma.com/downloads/) 9 | -------------------------------------------------------------------------------- /fetch-image/README.md: -------------------------------------------------------------------------------- 1 | This is a small example of fetching an image from somewhere on the 2 | internet and adds it to the canvas in Figma, without using a UI. 3 | 4 | ## Usage 5 | 6 | In Figma, add a new dev plugin and select this folder 7 | -------------------------------------------------------------------------------- /fetch-image/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "1.0.0", 3 | "name": "fetch-image", 4 | "main": "plugin.js" 5 | } 6 | -------------------------------------------------------------------------------- /fetch-image/plugin.js: -------------------------------------------------------------------------------- 1 | // URL to fetch 2 | let url = "https://scripter.rsms.me/icon.png" 3 | 4 | // Run fetch in the UI process, sending the result to the plugin when done 5 | figma.showUI(``, { 12 | visible:false, // don't actually show a UI window 13 | }) 14 | 15 | // listen for messages from the UI process 16 | figma.ui.onmessage = msg => { 17 | if (msg.data && msg.data.length > 0) { 18 | addImageToCanvas(msg.data) 19 | } 20 | figma.closePlugin(msg.error || "") 21 | } 22 | 23 | // Function that creates a rectangle on canvas with an image fill from image data 24 | function addImageToCanvas(data) { 25 | let imageHash = figma.createImage(data).hash 26 | const rect = figma.createRectangle() 27 | rect.fills = [ { type: "IMAGE", scaleMode: "FIT", imageHash } ] 28 | figma.currentPage.appendChild(rect) 29 | 30 | // select the rectangle and focus the viewport 31 | figma.currentPage.selection = [rect] 32 | figma.viewport.scrollAndZoomIntoView([rect]) 33 | } 34 | 35 | // Notes: 36 | // 37 | // If you are to fetch resources in a real production plugin, you will most 38 | // likely want to make sure you can handle multiple fetches concurrently. 39 | // 40 | // The order by which messages are send and received is not deterministic when 41 | // dealing with the network. Therefore you will need to "multiplex" your fetches. 42 | // Multiplexing is the ability to do multiple things over the same "channel"; in 43 | // our case messages passed between the plugin process and the UI process. 44 | // 45 | // Implementing multiplexing is pretty easy for this case: 46 | // 47 | // 1. When you being a fetch call, generate a unique identifier. For instance, 48 | // a number variable that you keep incrementing. 49 | // 50 | // 2. Associate this ID with the promise or callback for the fetch call. 51 | // 52 | // 3. Include this ID with your postMessage call to the UI process. 53 | // 54 | // 4. In your UI process where you execute fetch(), include that same ID with the 55 | // response message that the UI sends back to the plugin with postMessage. 56 | // 57 | // 5. In your plugin process, look up the promise or callback for the ID in the 58 | // message you receive. This is the association you made in step 2. 59 | // Resolve the promise or call the callback with the message result. 60 | // 61 | // 6. Clear the association between the ID and promise/callback to free up memory. 62 | // 63 | // Another way to think about this is as requests and responses — you want to 64 | // track the request as your process it so that when the sender seens a response it 65 | // knows which request it is a response for. 66 | // 67 | -------------------------------------------------------------------------------- /graphviz/README.md: -------------------------------------------------------------------------------- 1 | # Graphviz 2 | 3 | 4 | 5 | A plugin that uses graphviz to generate graphs 6 | 7 | ![screenshot](artwork.png) 8 | 9 | ## Development 10 | 11 | - Development mode: `npm run dev` 12 | - Build in release mode: `npm run build` 13 | -------------------------------------------------------------------------------- /graphviz/artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/figma-plugins/2c5d0c24180d14bce7776656584da99b8996a696/graphviz/artwork.png -------------------------------------------------------------------------------- /graphviz/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "1.0.0", 3 | "name": "Graphviz", 4 | "id": "770827538515501401", 5 | "main": "src/plugin.ts", 6 | "ui": "src/ui.ts" 7 | } 8 | -------------------------------------------------------------------------------- /graphviz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Graphviz", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "figplug build -O -o=build", 7 | "dev": "figplug build -g -w -v -o=build" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "figplug": "0.1.12" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /graphviz/src/editor.ts: -------------------------------------------------------------------------------- 1 | import { isMac, addKeyEventHandler } from "./util" 2 | 3 | export class Editor { 4 | readonly ta :HTMLTextAreaElement 5 | readonly defaultText :string 6 | 7 | focused = false 8 | _textSize = 0 9 | 10 | constructor(ta :HTMLTextAreaElement) { 11 | this.ta = ta 12 | this.defaultText = ta.value.trim() 13 | } 14 | 15 | 16 | init() { 17 | addKeyEventHandler(this.ta, this.onKeyEvent) 18 | this.ta.addEventListener("focus", this.onReceivedFocus) 19 | this.ta.addEventListener("blur", this.onLostFocus) 20 | this.ta.focus() 21 | } 22 | 23 | 24 | onReceivedFocus = () => { 25 | this.focused = true 26 | } 27 | 28 | onLostFocus = () => { 29 | this.focused = false 30 | } 31 | 32 | 33 | onKeyEvent = (ev :KeyboardEvent, key :string) => { 34 | if (!this.focused) { 35 | return false 36 | } 37 | 38 | if ((ev.metaKey || ev.ctrlKey) && key == "a") { 39 | return this.selectAll(), true 40 | } 41 | 42 | // Figma captures undo and redo, not sending them to the plugin, unless we capture them. 43 | if ((ev.metaKey || ev.ctrlKey) && key == "z") { 44 | return this.undo(), true 45 | } 46 | if ( ((ev.metaKey || ev.ctrlKey) && ev.shiftKey && (key == "Z" || key == "z")) || 47 | (!isMac && ev.ctrlKey && key == "y") ) { 48 | return this.redo(), true 49 | } 50 | 51 | // Figma grabs copy, paste etc which is slow. Intercept. 52 | if ((ev.metaKey || ev.ctrlKey) && key == "c") { 53 | return document.execCommand("copy"), true 54 | } 55 | if ((ev.metaKey || ev.ctrlKey) && key == "x") { 56 | return document.execCommand("cut"), true 57 | } 58 | if ((ev.metaKey || ev.ctrlKey) && key == "v") { 59 | return document.execCommand("paste"), true 60 | } 61 | 62 | // indentation 63 | if ((ev.metaKey || ev.ctrlKey) && key == "]") { 64 | return this.indent(), true 65 | } 66 | if ((ev.metaKey || ev.ctrlKey) && key == "[") { 67 | return this.dedent(), true 68 | } 69 | if (key == "Tab") { 70 | return this.dedent(), true 71 | } 72 | 73 | // text size 74 | if ((ev.metaKey || ev.ctrlKey) && (key == "+" || key == "=" || key == "Plus")) { 75 | return this.increaseTextSize(), true 76 | } 77 | if ((ev.metaKey || ev.ctrlKey) && (key == "-" || key == "Minus")) { 78 | return this.decreaseTextSize(), true 79 | } 80 | 81 | // if (DEBUG && (ev.metaKey || ev.ctrlKey || ev.altKey)) { 82 | // dlog("Editor onKeyEvent: [unhandled] key:", key, ev) 83 | // } 84 | 85 | return false 86 | } 87 | 88 | 89 | getTextSize() :number { 90 | if (!this._textSize) { 91 | let s = window.getComputedStyle(this.ta) 92 | let v = s.getPropertyValue("--editorFontSize") 93 | this._textSize = parseInt(v) 94 | if (isNaN(this._textSize)) { 95 | this._textSize = 11 96 | } 97 | } 98 | return this._textSize 99 | } 100 | 101 | setTextSize(textSize :number) { 102 | this._textSize = Math.min(40, Math.max(7, textSize)) 103 | document.body.style.setProperty("--editorFontSize", `${this._textSize}px`) 104 | } 105 | 106 | resetTextSize() { 107 | document.body.style.removeProperty("--editorFontSize") 108 | this._textSize = 0 109 | // this.setTextSize(11) // XXX hard coded 110 | } 111 | 112 | 113 | increaseTextSize() { 114 | this.setTextSize(this.getTextSize() + 1) 115 | } 116 | 117 | decreaseTextSize() { 118 | this.setTextSize(this.getTextSize() - 1) 119 | } 120 | 121 | 122 | indent() { 123 | dlog("TODO: Editor indent") 124 | } 125 | 126 | 127 | dedent() { 128 | dlog("TODO: Editor dedent") 129 | } 130 | 131 | 132 | undo() { 133 | document.execCommand("undo") 134 | } 135 | 136 | redo() { 137 | document.execCommand("redo") 138 | } 139 | 140 | 141 | get text() :string { 142 | return this.ta.value 143 | } 144 | 145 | set text(value :string) { 146 | this.ta.value = value 147 | } 148 | 149 | 150 | focus() { 151 | this.ta.focus() 152 | } 153 | 154 | selectAll() { 155 | this.ta.select() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /graphviz/src/figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 5 2 | 3 | // Global variable with Figma's plugin API. 4 | declare const figma: PluginAPI 5 | declare const __html__: string 6 | 7 | interface PluginAPI { 8 | readonly apiVersion: "1.0.0" 9 | readonly command: string 10 | readonly viewport: ViewportAPI 11 | closePlugin(message?: string): void 12 | 13 | notify(message: string, options?: NotificationOptions): NotificationHandler 14 | 15 | showUI(html: string, options?: ShowUIOptions): void 16 | readonly ui: UIAPI 17 | 18 | readonly clientStorage: ClientStorageAPI 19 | 20 | getNodeById(id: string): BaseNode | null 21 | getStyleById(id: string): BaseStyle | null 22 | 23 | readonly root: DocumentNode 24 | currentPage: PageNode 25 | 26 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void) 27 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void) 28 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void) 29 | 30 | readonly mixed: symbol 31 | 32 | createRectangle(): RectangleNode 33 | createLine(): LineNode 34 | createEllipse(): EllipseNode 35 | createPolygon(): PolygonNode 36 | createStar(): StarNode 37 | createVector(): VectorNode 38 | createText(): TextNode 39 | createFrame(): FrameNode 40 | createComponent(): ComponentNode 41 | createPage(): PageNode 42 | createSlice(): SliceNode 43 | /** 44 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 45 | */ 46 | createBooleanOperation(): BooleanOperationNode 47 | 48 | createPaintStyle(): PaintStyle 49 | createTextStyle(): TextStyle 50 | createEffectStyle(): EffectStyle 51 | createGridStyle(): GridStyle 52 | 53 | // The styles are returned in the same order as displayed in the UI. Only 54 | // local styles are returned. Never styles from team library. 55 | getLocalPaintStyles(): PaintStyle[] 56 | getLocalTextStyles(): TextStyle[] 57 | getLocalEffectStyles(): EffectStyle[] 58 | getLocalGridStyles(): GridStyle[] 59 | 60 | importComponentByKeyAsync(key: string): Promise 61 | importStyleByKeyAsync(key: string): Promise 62 | 63 | listAvailableFontsAsync(): Promise 64 | loadFontAsync(fontName: FontName): Promise 65 | readonly hasMissingFont: boolean 66 | 67 | createNodeFromSvg(svg: string): FrameNode 68 | 69 | createImage(data: Uint8Array): Image 70 | getImageByHash(hash: string): Image 71 | 72 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 73 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 74 | 75 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 76 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | } 80 | 81 | interface ClientStorageAPI { 82 | getAsync(key: string): Promise 83 | setAsync(key: string, value: any): Promise 84 | } 85 | 86 | interface NotificationOptions { 87 | timeout?: number, 88 | } 89 | 90 | interface NotificationHandler { 91 | cancel: () => void, 92 | } 93 | 94 | interface ShowUIOptions { 95 | visible?: boolean, 96 | width?: number, 97 | height?: number, 98 | position?: 'default' | 'last' | 'auto' // PROPOSED API ONLY 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) 120 | once(type: "message", callback: MessageEventHandler) 121 | off(type: "message", callback: MessageEventHandler) 122 | } 123 | 124 | interface ViewportAPI { 125 | center: { x: number, y: number } 126 | zoom: number 127 | scrollAndZoomIntoView(nodes: ReadonlyArray) 128 | } 129 | 130 | //////////////////////////////////////////////////////////////////////////////// 131 | // Datatypes 132 | 133 | type Transform = [ 134 | [number, number, number], 135 | [number, number, number] 136 | ] 137 | 138 | interface Vector { 139 | readonly x: number 140 | readonly y: number 141 | } 142 | 143 | interface RGB { 144 | readonly r: number 145 | readonly g: number 146 | readonly b: number 147 | } 148 | 149 | interface RGBA { 150 | readonly r: number 151 | readonly g: number 152 | readonly b: number 153 | readonly a: number 154 | } 155 | 156 | interface FontName { 157 | readonly family: string 158 | readonly style: string 159 | } 160 | 161 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 162 | 163 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 164 | 165 | interface ArcData { 166 | readonly startingAngle: number 167 | readonly endingAngle: number 168 | readonly innerRadius: number 169 | } 170 | 171 | interface ShadowEffect { 172 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 173 | readonly color: RGBA 174 | readonly offset: Vector 175 | readonly radius: number 176 | readonly visible: boolean 177 | readonly blendMode: BlendMode 178 | } 179 | 180 | interface BlurEffect { 181 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 182 | readonly radius: number 183 | readonly visible: boolean 184 | } 185 | 186 | type Effect = ShadowEffect | BlurEffect 187 | 188 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 189 | 190 | interface Constraints { 191 | readonly horizontal: ConstraintType 192 | readonly vertical: ConstraintType 193 | } 194 | 195 | interface ColorStop { 196 | readonly position: number 197 | readonly color: RGBA 198 | } 199 | 200 | interface ImageFilters { 201 | readonly exposure?: number 202 | readonly contrast?: number 203 | readonly saturation?: number 204 | readonly temperature?: number 205 | readonly tint?: number 206 | readonly highlights?: number 207 | readonly shadows?: number 208 | } 209 | 210 | interface SolidPaint { 211 | readonly type: "SOLID" 212 | readonly color: RGB 213 | 214 | readonly visible?: boolean 215 | readonly opacity?: number 216 | readonly blendMode?: BlendMode 217 | } 218 | 219 | interface GradientPaint { 220 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 221 | readonly gradientTransform: Transform 222 | readonly gradientStops: ReadonlyArray 223 | 224 | readonly visible?: boolean 225 | readonly opacity?: number 226 | readonly blendMode?: BlendMode 227 | } 228 | 229 | interface ImagePaint { 230 | readonly type: "IMAGE" 231 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 232 | readonly imageHash: string | null 233 | readonly imageTransform?: Transform // setting for "CROP" 234 | readonly scalingFactor?: number // setting for "TILE" 235 | readonly filters?: ImageFilters 236 | 237 | readonly visible?: boolean 238 | readonly opacity?: number 239 | readonly blendMode?: BlendMode 240 | } 241 | 242 | type Paint = SolidPaint | GradientPaint | ImagePaint 243 | 244 | interface Guide { 245 | readonly axis: "X" | "Y" 246 | readonly offset: number 247 | } 248 | 249 | interface RowsColsLayoutGrid { 250 | readonly pattern: "ROWS" | "COLUMNS" 251 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 252 | readonly gutterSize: number 253 | 254 | readonly count: number // Infinity when "Auto" is set in the UI 255 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 256 | readonly offset?: number // Not set for alignment: "CENTER" 257 | 258 | readonly visible?: boolean 259 | readonly color?: RGBA 260 | } 261 | 262 | interface GridLayoutGrid { 263 | readonly pattern: "GRID" 264 | readonly sectionSize: number 265 | 266 | readonly visible?: boolean 267 | readonly color?: RGBA 268 | } 269 | 270 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 271 | 272 | interface ExportSettingsConstraints { 273 | type: "SCALE" | "WIDTH" | "HEIGHT" 274 | value: number 275 | } 276 | 277 | interface ExportSettingsImage { 278 | format: "JPG" | "PNG" 279 | contentsOnly?: boolean // defaults to true 280 | suffix?: string 281 | constraint?: ExportSettingsConstraints 282 | } 283 | 284 | interface ExportSettingsSVG { 285 | format: "SVG" 286 | contentsOnly?: boolean // defaults to true 287 | suffix?: string 288 | svgOutlineText?: boolean // defaults to true 289 | svgIdAttribute?: boolean // defaults to false 290 | svgSimplifyStroke?: boolean // defaults to true 291 | } 292 | 293 | interface ExportSettingsPDF { 294 | format: "PDF" 295 | contentsOnly?: boolean // defaults to true 296 | suffix?: string 297 | } 298 | 299 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 300 | 301 | type WindingRule = "NONZERO" | "EVENODD" 302 | 303 | interface VectorVertex { 304 | readonly x: number 305 | readonly y: number 306 | readonly strokeCap?: StrokeCap 307 | readonly strokeJoin?: StrokeJoin 308 | readonly cornerRadius?: number 309 | readonly handleMirroring?: HandleMirroring 310 | } 311 | 312 | interface VectorSegment { 313 | readonly start: number 314 | readonly end: number 315 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 316 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 317 | } 318 | 319 | interface VectorRegion { 320 | readonly windingRule: WindingRule 321 | readonly loops: ReadonlyArray> 322 | } 323 | 324 | interface VectorNetwork { 325 | readonly vertices: ReadonlyArray 326 | readonly segments: ReadonlyArray 327 | readonly regions?: ReadonlyArray // Defaults to [] 328 | } 329 | 330 | interface VectorPath { 331 | readonly windingRule: WindingRule | "NONE" 332 | readonly data: string 333 | } 334 | 335 | type VectorPaths = ReadonlyArray 336 | 337 | interface LetterSpacing { 338 | readonly value: number 339 | readonly unit: "PIXELS" | "PERCENT" 340 | } 341 | 342 | type LineHeight = { 343 | readonly value: number 344 | readonly unit: "PIXELS" | "PERCENT" 345 | } | { 346 | readonly unit: "AUTO" 347 | } 348 | 349 | type BlendMode = 350 | "PASS_THROUGH" | 351 | "NORMAL" | 352 | "DARKEN" | 353 | "MULTIPLY" | 354 | "LINEAR_BURN" | 355 | "COLOR_BURN" | 356 | "LIGHTEN" | 357 | "SCREEN" | 358 | "LINEAR_DODGE" | 359 | "COLOR_DODGE" | 360 | "OVERLAY" | 361 | "SOFT_LIGHT" | 362 | "HARD_LIGHT" | 363 | "DIFFERENCE" | 364 | "EXCLUSION" | 365 | "HUE" | 366 | "SATURATION" | 367 | "COLOR" | 368 | "LUMINOSITY" 369 | 370 | interface Font { 371 | fontName: FontName 372 | } 373 | 374 | //////////////////////////////////////////////////////////////////////////////// 375 | // Mixins 376 | 377 | interface BaseNodeMixin { 378 | readonly id: string 379 | readonly parent: (BaseNode & ChildrenMixin) | null 380 | name: string // Note: setting this also sets \`autoRename\` to false on TextNodes 381 | readonly removed: boolean 382 | toString(): string 383 | remove(): void 384 | 385 | getPluginData(key: string): string 386 | setPluginData(key: string, value: string): void 387 | 388 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 389 | // be a name related to your plugin. Other plugins will be able to read this data. 390 | getSharedPluginData(namespace: string, key: string): string 391 | setSharedPluginData(namespace: string, key: string, value: string): void 392 | } 393 | 394 | interface SceneNodeMixin { 395 | visible: boolean 396 | locked: boolean 397 | } 398 | 399 | interface ChildrenMixin { 400 | readonly children: ReadonlyArray 401 | 402 | appendChild(child: SceneNode): void 403 | insertChild(index: number, child: SceneNode): void 404 | 405 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 406 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 407 | } 408 | 409 | interface ConstraintMixin { 410 | constraints: Constraints 411 | } 412 | 413 | interface LayoutMixin { 414 | readonly absoluteTransform: Transform 415 | relativeTransform: Transform 416 | x: number 417 | y: number 418 | rotation: number // In degrees 419 | 420 | readonly width: number 421 | readonly height: number 422 | 423 | resize(width: number, height: number): void 424 | resizeWithoutConstraints(width: number, height: number): void 425 | } 426 | 427 | interface BlendMixin { 428 | opacity: number 429 | blendMode: BlendMode 430 | isMask: boolean 431 | effects: ReadonlyArray 432 | effectStyleId: string 433 | } 434 | 435 | interface FrameMixin { 436 | backgrounds: ReadonlyArray 437 | layoutGrids: ReadonlyArray 438 | clipsContent: boolean 439 | guides: ReadonlyArray 440 | gridStyleId: string 441 | backgroundStyleId: string 442 | } 443 | 444 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 445 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 446 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 447 | 448 | interface GeometryMixin { 449 | fills: ReadonlyArray | symbol 450 | strokes: ReadonlyArray 451 | strokeWeight: number 452 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 453 | strokeCap: StrokeCap | symbol 454 | strokeJoin: StrokeJoin | symbol 455 | dashPattern: ReadonlyArray 456 | fillStyleId: string | symbol 457 | strokeStyleId: string 458 | } 459 | 460 | interface CornerMixin { 461 | cornerRadius: number | symbol 462 | cornerSmoothing: number 463 | } 464 | 465 | interface ExportMixin { 466 | exportSettings: ReadonlyArray 467 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 468 | } 469 | 470 | interface DefaultShapeMixin extends 471 | BaseNodeMixin, SceneNodeMixin, 472 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 473 | } 474 | 475 | interface DefaultContainerMixin extends 476 | BaseNodeMixin, SceneNodeMixin, 477 | ChildrenMixin, FrameMixin, 478 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 479 | } 480 | 481 | //////////////////////////////////////////////////////////////////////////////// 482 | // Nodes 483 | 484 | interface DocumentNode extends BaseNodeMixin { 485 | readonly type: "DOCUMENT" 486 | 487 | readonly children: ReadonlyArray 488 | 489 | appendChild(child: PageNode): void 490 | insertChild(index: number, child: PageNode): void 491 | 492 | findAll(callback?: (node: (PageNode | SceneNode)) => boolean): Array 493 | findOne(callback: (node: (PageNode | SceneNode)) => boolean): PageNode | SceneNode | null 494 | } 495 | 496 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 497 | readonly type: "PAGE" 498 | clone(): PageNode 499 | 500 | guides: ReadonlyArray 501 | selection: ReadonlyArray 502 | 503 | backgrounds: ReadonlyArray 504 | } 505 | 506 | interface FrameNode extends DefaultContainerMixin { 507 | readonly type: "FRAME" | "GROUP" 508 | clone(): FrameNode 509 | } 510 | 511 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 512 | readonly type: "SLICE" 513 | clone(): SliceNode 514 | } 515 | 516 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 517 | readonly type: "RECTANGLE" 518 | clone(): RectangleNode 519 | topLeftRadius: number 520 | topRightRadius: number 521 | bottomLeftRadius: number 522 | bottomRightRadius: number 523 | } 524 | 525 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 526 | readonly type: "LINE" 527 | clone(): LineNode 528 | } 529 | 530 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 531 | readonly type: "ELLIPSE" 532 | clone(): EllipseNode 533 | arcData: ArcData 534 | } 535 | 536 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 537 | readonly type: "POLYGON" 538 | clone(): PolygonNode 539 | pointCount: number 540 | } 541 | 542 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 543 | readonly type: "STAR" 544 | clone(): StarNode 545 | pointCount: number 546 | innerRadius: number 547 | } 548 | 549 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 550 | readonly type: "VECTOR" 551 | clone(): VectorNode 552 | vectorNetwork: VectorNetwork 553 | vectorPaths: VectorPaths 554 | handleMirroring: HandleMirroring | symbol 555 | } 556 | 557 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 558 | readonly type: "TEXT" 559 | clone(): TextNode 560 | characters: string 561 | readonly hasMissingFont: boolean 562 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 563 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 564 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 565 | paragraphIndent: number 566 | paragraphSpacing: number 567 | autoRename: boolean 568 | 569 | textStyleId: string | symbol 570 | fontSize: number | symbol 571 | fontName: FontName | symbol 572 | textCase: TextCase | symbol 573 | textDecoration: TextDecoration | symbol 574 | letterSpacing: LetterSpacing | symbol 575 | lineHeight: LineHeight | symbol 576 | 577 | getRangeFontSize(start: number, end: number): number | symbol 578 | setRangeFontSize(start: number, end: number, value: number): void 579 | getRangeFontName(start: number, end: number): FontName | symbol 580 | setRangeFontName(start: number, end: number, value: FontName): void 581 | getRangeTextCase(start: number, end: number): TextCase | symbol 582 | setRangeTextCase(start: number, end: number, value: TextCase): void 583 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 584 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 585 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 586 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 587 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 588 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 589 | getRangeFills(start: number, end: number): Paint[] | symbol 590 | setRangeFills(start: number, end: number, value: Paint[]): void 591 | getRangeTextStyleId(start: number, end: number): string | symbol 592 | setRangeTextStyleId(start: number, end: number, value: string): void 593 | getRangeFillStyleId(start: number, end: number): string | symbol 594 | setRangeFillStyleId(start: number, end: number, value: string): void 595 | } 596 | 597 | interface ComponentNode extends DefaultContainerMixin { 598 | readonly type: "COMPONENT" 599 | clone(): ComponentNode 600 | 601 | createInstance(): InstanceNode 602 | description: string 603 | readonly remote: boolean 604 | readonly key: string // The key to use with "importComponentByKeyAsync" 605 | } 606 | 607 | interface InstanceNode extends DefaultContainerMixin { 608 | readonly type: "INSTANCE" 609 | clone(): InstanceNode 610 | masterComponent: ComponentNode 611 | } 612 | 613 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 614 | readonly type: "BOOLEAN_OPERATION" 615 | clone(): BooleanOperationNode 616 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 617 | } 618 | 619 | type BaseNode = 620 | DocumentNode | 621 | PageNode | 622 | SceneNode 623 | 624 | type SceneNode = 625 | SliceNode | 626 | FrameNode | 627 | ComponentNode | 628 | InstanceNode | 629 | BooleanOperationNode | 630 | VectorNode | 631 | StarNode | 632 | LineNode | 633 | EllipseNode | 634 | PolygonNode | 635 | RectangleNode | 636 | TextNode 637 | 638 | type NodeType = 639 | "DOCUMENT" | 640 | "PAGE" | 641 | "SLICE" | 642 | "FRAME" | 643 | "GROUP" | 644 | "COMPONENT" | 645 | "INSTANCE" | 646 | "BOOLEAN_OPERATION" | 647 | "VECTOR" | 648 | "STAR" | 649 | "LINE" | 650 | "ELLIPSE" | 651 | "POLYGON" | 652 | "RECTANGLE" | 653 | "TEXT" 654 | 655 | //////////////////////////////////////////////////////////////////////////////// 656 | // Styles 657 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 658 | 659 | interface BaseStyle { 660 | readonly id: string 661 | readonly type: StyleType 662 | name: string 663 | description: string 664 | remote: boolean 665 | readonly key: string // The key to use with "importStyleByKeyAsync" 666 | remove(): void 667 | } 668 | 669 | interface PaintStyle extends BaseStyle { 670 | type: "PAINT" 671 | paints: ReadonlyArray 672 | } 673 | 674 | interface TextStyle extends BaseStyle { 675 | type: "TEXT" 676 | fontSize: number 677 | textDecoration: TextDecoration 678 | fontName: FontName 679 | letterSpacing: LetterSpacing 680 | lineHeight: LineHeight 681 | paragraphIndent: number 682 | paragraphSpacing: number 683 | textCase: TextCase 684 | } 685 | 686 | interface EffectStyle extends BaseStyle { 687 | type: "EFFECT" 688 | effects: ReadonlyArray 689 | } 690 | 691 | interface GridStyle extends BaseStyle { 692 | type: "GRID" 693 | layoutGrids: ReadonlyArray 694 | } 695 | 696 | //////////////////////////////////////////////////////////////////////////////// 697 | // Other 698 | 699 | interface Image { 700 | readonly hash: string 701 | getBytesAsync(): Promise 702 | } -------------------------------------------------------------------------------- /graphviz/src/figplug.d.ts: -------------------------------------------------------------------------------- 1 | // Helpers provided automatically, as needed, by figplug. 2 | 3 | // symbolic type aliases 4 | type int = number 5 | type float = number 6 | type byte = number 7 | type bool = boolean 8 | 9 | // compile-time constants 10 | declare const DEBUG :boolean 11 | declare const VERSION :string 12 | 13 | // global namespace. Same as `window` in a regular web context. 14 | declare const global :{[k:string]:any} 15 | 16 | // panic prints a message, stack trace and exits the process 17 | // 18 | declare function panic(msg :any, ...v :any[]) :void 19 | 20 | // repr returns a detailed string representation of the input 21 | // 22 | declare function repr(obj :any) :string 23 | 24 | // print works just like console.log 25 | declare function print(msg :any, ...v :any[]) :void 26 | 27 | // dlog works just like console.log but is stripped out from non-debug builds 28 | declare function dlog(msg :any, ...v :any[]) :void 29 | 30 | // assert checks the condition for truth, and if false, prints an optional 31 | // message, stack trace and exits the process. 32 | // assert is removed in release builds 33 | declare var assert :AssertFun 34 | declare var AssertionError :ErrorConstructor 35 | declare interface AssertFun { 36 | (cond :any, msg? :string, cons? :Function) :void 37 | 38 | // throws can be set to true to cause assertions to be thrown as exceptions, 39 | // or set to false to cause the process to exit. 40 | // Only has an effect in Nodejs-like environments. 41 | // false by default. 42 | throws :bool 43 | } 44 | -------------------------------------------------------------------------------- /graphviz/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Msg, UpdateUIMsg, UpdateGraphMsg, ResponseMsg, ErrorMsg } from "./structs" 2 | 3 | 4 | // TODO: 5 | // on startup and on selection change: 6 | // - getPluginData("viz.source") 7 | // - if there's source, send message to UI 8 | // - have UI update the source 9 | // 10 | // Need to figure out how to not have source disappear if the user enters some source 11 | // and then clicks on a graph on the canvas. 12 | // - idea: textarea.oninput => store value in localStorage, 13 | // when there's no graph selected: textarea.value = get localStorage. 14 | // 15 | 16 | function importGraphSvg(svg :string) :FrameNode { 17 | let n = figma.createNodeFromSvg(svg) 18 | // TODO: ungroup the single group child of n 19 | // // expect a single child: a group ("graph1") 20 | // if (n.children.length == 1 && n.children[0].type == "GROUP") { 21 | // let g = n.children[0] as FrameNode & { type: "GROUP" } 22 | // UNGROUP 23 | // } 24 | return n 25 | } 26 | 27 | 28 | class GraphFrame { 29 | n :FrameNode 30 | sourceCode :string 31 | 32 | constructor(n :FrameNode, sourceCode :string) { 33 | this.n = n 34 | this.sourceCode = sourceCode 35 | } 36 | 37 | update(msg :UpdateGraphMsg) { 38 | // For now, replace all. 39 | // 40 | // We could do something more efficient and more useful here. 41 | // - derive a hash for each part of the svg (could be done in UI with parseHtml) 42 | // - store hash using PluginData on generated nodes 43 | // - skip nodes with identical hash 44 | // 45 | 46 | // "import" SVG 47 | let n = importGraphSvg(msg.svgCode) 48 | 49 | // remove contents of existing frame 50 | this.n.children.map(c => c.remove()) 51 | 52 | // update size of frame to match new size 53 | this.n.resizeWithoutConstraints(n.width, n.height) 54 | 55 | // add contents of newly imported SVG 56 | for (let c of n.children) { 57 | this.n.appendChild(c) 58 | } 59 | 60 | // remove now-empty frame 61 | n.remove() 62 | 63 | // update viz source code 64 | this.n.setPluginData("viz.source", msg.sourceCode) 65 | } 66 | 67 | toString() { 68 | return `GraphFrame(n.id=${this.n.id})` 69 | } 70 | } 71 | 72 | 73 | // currently selected graph frame 74 | let selGraphFrame :GraphFrame|null = null 75 | 76 | 77 | function setSelectedGraphFrame(gf :GraphFrame|null) { 78 | if (selGraphFrame !== gf) { 79 | // dlog(`set selGraphFrame ${selGraphFrame} -> ${gf}`) 80 | selGraphFrame = gf 81 | sendmsg({ 82 | type: "update-ui", 83 | nodeId: selGraphFrame ? selGraphFrame.n.id : "", 84 | sourceCode: selGraphFrame ? selGraphFrame.sourceCode : "" 85 | }) 86 | } 87 | } 88 | 89 | 90 | function updateSelectedGraphFrame() { 91 | if (figma.currentPage.selection.length == 1) { 92 | let n = figma.currentPage.selection[0] 93 | if (selGraphFrame && selGraphFrame.n === n) { 94 | // already selected 95 | return 96 | } 97 | if (n.type == "FRAME") { 98 | let sourceCode = n.getPluginData("viz.source") 99 | if (sourceCode) { 100 | setSelectedGraphFrame(new GraphFrame(n, sourceCode)) 101 | return 102 | } 103 | } 104 | } 105 | setSelectedGraphFrame(null) 106 | } 107 | 108 | 109 | function findSvgGraphTitle(svg :string) :string { 110 | const titleChunk = "" 111 | let titleStart = svg.indexOf(titleChunk) 112 | if (titleStart != -1) { 113 | titleStart += titleChunk.length 114 | let titleEnd = svg.indexOf("</", titleStart) 115 | if (titleEnd != -1) { 116 | let title = svg.substring(titleStart, titleEnd) 117 | if (title != "%0") { 118 | return title 119 | } 120 | } 121 | } 122 | return "" 123 | } 124 | 125 | 126 | function createNewGraph(msg :UpdateGraphMsg) { 127 | let n = importGraphSvg(msg.svgCode) 128 | 129 | // get name from SVG graph title (i.e. from "graph Name {...") 130 | n.name = findSvgGraphTitle(msg.svgCode) || "Graph" 131 | 132 | let vp = figma.viewport.center 133 | n.x = vp.x 134 | n.y = vp.y 135 | 136 | let sourceCode = msg.sourceCode 137 | n.setPluginData("viz.source", sourceCode) 138 | 139 | figma.currentPage.appendChild(n) 140 | figma.currentPage.selection = [ n ] 141 | 142 | setSelectedGraphFrame(new GraphFrame(n, sourceCode)) 143 | } 144 | 145 | 146 | function onUpdateGraph(msg :UpdateGraphMsg) { 147 | let error :undefined|string 148 | try { 149 | let timeStarted = Date.now() 150 | if (!msg.forceInsertNew && selGraphFrame) { 151 | selGraphFrame.update(msg) 152 | } else { 153 | createNewGraph(msg) 154 | } 155 | print(`graphviz integration finished in ${(Date.now()-timeStarted).toFixed(0)}ms`) 156 | } catch (err) { 157 | console.error("[graphviz plugin] " + (err.stack||err)) 158 | error = ""+err 159 | } 160 | sendmsg<ResponseMsg>({ 161 | type: "response", 162 | reqId: msg.reqId, 163 | error, 164 | }) 165 | } 166 | 167 | 168 | function onUIError(msg :ErrorMsg) { 169 | figma.notify(msg.error) 170 | } 171 | 172 | 173 | function sendmsg<T extends Msg>(msg :T) { 174 | // send message to ui 175 | figma.ui.postMessage(msg) 176 | } 177 | 178 | 179 | function main() { 180 | figma.showUI(__html__, { 181 | width: 440, 182 | height: 600, 183 | }) 184 | 185 | figma.ui.onmessage = msg => { 186 | switch (msg.type) { 187 | 188 | case "update-graph": 189 | onUpdateGraph(msg as UpdateGraphMsg) 190 | break 191 | 192 | case "error": 193 | onUIError(msg as ErrorMsg) 194 | break 195 | 196 | case "close-plugin": 197 | figma.closePlugin() 198 | break 199 | 200 | default: 201 | console.warn(`plugin received unexpected message`, msg) 202 | break 203 | } 204 | } 205 | 206 | figma.on("selectionchange", updateSelectedGraphFrame) 207 | 208 | // initial check 209 | updateSelectedGraphFrame() 210 | } 211 | 212 | 213 | main() 214 | -------------------------------------------------------------------------------- /graphviz/src/structs.ts: -------------------------------------------------------------------------------- 1 | export interface Msg { 2 | type :string 3 | } 4 | 5 | export interface UpdateGraphMsg extends Msg { 6 | type :"update-graph" 7 | reqId :number 8 | svgCode :string 9 | sourceCode :string 10 | forceInsertNew :bool // don't attempt to replace exisiting graph 11 | } 12 | 13 | export interface ResponseMsg extends Msg { 14 | type :"response" 15 | reqId :number 16 | error? :string 17 | } 18 | 19 | export interface ErrorMsg extends Msg { 20 | type: "error" 21 | error: string 22 | } 23 | 24 | export interface ClosePluginMsg extends Msg { 25 | type: "close-plugin" 26 | } 27 | 28 | export interface UpdateUIMsg extends Msg { 29 | type: "update-ui" 30 | nodeId :string // non-empty when a graph node is selected 31 | sourceCode :string // valid when nodeId is set 32 | } 33 | -------------------------------------------------------------------------------- /graphviz/src/ui.css: -------------------------------------------------------------------------------- 1 | @import url("https://rsms.me/inter/inter.css"); 2 | @import url("https://rsms.me/res/fonts/iaw.css"); 3 | 4 | /* reset */ 5 | * { font-family: inherit; line-height: inherit; font-synthesis: none; } 6 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, 7 | body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, 8 | em, embed, fieldset, figcaption, figure, footer, form, grid, h1, h2, h3, h4, h5, 9 | h6, header, hgroup, hr, html, i, iframe, img, ins, kbd, label, legend, li, main, 10 | mark, menu, nav, noscript, object, ol, output, p, pre, q, s, samp, section, 11 | small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, 12 | thead, time, tr, tt, u, ul, var, video { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | vertical-align: baseline; 17 | } 18 | blockquote, q { quotes: none; } 19 | blockquote:before, blockquote:after, q:before, q:after { 20 | content: ""; 21 | content: none; 22 | } 23 | table { 24 | border-collapse: collapse; 25 | border-spacing: 0; 26 | } 27 | a, a:active, a:visited { color: inherit; } 28 | /* end of reset */ 29 | 30 | :root { 31 | --blue: #0085ff; 32 | --fontSize: 12px; 33 | --editorFontSize: var(--fontSize); 34 | --fontFamily: Inter; 35 | --editorFontFamily: 'iaw-quattro'; 36 | --toolbarHeight: 40px; 37 | } 38 | @supports (font-variation-settings: normal) { 39 | :root { 40 | --fontFamily: "Inter var"; 41 | --editorFontFamily: 'iaw-quattro-var'; 42 | } 43 | } 44 | 45 | body { 46 | background: transparent; 47 | color: #222; 48 | font: var(--fontSize)/1.4 var(--fontFamily), system-ui, -system-ui, sans-serif; 49 | display: flex; 50 | flex-direction: column; 51 | } 52 | 53 | textarea { 54 | flex:1 1 auto; 55 | min-height:100px; 56 | font-family: var(--editorFontFamily); 57 | font-size: var(--editorFontSize); 58 | line-height: 1.4; 59 | border: none; 60 | outline: none; 61 | padding: 8px 16px; 62 | resize: none; 63 | 64 | color: #666; 65 | 66 | &:focus { 67 | /*box-shadow: inset 0 0 0 2px rgba(0, 100, 255, 0.2);*/ 68 | color: #000; 69 | } 70 | } 71 | 72 | #toolbar { 73 | height: var(--toolbarHeight); 74 | display: flex; 75 | justify-content: space-evenly; 76 | box-shadow: 0 -1px 0 0 rgba(0,0,0,0.1); 77 | z-index: 1; 78 | 79 | & button { 80 | flex: 1 1 50%; 81 | background: none; 82 | border: none; 83 | border-left: 1px solid #e5e5e5; 84 | padding: 0 16px; 85 | line-height: var(--toolbarHeight); 86 | font-weight: 500; 87 | font-size: inherit; 88 | white-space: nowrap; 89 | 90 | &:active { 91 | background: rgba(0,0,0,0.07); 92 | } 93 | &:focus { 94 | color:green; 95 | } 96 | 97 | &:first-child { border: none } 98 | &.primary { 99 | /*font-weight: 600;*/ 100 | color: var(--blue); 101 | } 102 | } 103 | } 104 | 105 | @keyframes spin { 106 | 0% { transform: rotate(0deg); } 107 | 100% { transform: rotate(360deg); } 108 | } 109 | 110 | #spinner { 111 | position: fixed; 112 | left:0; top:0; right:0; bottom:48px; 113 | z-index: 9; 114 | pointer-events: none; 115 | display: flex; 116 | align-items: center; 117 | justify-content: center; 118 | opacity: 0; 119 | transition: opacity 100ms ease-in-out; 120 | & > div { 121 | position:relative; 122 | width:24px; 123 | height:24px; 124 | & svg { 125 | position:absolute; 126 | left:0; top:0; 127 | &.shadow { 128 | /* note: we can't use transform here */ 129 | margin-top: 5px; 130 | filter: blur(1.5px); 131 | } 132 | &.shadow path { 133 | stroke: rgba(0, 0, 100, 0.1); 134 | } 135 | } 136 | } 137 | } 138 | #spinner.active { 139 | opacity: 1; 140 | transition-delay: 400ms; 141 | transition-duration: 200ms; 142 | 143 | & svg { 144 | animation-name: spin; 145 | animation-duration: 800ms; 146 | animation-iteration-count: infinite; 147 | animation-timing-function: linear; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /graphviz/src/ui.html: -------------------------------------------------------------------------------- 1 | <head> 2 | <meta charset="utf-8"> 3 | <script src="https://rsms.me/graphviz/graphviz.js"></script> 4 | </head> 5 | <body> 6 | <textarea id="dotcode" autocomplete="on" autocapitalize="none" autofocus wrap="off"> 7 | # Attributes at top level apply to the graph itself. 8 | outputorder=edgesfirst 9 | pad="0.25" 10 | #bgcolor=hotpink 11 | 12 | # layout= specifies a layout engine: 13 | # circo — for circular layout of graphs 14 | # dot — for drawing directed graphs (the default) 15 | # fdp — for drawing undirected graphs 16 | # neato — for drawing undirected graphs 17 | # osage — for drawing large undirected graphs 18 | # twopi — for radial layouts of graphs 19 | layout=neato 20 | #layout=dot 21 | #layout=twopi 22 | 23 | # Default node attributes 24 | node [ 25 | shape = circle 26 | style = "filled,bold" 27 | color = black 28 | fillcolor = "#F2F2F2" 29 | ] 30 | 31 | # Uncomment this to hide labels 32 | #node [ label="" ] 33 | 34 | # Uncomment this to arrange nodes in a grid 35 | #layout=osage edge [style=invis] 36 | 37 | # Edges 38 | A -> C 39 | B -> { C, D, F } 40 | C -> H 41 | D -> { F, G } 42 | E -> { F, G, J } 43 | F -> I 44 | G -> L 45 | H -> K 46 | I -> K 47 | J -> M 48 | K -> N 49 | L -> N 50 | M -> N 51 | N -> O 52 | 53 | # Node attributes 54 | A [ fillcolor = "#ECD1C9" ] 55 | B [ fillcolor = "#FBB5AE" ] 56 | C [ fillcolor = "#FFEFBC" ] 57 | D [ fillcolor = "#B7D1DF" ] 58 | E [ fillcolor = "#D1E2CE" ] 59 | F [ fillcolor = "#FADAE5" ] 60 | G [ fillcolor = "#ECE3D5" ] 61 | H [ fillcolor = "#F2F2F2" ] 62 | I [ fillcolor = "#ECE3C1" ] 63 | J [ fillcolor = "#BEDFC8" ] 64 | K [ fillcolor = "#F9F2B6" ] 65 | L [ fillcolor = "#EFD0BD" ] 66 | M [ fillcolor = "#DDD0E5" ] 67 | N [ fillcolor = "#F2E4C8" ] 68 | O [ fillcolor = "#CBCBCB" ] 69 | </textarea> 70 | <div id="toolbar"> 71 | <button class="playground">Open in playground</button> 72 | <button class="demo">Open examples</button> 73 | <!-- <button class="gen-and-close">Create & close</button> --> 74 | <button class="gen primary">Create</button> 75 | </div> 76 | <div id="spinner"> 77 | <div> 78 | <svg class="shadow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 79 | <path d="M22.5 12C22.5 14.0767 21.8842 16.1068 20.7304 17.8335C19.5767 19.5602 17.9368 20.906 16.0182 21.7007C14.0996 22.4955 11.9884 22.7034 9.95155 22.2982C7.91475 21.8931 6.04383 20.8931 4.57538 19.4246C3.10693 17.9562 2.1069 16.0852 1.70175 14.0484C1.29661 12.0116 1.50454 9.90045 2.29926 7.98182C3.09399 6.0632 4.4398 4.42332 6.16651 3.26957C7.89323 2.11581 9.9233 1.5 12 1.5" stroke="#0085FF" stroke-width="2"/> 80 | </svg> 81 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 82 | <path d="M22.5 12C22.5 14.0767 21.8842 16.1068 20.7304 17.8335C19.5767 19.5602 17.9368 20.906 16.0182 21.7007C14.0996 22.4955 11.9884 22.7034 9.95155 22.2982C7.91475 21.8931 6.04383 20.8931 4.57538 19.4246C3.10693 17.9562 2.1069 16.0852 1.70175 14.0484C1.29661 12.0116 1.50454 9.90045 2.29926 7.98182C3.09399 6.0632 4.4398 4.42332 6.16651 3.26957C7.89323 2.11581 9.9233 1.5 12 1.5" stroke="#0085FF" stroke-width="2"/> 83 | </svg> 84 | </div> 85 | </div> 86 | </body> 87 | -------------------------------------------------------------------------------- /graphviz/src/ui.ts: -------------------------------------------------------------------------------- 1 | import { Msg, ClosePluginMsg, UpdateUIMsg, UpdateGraphMsg, ResponseMsg, ErrorMsg } from "./structs" 2 | import { Editor } from "./editor" 3 | import { addKeyEventHandler } from "./util" 4 | 5 | // declare function Viz(dotSource :string, outputFormat :string) 6 | 7 | // graphviz module 8 | type GVFormat = "svg" | "dot" | "json" | "dot_json" | "xdot_json"; 9 | type GVEngine = "circo" | "dot" | "fdp" | "neato" | "osage" | "patchwork" | "twopi"; 10 | const graphviz = window["graphviz"] as { 11 | layout(source :string, format? :GVFormat, engine? :GVEngine, timeout? :number) :Promise<string> 12 | } 13 | 14 | 15 | const isMac = navigator.platform.indexOf("Mac") != -1 16 | const genButton = document.querySelector('button.gen')! as HTMLButtonElement 17 | const playgroundButton = document.querySelector('button.playground')! as HTMLButtonElement 18 | const demoButton = document.querySelector('button.demo')! as HTMLButtonElement 19 | const spinner = document.querySelector('#spinner')! as HTMLDivElement 20 | const editor = new Editor(document.getElementById('dotcode')! as HTMLTextAreaElement) 21 | 22 | // memory-only, since we can't use localStorage in plugins 23 | let untitledSourceCode = editor.defaultText 24 | 25 | // genButton labels 26 | const genButtonLabelCreate = genButton.innerText 27 | const genButtonLabelUpdate = "Update" 28 | const genButtonLabelBusy = "Working" 29 | let genButtonLabel = genButtonLabelCreate 30 | 31 | 32 | const graphDefaults = ( 33 | ' graph [fontname="Arial,Inter" bgcolor=transparent];\n' + 34 | ' node [fontname="Arial,Inter"];\n' + 35 | ' edge [fontname="Arial,Inter"];\n' 36 | ) 37 | 38 | 39 | function wrapInGraphDirective(s :string) :string { 40 | return ( 41 | 'digraph G {\n' + 42 | graphDefaults + 43 | s + 44 | '\n}\n' 45 | ) 46 | } 47 | 48 | 49 | async function makeViz(dotSource :string) :Promise<string> { 50 | let svg = "" 51 | let originalDotSource = dotSource 52 | 53 | let addedGraphDirective = false 54 | let m = dotSource.match(/\b(?:di)?graph(?:\s+[^\{]+|)[\r\n\s]*\{/) 55 | if (m) { 56 | // found graph directive -- add defaults 57 | let i = (m.index||0) + m[0].length 58 | dotSource = dotSource.substr(0, i) + "\n" + graphDefaults + dotSource.substr(i) 59 | } else { 60 | // no graph directive -- wrap & add defaults 61 | dotSource = wrapInGraphDirective(dotSource) 62 | addedGraphDirective = true 63 | } 64 | 65 | while (1) { 66 | try { 67 | // svg = Viz(dotSource, "svg") 68 | svg = await graphviz.layout(dotSource, "svg", "dot", 30000) 69 | break 70 | } catch (err) { 71 | if (err.message && (err.message+"").toLowerCase().indexOf("syntax error") != -1) { 72 | if (!addedGraphDirective) { 73 | // try and see if adding graph directive fixes it 74 | dotSource = wrapInGraphDirective(originalDotSource) 75 | addedGraphDirective = true 76 | dlog("makeViz retry with wrapped graph directive. New dotSource:\n" + dotSource) 77 | } else { 78 | throw new Error("malformed dot code") 79 | } 80 | } else { 81 | throw err 82 | } 83 | } 84 | } 85 | 86 | // clean up svg 87 | // <?xml version="1.0" encoding="UTF-8" standalone="no"?> 88 | // <!DOCTYPE svg PUBLIC ...> 89 | // <!-- comments --> 90 | // <title>... 91 | // xmlns:xlink="http://www.w3.org/1999/xlink" 92 | // (useless rectangle) 93 | svg = svg.replace( 94 | /<\?xml[^>]+\?>|<\!DOCTYPE[^>]+>|<\!--.*-->|xmlns:xlink="http:\/\/www.w3.org\/1999\/xlink\"/gm, 95 | "" 96 | ) 97 | 98 | // remove comments and collapse linebreaks. 99 | // Note that none of the data generated actually has linebreaks, so this is safe. 100 | svg = svg.replace(/[\r\n]+/g, " ").replace(/<\!--.*-->/g, "").trim() 101 | 102 | // remove background rectangle 103 | svg = svg.replace( 104 | /^(]+>\s*]+>)\s*/, 105 | "$1" 106 | ) 107 | 108 | // replace fontname 109 | svg = svg.replace(/"Arial,Inter"/g, '"Inter"') 110 | 111 | // scale? 112 | let scale = [1,1] 113 | m = dotSource.match(/(?:^|\n)\s*scale\s*=\s*([\d"',]+);?/im) 114 | if (m) { 115 | scale = m[1].replace(/[^\d\.]/g, " ").trim().split(" ").map(parseFloat) 116 | if (scale.length == 1) { 117 | scale[1] = scale[0] 118 | } 119 | // class="graph" transform="scale(1 1) rotate(0) translate(72 374)" 120 | let i = svg.indexOf('class="graph" transform="') 121 | if (i != -1) { 122 | i += 'class="graph" transform="'.length 123 | svg = svg.substr(0, i) + `scale(${scale[0]} ${scale[1]}) ` + svg.substr(i) 124 | } 125 | } 126 | 127 | // update size if scale != 1 128 | if (scale[0] != 1 || scale[1] != 1) { 129 | // extract width & height 130 | // { 135 | let f = m.slice(0, 6).map(parseFloat) 136 | if (f.some(isNaN)) { 137 | return substr 138 | } 139 | width = Math.ceil(f[0] * scale[0]) 140 | height = Math.ceil(f[1] * scale[1]) 141 | return `(r => setTimeout(r, 1000)) 147 | // dlog(dotSource + "\n\n-> svg ->\n\n" + JSON.stringify(svg)) 148 | 149 | return svg 150 | } 151 | 152 | 153 | let isGeneratingGraph = false 154 | let generateAgainImmediately = false 155 | let nextReqId = 0 156 | 157 | 158 | async function genGraph() { 159 | if (isGeneratingGraph) { 160 | generateAgainImmediately = true 161 | return 162 | } 163 | print(`graphviz start`) 164 | 165 | isGeneratingGraph = true 166 | let reqId = nextReqId++ 167 | 168 | // Add active class to spinner, which is set to appear after a 400ms delay, 169 | // meaning that if we finish within that delay, the user never sees the spinner. 170 | spinner.classList.add("active") 171 | 172 | try { 173 | let timeStarted = Date.now() 174 | let sourceCode = editor.text 175 | let svgCode = await makeViz(sourceCode) 176 | sendmsg({ 177 | type: 'update-graph', 178 | reqId, 179 | svgCode, 180 | sourceCode, 181 | forceInsertNew: false, 182 | }) 183 | print(`graphviz layout completed in ${(Date.now()-timeStarted).toFixed(0)}ms`) 184 | await awaitResponse(reqId) 185 | print(`graphviz finished in ${(Date.now()-timeStarted).toFixed(0)}ms`) 186 | } catch (err) { 187 | sendmsg({ 188 | type: "error", 189 | error: err.message, 190 | }) 191 | } 192 | 193 | spinner.classList.remove("active") 194 | isGeneratingGraph = false 195 | 196 | // was a request made to genGraph while we were working? 197 | // if so, schedule a call to genGraph ASAP. 198 | if (generateAgainImmediately) { 199 | generateAgainImmediately = false 200 | setTimeout(genGraph, 1) 201 | } 202 | } 203 | 204 | 205 | interface PromiseResolver { 206 | resolve :()=>void 207 | reject :(e:any)=>void 208 | } 209 | let waitingForResponses = new Map() 210 | 211 | 212 | function awaitResponse(reqId :number) { 213 | if (waitingForResponses.has(reqId)) { 214 | throw new Error(`duplicate reqId ${reqId}`) 215 | } 216 | return new Promise((resolve, reject) => { 217 | waitingForResponses.set(reqId, {resolve, reject}) 218 | }) 219 | } 220 | 221 | 222 | function resolveResponse(msg :ResponseMsg) { 223 | let pr = waitingForResponses.get(msg.reqId) 224 | if (!pr) { 225 | console.warn(`resolveResponse did not find entry for reqId ${msg.reqId}`) 226 | return 227 | } 228 | waitingForResponses.delete(msg.reqId) 229 | if (msg.error) { 230 | pr.reject(new Error(msg.error)) 231 | } else { 232 | pr.resolve() 233 | } 234 | } 235 | 236 | 237 | function updateGenButton(label? :string) { 238 | if (label) { 239 | genButtonLabel = label 240 | } 241 | genButton.innerText = genButton.disabled ? genButtonLabelBusy : genButtonLabel 242 | } 243 | 244 | 245 | function onUpdateUI(msg :UpdateUIMsg) { 246 | // called by the plugin when the selection changes 247 | if (msg.nodeId) { 248 | // selection is an existing graph 249 | updateGenButton(genButtonLabelUpdate) 250 | editor.text = msg.sourceCode 251 | } else { 252 | updateGenButton(genButtonLabelCreate) 253 | editor.text = loadUntitledSourceCode() 254 | } 255 | // for now, avoid focusing as it steals inputs from interacting with Figma canvas 256 | // editor.focus() 257 | } 258 | 259 | 260 | function loadUntitledSourceCode() :string { 261 | return untitledSourceCode 262 | } 263 | 264 | function saveUntitledSourceCode(source :string) { 265 | untitledSourceCode = source 266 | } 267 | 268 | 269 | function sendmsg(msg :T) { 270 | // send message to plugin 271 | parent.postMessage({ pluginMessage: msg }, '*') 272 | } 273 | 274 | 275 | function closePlugin() { 276 | sendmsg({ type: "close-plugin" }) 277 | } 278 | 279 | 280 | function setupEventHandlers() { 281 | document.addEventListener("focus", ev => { 282 | if (ev.target !== editor.ta) { 283 | // requestAnimationFrame(() => editor.focus()) 284 | ev.stopPropagation() 285 | ev.preventDefault() 286 | editor.focus() 287 | } 288 | }, {passive:false,capture:true}) 289 | 290 | // handle ESC-ESC to close 291 | let lastEscapeKeypress = 0 292 | 293 | // escapeToCloseThreshold 294 | // When ESC is pressed at least twice within this time window, the plugin closes. 295 | const escapeToCloseThreshold = 150 296 | 297 | addKeyEventHandler(window, (ev :KeyboardEvent, key :string) => { 298 | if ((ev.metaKey || ev.ctrlKey) && key == "Enter") { 299 | // meta-return: generate graph 300 | genGraph() 301 | return true 302 | } else if (key == "Escape") { 303 | // ESC-ESC: close plugin 304 | if (!ev.metaKey && !ev.ctrlKey && !ev.altKey && !ev.shiftKey) { 305 | if (ev.timeStamp - lastEscapeKeypress <= escapeToCloseThreshold) { 306 | closePlugin() 307 | return true 308 | } 309 | lastEscapeKeypress = ev.timeStamp 310 | } 311 | } else if (!DEBUG && (ev.keyCode == 80 /*P*/ && (ev.metaKey || ev.ctrlKey) && ev.altKey)) { 312 | // meta-alt-P: close plugin 313 | closePlugin() 314 | return true 315 | } 316 | return false 317 | }) 318 | } 319 | 320 | 321 | function main() { 322 | 323 | // toolbar buttons 324 | genButton.onclick = () => { genGraph() } 325 | genButton.title = isMac ? "⌘↩" : "Ctrl+Return" 326 | playgroundButton.onclick = () => { 327 | window.open("https://rsms.me/graphviz/?source=" + encodeURIComponent(editor.text)) 328 | } 329 | demoButton.onclick = () => { 330 | window.open("https://www.figma.com/file/j0LbONPTHzDEhJWZBNNP3D/Graphviz-examples/duplicate") 331 | } 332 | 333 | // [debug] Test the spinner UI 334 | // setTimeout(() => { 335 | // spinner.classList.add("active") 336 | // setTimeout(() => { 337 | // spinner.classList.remove("active") 338 | // },10000) 339 | // }, 100) 340 | 341 | // event handlers 342 | setupEventHandlers() 343 | 344 | // message handlers 345 | window.onmessage = ev => { 346 | let msg = ev.data 347 | if (msg && typeof msg == "object" && msg.pluginMessage) { 348 | msg = msg.pluginMessage 349 | switch (msg.type) { 350 | 351 | case "update-ui": 352 | onUpdateUI(msg as UpdateUIMsg) 353 | break 354 | 355 | case "response": 356 | resolveResponse(msg as ResponseMsg) 357 | break 358 | 359 | default: 360 | print(`ui received unexpected message`, msg) 361 | break 362 | } 363 | } 364 | } 365 | 366 | editor.init() 367 | // document.body.appendChild(parseHtml(svgCode)) 368 | } 369 | 370 | 371 | 372 | main() 373 | -------------------------------------------------------------------------------- /graphviz/src/util.ts: -------------------------------------------------------------------------------- 1 | 2 | export const isMac = navigator.platform.indexOf("Mac") != -1 3 | 4 | export function addKeyEventHandler( 5 | el :Element|Window, 6 | handler :(ev :KeyboardEvent, key :string)=>bool, 7 | ) { 8 | el.addEventListener("keydown", (ev :KeyboardEvent) => { 9 | if (handler(ev, ev.key)) { 10 | ev.preventDefault() 11 | ev.stopPropagation() 12 | } 13 | }, { capture: true, passive: false }) 14 | } 15 | -------------------------------------------------------------------------------- /graphviz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2017", 5 | "lib": [ 6 | "es2017", 7 | "dom" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /minimap/README.md: -------------------------------------------------------------------------------- 1 | # Minimap 2 | 3 | 4 | 5 | Inspired by computer games, this plugin shows a "minimap" to help navigate large files. 6 | 7 | ![screenshot](artwork.png) 8 | 9 | ## Development 10 | 11 | - Development mode: `npm run dev` 12 | - Build in release mode: `npm run build` 13 | -------------------------------------------------------------------------------- /minimap/artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/figma-plugins/2c5d0c24180d14bce7776656584da99b8996a696/minimap/artwork.png -------------------------------------------------------------------------------- /minimap/figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 4 2 | 3 | // Global variable with Figma's plugin API. 4 | declare const figma: PluginAPI 5 | declare const __html__: string 6 | 7 | interface PluginAPI { 8 | readonly apiVersion: "1.0.0" 9 | readonly command: string 10 | readonly viewport: ViewportAPI 11 | closePlugin(message?: string): void 12 | 13 | notify(message: string, options?: NotificationOptions): NotificationHandler 14 | 15 | showUI(html: string, options?: ShowUIOptions): void 16 | readonly ui: UIAPI 17 | 18 | readonly clientStorage: ClientStorageAPI 19 | 20 | getNodeById(id: string): BaseNode | null 21 | getStyleById(id: string): BaseStyle | null 22 | 23 | readonly root: DocumentNode 24 | currentPage: PageNode 25 | 26 | on(type: "selectionchange" | "currentpagechange", callback: () => void) // PROPOSED API ONLY 27 | once(type: "selectionchange" | "currentpagechange", callback: () => void) // PROPOSED API ONLY 28 | off(type: "selectionchange" | "currentpagechange", callback: () => void) // PROPOSED API ONLY 29 | 30 | readonly mixed: symbol 31 | 32 | createRectangle(): RectangleNode 33 | createLine(): LineNode 34 | createEllipse(): EllipseNode 35 | createPolygon(): PolygonNode 36 | createStar(): StarNode 37 | createVector(): VectorNode 38 | createText(): TextNode 39 | createFrame(): FrameNode 40 | createComponent(): ComponentNode 41 | createPage(): PageNode 42 | createSlice(): SliceNode 43 | /** 44 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 45 | */ 46 | createBooleanOperation(): BooleanOperationNode 47 | 48 | createPaintStyle(): PaintStyle 49 | createTextStyle(): TextStyle 50 | createEffectStyle(): EffectStyle 51 | createGridStyle(): GridStyle 52 | 53 | // The styles are returned in the same order as displayed in the UI. Only 54 | // local styles are returned. Never styles from team library. 55 | getLocalPaintStyles(): PaintStyle[] 56 | getLocalTextStyles(): TextStyle[] 57 | getLocalEffectStyles(): EffectStyle[] 58 | getLocalGridStyles(): GridStyle[] 59 | 60 | importComponentByKeyAsync(key: string): Promise 61 | importStyleByKeyAsync(key: string): Promise 62 | 63 | listAvailableFontsAsync(): Promise 64 | loadFontAsync(fontName: FontName): Promise 65 | readonly hasMissingFont: boolean 66 | 67 | createNodeFromSvg(svg: string): FrameNode 68 | 69 | createImage(data: Uint8Array): Image 70 | getImageByHash(hash: string): Image 71 | 72 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 73 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 74 | 75 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 76 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | } 80 | 81 | interface ClientStorageAPI { 82 | getAsync(key: string): Promise 83 | setAsync(key: string, value: any): Promise 84 | } 85 | 86 | interface NotificationOptions { 87 | timeout?: number, 88 | } 89 | 90 | interface NotificationHandler { 91 | cancel: () => void, 92 | } 93 | 94 | interface ShowUIOptions { 95 | visible?: boolean, 96 | width?: number, 97 | height?: number, 98 | } 99 | 100 | interface UIPostMessageOptions { 101 | origin?: string, 102 | } 103 | 104 | interface OnMessageProperties { 105 | origin: string, 106 | } 107 | 108 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void 109 | 110 | interface UIAPI { 111 | show(): void 112 | hide(): void 113 | resize(width: number, height: number): void 114 | close(): void 115 | 116 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 117 | onmessage: ((pluginMessage: any, props: OnMessageProperties) => void) | undefined 118 | on(type: "message", callback: MessageEventHandler) // PROPOSED API ONLY 119 | once(type: "message", callback: MessageEventHandler) // PROPOSED API ONLY 120 | off(type: "message", callback: MessageEventHandler) // PROPOSED API ONLY 121 | } 122 | 123 | interface ViewportAPI { 124 | center: { x: number, y: number } 125 | zoom: number 126 | scrollAndZoomIntoView(nodes: ReadonlyArray) 127 | } 128 | 129 | //////////////////////////////////////////////////////////////////////////////// 130 | // Datatypes 131 | 132 | type Transform = [ 133 | [number, number, number], 134 | [number, number, number] 135 | ] 136 | 137 | interface Vector { 138 | readonly x: number 139 | readonly y: number 140 | } 141 | 142 | interface RGB { 143 | readonly r: number 144 | readonly g: number 145 | readonly b: number 146 | } 147 | 148 | interface RGBA { 149 | readonly r: number 150 | readonly g: number 151 | readonly b: number 152 | readonly a: number 153 | } 154 | 155 | interface FontName { 156 | readonly family: string 157 | readonly style: string 158 | } 159 | 160 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 161 | 162 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 163 | 164 | interface ArcData { 165 | readonly startingAngle: number 166 | readonly endingAngle: number 167 | readonly innerRadius: number 168 | } 169 | 170 | interface ShadowEffect { 171 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 172 | readonly color: RGBA 173 | readonly offset: Vector 174 | readonly radius: number 175 | readonly visible: boolean 176 | readonly blendMode: BlendMode 177 | } 178 | 179 | interface BlurEffect { 180 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 181 | readonly radius: number 182 | readonly visible: boolean 183 | } 184 | 185 | type Effect = ShadowEffect | BlurEffect 186 | 187 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 188 | 189 | interface Constraints { 190 | readonly horizontal: ConstraintType 191 | readonly vertical: ConstraintType 192 | } 193 | 194 | interface ColorStop { 195 | readonly position: number 196 | readonly color: RGBA 197 | } 198 | 199 | interface ImageFilters { 200 | readonly exposure?: number 201 | readonly contrast?: number 202 | readonly saturation?: number 203 | readonly temperature?: number 204 | readonly tint?: number 205 | readonly highlights?: number 206 | readonly shadows?: number 207 | } 208 | 209 | interface SolidPaint { 210 | readonly type: "SOLID" 211 | readonly color: RGB 212 | 213 | readonly visible?: boolean 214 | readonly opacity?: number 215 | readonly blendMode?: BlendMode 216 | } 217 | 218 | interface GradientPaint { 219 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 220 | readonly gradientTransform: Transform 221 | readonly gradientStops: ReadonlyArray 222 | 223 | readonly visible?: boolean 224 | readonly opacity?: number 225 | readonly blendMode?: BlendMode 226 | } 227 | 228 | interface ImagePaint { 229 | readonly type: "IMAGE" 230 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 231 | readonly imageHash: string | null 232 | readonly imageTransform?: Transform // setting for "CROP" 233 | readonly scalingFactor?: number // setting for "TILE" 234 | readonly filters?: ImageFilters 235 | 236 | readonly visible?: boolean 237 | readonly opacity?: number 238 | readonly blendMode?: BlendMode 239 | } 240 | 241 | type Paint = SolidPaint | GradientPaint | ImagePaint 242 | 243 | interface Guide { 244 | readonly axis: "X" | "Y" 245 | readonly offset: number 246 | } 247 | 248 | interface RowsColsLayoutGrid { 249 | readonly pattern: "ROWS" | "COLUMNS" 250 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 251 | readonly gutterSize: number 252 | 253 | readonly count: number // Infinity when "Auto" is set in the UI 254 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 255 | readonly offset?: number // Not set for alignment: "CENTER" 256 | 257 | readonly visible?: boolean 258 | readonly color?: RGBA 259 | } 260 | 261 | interface GridLayoutGrid { 262 | readonly pattern: "GRID" 263 | readonly sectionSize: number 264 | 265 | readonly visible?: boolean 266 | readonly color?: RGBA 267 | } 268 | 269 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 270 | 271 | interface ExportSettingsConstraints { 272 | type: "SCALE" | "WIDTH" | "HEIGHT" 273 | value: number 274 | } 275 | 276 | interface ExportSettingsImage { 277 | format: "JPG" | "PNG" 278 | contentsOnly?: boolean // defaults to true 279 | suffix?: string 280 | constraint?: ExportSettingsConstraints 281 | } 282 | 283 | interface ExportSettingsSVG { 284 | format: "SVG" 285 | contentsOnly?: boolean // defaults to true 286 | suffix?: string 287 | svgOutlineText?: boolean // defaults to true 288 | svgIdAttribute?: boolean // defaults to false 289 | svgSimplifyStroke?: boolean // defaults to true 290 | } 291 | 292 | interface ExportSettingsPDF { 293 | format: "PDF" 294 | contentsOnly?: boolean // defaults to true 295 | suffix?: string 296 | } 297 | 298 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 299 | 300 | type WindingRule = "NONZERO" | "EVENODD" 301 | 302 | interface VectorVertex { 303 | readonly x: number 304 | readonly y: number 305 | readonly strokeCap?: StrokeCap 306 | readonly strokeJoin?: StrokeJoin 307 | readonly cornerRadius?: number 308 | readonly handleMirroring?: HandleMirroring 309 | } 310 | 311 | interface VectorSegment { 312 | readonly start: number 313 | readonly end: number 314 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 315 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 316 | } 317 | 318 | interface VectorRegion { 319 | readonly windingRule: WindingRule 320 | readonly loops: ReadonlyArray> 321 | } 322 | 323 | interface VectorNetwork { 324 | readonly vertices: ReadonlyArray 325 | readonly segments: ReadonlyArray 326 | readonly regions?: ReadonlyArray // Defaults to [] 327 | } 328 | 329 | interface VectorPath { 330 | readonly windingRule: WindingRule | "NONE" 331 | readonly data: string 332 | } 333 | 334 | type VectorPaths = ReadonlyArray 335 | 336 | interface LetterSpacing { 337 | readonly value: number 338 | readonly unit: "PIXELS" | "PERCENT" 339 | } 340 | 341 | type LineHeight = { 342 | readonly value: number 343 | readonly unit: "PIXELS" | "PERCENT" 344 | } | { 345 | readonly unit: "AUTO" 346 | } 347 | 348 | type BlendMode = 349 | "PASS_THROUGH" | 350 | "NORMAL" | 351 | "DARKEN" | 352 | "MULTIPLY" | 353 | "LINEAR_BURN" | 354 | "COLOR_BURN" | 355 | "LIGHTEN" | 356 | "SCREEN" | 357 | "LINEAR_DODGE" | 358 | "COLOR_DODGE" | 359 | "OVERLAY" | 360 | "SOFT_LIGHT" | 361 | "HARD_LIGHT" | 362 | "DIFFERENCE" | 363 | "EXCLUSION" | 364 | "HUE" | 365 | "SATURATION" | 366 | "COLOR" | 367 | "LUMINOSITY" 368 | 369 | interface Font { 370 | fontName: FontName 371 | } 372 | 373 | //////////////////////////////////////////////////////////////////////////////// 374 | // Mixins 375 | 376 | interface BaseNodeMixin { 377 | readonly id: string 378 | readonly parent: (BaseNode & ChildrenMixin) | null 379 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 380 | readonly removed: boolean 381 | toString(): string 382 | remove(): void 383 | 384 | getPluginData(key: string): string 385 | setPluginData(key: string, value: string): void 386 | 387 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 388 | // be a name related to your plugin. Other plugins will be able to read this data. 389 | getSharedPluginData(namespace: string, key: string): string 390 | setSharedPluginData(namespace: string, key: string, value: string): void 391 | } 392 | 393 | interface SceneNodeMixin { 394 | visible: boolean 395 | locked: boolean 396 | } 397 | 398 | interface ChildrenMixin { 399 | readonly children: ReadonlyArray 400 | 401 | appendChild(child: SceneNode): void 402 | insertChild(index: number, child: SceneNode): void 403 | 404 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 405 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 406 | } 407 | 408 | interface ConstraintMixin { 409 | constraints: Constraints 410 | } 411 | 412 | interface LayoutMixin { 413 | readonly absoluteTransform: Transform 414 | relativeTransform: Transform 415 | x: number 416 | y: number 417 | rotation: number // In degrees 418 | 419 | readonly width: number 420 | readonly height: number 421 | 422 | resize(width: number, height: number): void 423 | resizeWithoutConstraints(width: number, height: number): void 424 | } 425 | 426 | interface BlendMixin { 427 | opacity: number 428 | blendMode: BlendMode 429 | isMask: boolean 430 | effects: ReadonlyArray 431 | effectStyleId: string 432 | } 433 | 434 | interface FrameMixin { 435 | backgrounds: ReadonlyArray 436 | layoutGrids: ReadonlyArray 437 | clipsContent: boolean 438 | guides: ReadonlyArray 439 | gridStyleId: string 440 | backgroundStyleId: string 441 | } 442 | 443 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 444 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 445 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 446 | 447 | interface GeometryMixin { 448 | fills: ReadonlyArray | symbol 449 | strokes: ReadonlyArray 450 | strokeWeight: number 451 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 452 | strokeCap: StrokeCap | symbol 453 | strokeJoin: StrokeJoin | symbol 454 | dashPattern: ReadonlyArray 455 | fillStyleId: string | symbol 456 | strokeStyleId: string 457 | } 458 | 459 | interface CornerMixin { 460 | cornerRadius: number | symbol 461 | cornerSmoothing: number 462 | } 463 | 464 | interface ExportMixin { 465 | exportSettings: ReadonlyArray 466 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 467 | } 468 | 469 | interface DefaultShapeMixin extends 470 | BaseNodeMixin, SceneNodeMixin, 471 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 472 | } 473 | 474 | interface DefaultContainerMixin extends 475 | BaseNodeMixin, SceneNodeMixin, 476 | ChildrenMixin, FrameMixin, 477 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 478 | } 479 | 480 | //////////////////////////////////////////////////////////////////////////////// 481 | // Nodes 482 | 483 | interface DocumentNode extends BaseNodeMixin { 484 | readonly type: "DOCUMENT" 485 | 486 | readonly children: ReadonlyArray 487 | 488 | appendChild(child: PageNode): void 489 | insertChild(index: number, child: PageNode): void 490 | 491 | findAll(callback?: (node: (PageNode | SceneNode)) => boolean): Array 492 | findOne(callback: (node: (PageNode | SceneNode)) => boolean): PageNode | SceneNode | null 493 | } 494 | 495 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 496 | readonly type: "PAGE" 497 | clone(): PageNode 498 | 499 | guides: ReadonlyArray 500 | selection: ReadonlyArray 501 | 502 | backgrounds: ReadonlyArray 503 | } 504 | 505 | interface FrameNode extends DefaultContainerMixin { 506 | readonly type: "FRAME" | "GROUP" 507 | clone(): FrameNode 508 | } 509 | 510 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 511 | readonly type: "SLICE" 512 | clone(): SliceNode 513 | } 514 | 515 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 516 | readonly type: "RECTANGLE" 517 | clone(): RectangleNode 518 | topLeftRadius: number 519 | topRightRadius: number 520 | bottomLeftRadius: number 521 | bottomRightRadius: number 522 | } 523 | 524 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 525 | readonly type: "LINE" 526 | clone(): LineNode 527 | } 528 | 529 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 530 | readonly type: "ELLIPSE" 531 | clone(): EllipseNode 532 | arcData: ArcData 533 | } 534 | 535 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 536 | readonly type: "POLYGON" 537 | clone(): PolygonNode 538 | pointCount: number 539 | } 540 | 541 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 542 | readonly type: "STAR" 543 | clone(): StarNode 544 | pointCount: number 545 | innerRadius: number 546 | } 547 | 548 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 549 | readonly type: "VECTOR" 550 | clone(): VectorNode 551 | vectorNetwork: VectorNetwork 552 | vectorPaths: VectorPaths 553 | handleMirroring: HandleMirroring | symbol 554 | } 555 | 556 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 557 | readonly type: "TEXT" 558 | clone(): TextNode 559 | characters: string 560 | readonly hasMissingFont: boolean 561 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 562 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 563 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 564 | paragraphIndent: number 565 | paragraphSpacing: number 566 | autoRename: boolean 567 | 568 | textStyleId: string | symbol 569 | fontSize: number | symbol 570 | fontName: FontName | symbol 571 | textCase: TextCase | symbol 572 | textDecoration: TextDecoration | symbol 573 | letterSpacing: LetterSpacing | symbol 574 | lineHeight: LineHeight | symbol 575 | 576 | getRangeFontSize(start: number, end: number): number | symbol 577 | setRangeFontSize(start: number, end: number, value: number): void 578 | getRangeFontName(start: number, end: number): FontName | symbol 579 | setRangeFontName(start: number, end: number, value: FontName): void 580 | getRangeTextCase(start: number, end: number): TextCase | symbol 581 | setRangeTextCase(start: number, end: number, value: TextCase): void 582 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 583 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 584 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 585 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 586 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 587 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 588 | getRangeFills(start: number, end: number): Paint[] | symbol 589 | setRangeFills(start: number, end: number, value: Paint[]): void 590 | getRangeTextStyleId(start: number, end: number): string | symbol 591 | setRangeTextStyleId(start: number, end: number, value: string): void 592 | getRangeFillStyleId(start: number, end: number): string | symbol 593 | setRangeFillStyleId(start: number, end: number, value: string): void 594 | } 595 | 596 | interface ComponentNode extends DefaultContainerMixin { 597 | readonly type: "COMPONENT" 598 | clone(): ComponentNode 599 | 600 | createInstance(): InstanceNode 601 | description: string 602 | readonly remote: boolean 603 | readonly key: string // The key to use with "importComponentByKeyAsync" 604 | } 605 | 606 | interface InstanceNode extends DefaultContainerMixin { 607 | readonly type: "INSTANCE" 608 | clone(): InstanceNode 609 | masterComponent: ComponentNode 610 | } 611 | 612 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 613 | readonly type: "BOOLEAN_OPERATION" 614 | clone(): BooleanOperationNode 615 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 616 | } 617 | 618 | type BaseNode = 619 | DocumentNode | 620 | PageNode | 621 | SceneNode 622 | 623 | type SceneNode = 624 | SliceNode | 625 | FrameNode | 626 | ComponentNode | 627 | InstanceNode | 628 | BooleanOperationNode | 629 | VectorNode | 630 | StarNode | 631 | LineNode | 632 | EllipseNode | 633 | PolygonNode | 634 | RectangleNode | 635 | TextNode 636 | 637 | type NodeType = 638 | "DOCUMENT" | 639 | "PAGE" | 640 | "SLICE" | 641 | "FRAME" | 642 | "GROUP" | 643 | "COMPONENT" | 644 | "INSTANCE" | 645 | "BOOLEAN_OPERATION" | 646 | "VECTOR" | 647 | "STAR" | 648 | "LINE" | 649 | "ELLIPSE" | 650 | "POLYGON" | 651 | "RECTANGLE" | 652 | "TEXT" 653 | 654 | //////////////////////////////////////////////////////////////////////////////// 655 | // Styles 656 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 657 | 658 | interface BaseStyle { 659 | readonly id: string 660 | readonly type: StyleType 661 | name: string 662 | description: string 663 | remote: boolean 664 | readonly key: string // The key to use with "importStyleByKeyAsync" 665 | remove(): void 666 | } 667 | 668 | interface PaintStyle extends BaseStyle { 669 | type: "PAINT" 670 | paints: ReadonlyArray 671 | } 672 | 673 | interface TextStyle extends BaseStyle { 674 | type: "TEXT" 675 | fontSize: number 676 | textDecoration: TextDecoration 677 | fontName: FontName 678 | letterSpacing: LetterSpacing 679 | lineHeight: LineHeight 680 | paragraphIndent: number 681 | paragraphSpacing: number 682 | textCase: TextCase 683 | } 684 | 685 | interface EffectStyle extends BaseStyle { 686 | type: "EFFECT" 687 | effects: ReadonlyArray 688 | } 689 | 690 | interface GridStyle extends BaseStyle { 691 | type: "GRID" 692 | layoutGrids: ReadonlyArray 693 | } 694 | 695 | //////////////////////////////////////////////////////////////////////////////// 696 | // Other 697 | 698 | interface Image { 699 | readonly hash: string 700 | getBytesAsync(): Promise 701 | } 702 | -------------------------------------------------------------------------------- /minimap/figplug.d.ts: -------------------------------------------------------------------------------- 1 | // Helpers provided automatically, as needed, by figplug. 2 | 3 | // symbolic type aliases 4 | type int = number 5 | type float = number 6 | type byte = number 7 | type bool = boolean 8 | 9 | // compile-time constants 10 | declare const DEBUG :boolean 11 | declare const VERSION :string 12 | 13 | // global namespace. Same as `window` in a regular web context. 14 | declare const global :{[k:string]:any} 15 | 16 | // panic prints a message, stack trace and exits the process 17 | // 18 | declare function panic(msg :any, ...v :any[]) :void 19 | 20 | // repr returns a detailed string representation of the input 21 | // 22 | declare function repr(obj :any) :string 23 | 24 | // print works just like console.log 25 | declare function print(msg :any, ...v :any[]) :void 26 | 27 | // dlog works just like console.log but is stripped out from non-debug builds 28 | declare function dlog(msg :any, ...v :any[]) :void 29 | 30 | // assert checks the condition for truth, and if false, prints an optional 31 | // message, stack trace and exits the process. 32 | // assert is removed in release builds 33 | declare var assert :AssertFun 34 | declare var AssertionError :ErrorConstructor 35 | declare interface AssertFun { 36 | (cond :any, msg? :string, cons? :Function) :void 37 | 38 | // throws can be set to true to cause assertions to be thrown as exceptions, 39 | // or set to false to cause the process to exit. 40 | // Only has an effect in Nodejs-like environments. 41 | // false by default. 42 | throws :bool 43 | } 44 | -------------------------------------------------------------------------------- /minimap/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "1.0.0", 3 | "id": "772952119002135124", 4 | "name": "Minimap", 5 | "main": "src/plugin.ts", 6 | "ui": "src/ui.ts" 7 | } 8 | -------------------------------------------------------------------------------- /minimap/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimap", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/estree": { 8 | "version": "0.0.39", 9 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 10 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 11 | "dev": true 12 | }, 13 | "@types/node": { 14 | "version": "12.12.5", 15 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.5.tgz", 16 | "integrity": "sha512-KEjODidV4XYUlJBF3XdjSH5FWoMCtO0utnhtdLf1AgeuZLOrRbvmU/gaRCVg7ZaQDjVf3l84egiY0mRNe5xE4A==", 17 | "dev": true 18 | }, 19 | "@types/q": { 20 | "version": "1.5.2", 21 | "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", 22 | "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", 23 | "dev": true 24 | }, 25 | "@types/resolve": { 26 | "version": "0.0.8", 27 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 28 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 29 | "dev": true, 30 | "requires": { 31 | "@types/node": "*" 32 | } 33 | }, 34 | "acorn": { 35 | "version": "7.1.0", 36 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", 37 | "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", 38 | "dev": true 39 | }, 40 | "ansi-styles": { 41 | "version": "3.2.1", 42 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 43 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 44 | "dev": true, 45 | "requires": { 46 | "color-convert": "^1.9.0" 47 | } 48 | }, 49 | "argparse": { 50 | "version": "1.0.10", 51 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 52 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 53 | "dev": true, 54 | "requires": { 55 | "sprintf-js": "~1.0.2" 56 | } 57 | }, 58 | "arr-diff": { 59 | "version": "4.0.0", 60 | "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", 61 | "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", 62 | "dev": true 63 | }, 64 | "arr-flatten": { 65 | "version": "1.1.0", 66 | "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", 67 | "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", 68 | "dev": true 69 | }, 70 | "arr-union": { 71 | "version": "3.1.0", 72 | "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", 73 | "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", 74 | "dev": true 75 | }, 76 | "array-unique": { 77 | "version": "0.3.2", 78 | "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", 79 | "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", 80 | "dev": true 81 | }, 82 | "assign-symbols": { 83 | "version": "1.0.0", 84 | "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", 85 | "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", 86 | "dev": true 87 | }, 88 | "atob": { 89 | "version": "2.1.2", 90 | "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", 91 | "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", 92 | "dev": true 93 | }, 94 | "base": { 95 | "version": "0.11.2", 96 | "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", 97 | "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", 98 | "dev": true, 99 | "requires": { 100 | "cache-base": "^1.0.1", 101 | "class-utils": "^0.3.5", 102 | "component-emitter": "^1.2.1", 103 | "define-property": "^1.0.0", 104 | "isobject": "^3.0.1", 105 | "mixin-deep": "^1.2.0", 106 | "pascalcase": "^0.1.1" 107 | }, 108 | "dependencies": { 109 | "define-property": { 110 | "version": "1.0.0", 111 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", 112 | "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", 113 | "dev": true, 114 | "requires": { 115 | "is-descriptor": "^1.0.0" 116 | } 117 | }, 118 | "is-accessor-descriptor": { 119 | "version": "1.0.0", 120 | "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", 121 | "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", 122 | "dev": true, 123 | "requires": { 124 | "kind-of": "^6.0.0" 125 | } 126 | }, 127 | "is-data-descriptor": { 128 | "version": "1.0.0", 129 | "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", 130 | "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", 131 | "dev": true, 132 | "requires": { 133 | "kind-of": "^6.0.0" 134 | } 135 | }, 136 | "is-descriptor": { 137 | "version": "1.0.2", 138 | "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", 139 | "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", 140 | "dev": true, 141 | "requires": { 142 | "is-accessor-descriptor": "^1.0.0", 143 | "is-data-descriptor": "^1.0.0", 144 | "kind-of": "^6.0.2" 145 | } 146 | } 147 | } 148 | }, 149 | "boolbase": { 150 | "version": "1.0.0", 151 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 152 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", 153 | "dev": true 154 | }, 155 | "braces": { 156 | "version": "2.3.2", 157 | "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", 158 | "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", 159 | "dev": true, 160 | "requires": { 161 | "arr-flatten": "^1.1.0", 162 | "array-unique": "^0.3.2", 163 | "extend-shallow": "^2.0.1", 164 | "fill-range": "^4.0.0", 165 | "isobject": "^3.0.1", 166 | "repeat-element": "^1.1.2", 167 | "snapdragon": "^0.8.1", 168 | "snapdragon-node": "^2.0.1", 169 | "split-string": "^3.0.2", 170 | "to-regex": "^3.0.1" 171 | }, 172 | "dependencies": { 173 | "extend-shallow": { 174 | "version": "2.0.1", 175 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 176 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 177 | "dev": true, 178 | "requires": { 179 | "is-extendable": "^0.1.0" 180 | } 181 | } 182 | } 183 | }, 184 | "builtin-modules": { 185 | "version": "3.1.0", 186 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 187 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", 188 | "dev": true 189 | }, 190 | "cache-base": { 191 | "version": "1.0.1", 192 | "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", 193 | "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", 194 | "dev": true, 195 | "requires": { 196 | "collection-visit": "^1.0.0", 197 | "component-emitter": "^1.2.1", 198 | "get-value": "^2.0.6", 199 | "has-value": "^1.0.0", 200 | "isobject": "^3.0.1", 201 | "set-value": "^2.0.0", 202 | "to-object-path": "^0.3.0", 203 | "union-value": "^1.0.0", 204 | "unset-value": "^1.0.0" 205 | } 206 | }, 207 | "chalk": { 208 | "version": "2.4.2", 209 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 210 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 211 | "dev": true, 212 | "requires": { 213 | "ansi-styles": "^3.2.1", 214 | "escape-string-regexp": "^1.0.5", 215 | "supports-color": "^5.3.0" 216 | }, 217 | "dependencies": { 218 | "supports-color": { 219 | "version": "5.5.0", 220 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 221 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 222 | "dev": true, 223 | "requires": { 224 | "has-flag": "^3.0.0" 225 | } 226 | } 227 | } 228 | }, 229 | "class-utils": { 230 | "version": "0.3.6", 231 | "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", 232 | "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", 233 | "dev": true, 234 | "requires": { 235 | "arr-union": "^3.1.0", 236 | "define-property": "^0.2.5", 237 | "isobject": "^3.0.0", 238 | "static-extend": "^0.1.1" 239 | }, 240 | "dependencies": { 241 | "define-property": { 242 | "version": "0.2.5", 243 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", 244 | "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", 245 | "dev": true, 246 | "requires": { 247 | "is-descriptor": "^0.1.0" 248 | } 249 | } 250 | } 251 | }, 252 | "coa": { 253 | "version": "2.0.2", 254 | "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", 255 | "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", 256 | "dev": true, 257 | "requires": { 258 | "@types/q": "^1.5.1", 259 | "chalk": "^2.4.1", 260 | "q": "^1.1.2" 261 | } 262 | }, 263 | "collection-visit": { 264 | "version": "1.0.0", 265 | "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", 266 | "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", 267 | "dev": true, 268 | "requires": { 269 | "map-visit": "^1.0.0", 270 | "object-visit": "^1.0.0" 271 | } 272 | }, 273 | "color-convert": { 274 | "version": "1.9.3", 275 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 276 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 277 | "dev": true, 278 | "requires": { 279 | "color-name": "1.1.3" 280 | } 281 | }, 282 | "color-name": { 283 | "version": "1.1.3", 284 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 285 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 286 | "dev": true 287 | }, 288 | "component-emitter": { 289 | "version": "1.3.0", 290 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 291 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", 292 | "dev": true 293 | }, 294 | "copy-descriptor": { 295 | "version": "0.1.1", 296 | "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", 297 | "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", 298 | "dev": true 299 | }, 300 | "css-select": { 301 | "version": "2.0.2", 302 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", 303 | "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", 304 | "dev": true, 305 | "requires": { 306 | "boolbase": "^1.0.0", 307 | "css-what": "^2.1.2", 308 | "domutils": "^1.7.0", 309 | "nth-check": "^1.0.2" 310 | } 311 | }, 312 | "css-select-base-adapter": { 313 | "version": "0.1.1", 314 | "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", 315 | "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", 316 | "dev": true 317 | }, 318 | "css-tree": { 319 | "version": "1.0.0-alpha.37", 320 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", 321 | "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", 322 | "dev": true, 323 | "requires": { 324 | "mdn-data": "2.0.4", 325 | "source-map": "^0.6.1" 326 | } 327 | }, 328 | "css-what": { 329 | "version": "2.1.3", 330 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", 331 | "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", 332 | "dev": true 333 | }, 334 | "csso": { 335 | "version": "4.0.2", 336 | "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.2.tgz", 337 | "integrity": "sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==", 338 | "dev": true, 339 | "requires": { 340 | "css-tree": "1.0.0-alpha.37" 341 | } 342 | }, 343 | "debug": { 344 | "version": "2.6.9", 345 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 346 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 347 | "dev": true, 348 | "requires": { 349 | "ms": "2.0.0" 350 | } 351 | }, 352 | "decode-uri-component": { 353 | "version": "0.2.0", 354 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", 355 | "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", 356 | "dev": true 357 | }, 358 | "define-properties": { 359 | "version": "1.1.3", 360 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 361 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 362 | "dev": true, 363 | "requires": { 364 | "object-keys": "^1.0.12" 365 | } 366 | }, 367 | "define-property": { 368 | "version": "2.0.2", 369 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", 370 | "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", 371 | "dev": true, 372 | "requires": { 373 | "is-descriptor": "^1.0.2", 374 | "isobject": "^3.0.1" 375 | }, 376 | "dependencies": { 377 | "is-accessor-descriptor": { 378 | "version": "1.0.0", 379 | "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", 380 | "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", 381 | "dev": true, 382 | "requires": { 383 | "kind-of": "^6.0.0" 384 | } 385 | }, 386 | "is-data-descriptor": { 387 | "version": "1.0.0", 388 | "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", 389 | "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", 390 | "dev": true, 391 | "requires": { 392 | "kind-of": "^6.0.0" 393 | } 394 | }, 395 | "is-descriptor": { 396 | "version": "1.0.2", 397 | "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", 398 | "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", 399 | "dev": true, 400 | "requires": { 401 | "is-accessor-descriptor": "^1.0.0", 402 | "is-data-descriptor": "^1.0.0", 403 | "kind-of": "^6.0.2" 404 | } 405 | } 406 | } 407 | }, 408 | "dom-serializer": { 409 | "version": "0.2.1", 410 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", 411 | "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", 412 | "dev": true, 413 | "requires": { 414 | "domelementtype": "^2.0.1", 415 | "entities": "^2.0.0" 416 | }, 417 | "dependencies": { 418 | "domelementtype": { 419 | "version": "2.0.1", 420 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", 421 | "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", 422 | "dev": true 423 | } 424 | } 425 | }, 426 | "domelementtype": { 427 | "version": "1.3.1", 428 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", 429 | "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", 430 | "dev": true 431 | }, 432 | "domutils": { 433 | "version": "1.7.0", 434 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", 435 | "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", 436 | "dev": true, 437 | "requires": { 438 | "dom-serializer": "0", 439 | "domelementtype": "1" 440 | } 441 | }, 442 | "entities": { 443 | "version": "2.0.0", 444 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", 445 | "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", 446 | "dev": true 447 | }, 448 | "es-abstract": { 449 | "version": "1.16.0", 450 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", 451 | "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", 452 | "dev": true, 453 | "requires": { 454 | "es-to-primitive": "^1.2.0", 455 | "function-bind": "^1.1.1", 456 | "has": "^1.0.3", 457 | "has-symbols": "^1.0.0", 458 | "is-callable": "^1.1.4", 459 | "is-regex": "^1.0.4", 460 | "object-inspect": "^1.6.0", 461 | "object-keys": "^1.1.1", 462 | "string.prototype.trimleft": "^2.1.0", 463 | "string.prototype.trimright": "^2.1.0" 464 | } 465 | }, 466 | "es-to-primitive": { 467 | "version": "1.2.0", 468 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 469 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 470 | "dev": true, 471 | "requires": { 472 | "is-callable": "^1.1.4", 473 | "is-date-object": "^1.0.1", 474 | "is-symbol": "^1.0.2" 475 | } 476 | }, 477 | "escape-string-regexp": { 478 | "version": "1.0.5", 479 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 480 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 481 | "dev": true 482 | }, 483 | "esprima": { 484 | "version": "4.0.1", 485 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 486 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 487 | "dev": true 488 | }, 489 | "estree-walker": { 490 | "version": "0.6.1", 491 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 492 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 493 | "dev": true 494 | }, 495 | "expand-brackets": { 496 | "version": "2.1.4", 497 | "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", 498 | "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", 499 | "dev": true, 500 | "requires": { 501 | "debug": "^2.3.3", 502 | "define-property": "^0.2.5", 503 | "extend-shallow": "^2.0.1", 504 | "posix-character-classes": "^0.1.0", 505 | "regex-not": "^1.0.0", 506 | "snapdragon": "^0.8.1", 507 | "to-regex": "^3.0.1" 508 | }, 509 | "dependencies": { 510 | "define-property": { 511 | "version": "0.2.5", 512 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", 513 | "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", 514 | "dev": true, 515 | "requires": { 516 | "is-descriptor": "^0.1.0" 517 | } 518 | }, 519 | "extend-shallow": { 520 | "version": "2.0.1", 521 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 522 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 523 | "dev": true, 524 | "requires": { 525 | "is-extendable": "^0.1.0" 526 | } 527 | } 528 | } 529 | }, 530 | "extend-shallow": { 531 | "version": "3.0.2", 532 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", 533 | "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", 534 | "dev": true, 535 | "requires": { 536 | "assign-symbols": "^1.0.0", 537 | "is-extendable": "^1.0.1" 538 | }, 539 | "dependencies": { 540 | "is-extendable": { 541 | "version": "1.0.1", 542 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", 543 | "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", 544 | "dev": true, 545 | "requires": { 546 | "is-plain-object": "^2.0.4" 547 | } 548 | } 549 | } 550 | }, 551 | "extglob": { 552 | "version": "2.0.4", 553 | "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", 554 | "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", 555 | "dev": true, 556 | "requires": { 557 | "array-unique": "^0.3.2", 558 | "define-property": "^1.0.0", 559 | "expand-brackets": "^2.1.4", 560 | "extend-shallow": "^2.0.1", 561 | "fragment-cache": "^0.2.1", 562 | "regex-not": "^1.0.0", 563 | "snapdragon": "^0.8.1", 564 | "to-regex": "^3.0.1" 565 | }, 566 | "dependencies": { 567 | "define-property": { 568 | "version": "1.0.0", 569 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", 570 | "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", 571 | "dev": true, 572 | "requires": { 573 | "is-descriptor": "^1.0.0" 574 | } 575 | }, 576 | "extend-shallow": { 577 | "version": "2.0.1", 578 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 579 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 580 | "dev": true, 581 | "requires": { 582 | "is-extendable": "^0.1.0" 583 | } 584 | }, 585 | "is-accessor-descriptor": { 586 | "version": "1.0.0", 587 | "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", 588 | "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", 589 | "dev": true, 590 | "requires": { 591 | "kind-of": "^6.0.0" 592 | } 593 | }, 594 | "is-data-descriptor": { 595 | "version": "1.0.0", 596 | "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", 597 | "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", 598 | "dev": true, 599 | "requires": { 600 | "kind-of": "^6.0.0" 601 | } 602 | }, 603 | "is-descriptor": { 604 | "version": "1.0.2", 605 | "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", 606 | "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", 607 | "dev": true, 608 | "requires": { 609 | "is-accessor-descriptor": "^1.0.0", 610 | "is-data-descriptor": "^1.0.0", 611 | "kind-of": "^6.0.2" 612 | } 613 | } 614 | } 615 | }, 616 | "figplug": { 617 | "version": "0.1.12", 618 | "resolved": "https://registry.npmjs.org/figplug/-/figplug-0.1.12.tgz", 619 | "integrity": "sha512-Bic91eropAVKN/YuC1Vp7JQSvBdF7kX+o7heTCHtM9HrKRDWZ5ZunetSbndPidGHXoxPIpPj1xPwYfjSN8w0Pw==", 620 | "dev": true, 621 | "requires": { 622 | "postcss-nesting": "^7.0.0", 623 | "rollup": "^1.14.4", 624 | "rollup-plugin-commonjs": "^10.0.0", 625 | "rollup-plugin-node-resolve": "^5.0.1", 626 | "rollup-plugin-typescript2": "^0.21.1", 627 | "svgo": "^1.2.2", 628 | "typescript": "^3.5.1" 629 | } 630 | }, 631 | "fill-range": { 632 | "version": "4.0.0", 633 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", 634 | "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", 635 | "dev": true, 636 | "requires": { 637 | "extend-shallow": "^2.0.1", 638 | "is-number": "^3.0.0", 639 | "repeat-string": "^1.6.1", 640 | "to-regex-range": "^2.1.0" 641 | }, 642 | "dependencies": { 643 | "extend-shallow": { 644 | "version": "2.0.1", 645 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 646 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 647 | "dev": true, 648 | "requires": { 649 | "is-extendable": "^0.1.0" 650 | } 651 | } 652 | } 653 | }, 654 | "for-in": { 655 | "version": "1.0.2", 656 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 657 | "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", 658 | "dev": true 659 | }, 660 | "fragment-cache": { 661 | "version": "0.2.1", 662 | "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", 663 | "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", 664 | "dev": true, 665 | "requires": { 666 | "map-cache": "^0.2.2" 667 | } 668 | }, 669 | "fs-extra": { 670 | "version": "7.0.1", 671 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", 672 | "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", 673 | "dev": true, 674 | "requires": { 675 | "graceful-fs": "^4.1.2", 676 | "jsonfile": "^4.0.0", 677 | "universalify": "^0.1.0" 678 | } 679 | }, 680 | "function-bind": { 681 | "version": "1.1.1", 682 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 683 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 684 | "dev": true 685 | }, 686 | "get-value": { 687 | "version": "2.0.6", 688 | "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", 689 | "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", 690 | "dev": true 691 | }, 692 | "graceful-fs": { 693 | "version": "4.2.3", 694 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", 695 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", 696 | "dev": true 697 | }, 698 | "has": { 699 | "version": "1.0.3", 700 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 701 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 702 | "dev": true, 703 | "requires": { 704 | "function-bind": "^1.1.1" 705 | } 706 | }, 707 | "has-flag": { 708 | "version": "3.0.0", 709 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 710 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 711 | "dev": true 712 | }, 713 | "has-symbols": { 714 | "version": "1.0.0", 715 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 716 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 717 | "dev": true 718 | }, 719 | "has-value": { 720 | "version": "1.0.0", 721 | "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", 722 | "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", 723 | "dev": true, 724 | "requires": { 725 | "get-value": "^2.0.6", 726 | "has-values": "^1.0.0", 727 | "isobject": "^3.0.0" 728 | } 729 | }, 730 | "has-values": { 731 | "version": "1.0.0", 732 | "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", 733 | "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", 734 | "dev": true, 735 | "requires": { 736 | "is-number": "^3.0.0", 737 | "kind-of": "^4.0.0" 738 | }, 739 | "dependencies": { 740 | "kind-of": { 741 | "version": "4.0.0", 742 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", 743 | "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", 744 | "dev": true, 745 | "requires": { 746 | "is-buffer": "^1.1.5" 747 | } 748 | } 749 | } 750 | }, 751 | "is-accessor-descriptor": { 752 | "version": "0.1.6", 753 | "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", 754 | "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", 755 | "dev": true, 756 | "requires": { 757 | "kind-of": "^3.0.2" 758 | }, 759 | "dependencies": { 760 | "kind-of": { 761 | "version": "3.2.2", 762 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 763 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 764 | "dev": true, 765 | "requires": { 766 | "is-buffer": "^1.1.5" 767 | } 768 | } 769 | } 770 | }, 771 | "is-buffer": { 772 | "version": "1.1.6", 773 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 774 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", 775 | "dev": true 776 | }, 777 | "is-callable": { 778 | "version": "1.1.4", 779 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 780 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 781 | "dev": true 782 | }, 783 | "is-data-descriptor": { 784 | "version": "0.1.4", 785 | "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", 786 | "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", 787 | "dev": true, 788 | "requires": { 789 | "kind-of": "^3.0.2" 790 | }, 791 | "dependencies": { 792 | "kind-of": { 793 | "version": "3.2.2", 794 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 795 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 796 | "dev": true, 797 | "requires": { 798 | "is-buffer": "^1.1.5" 799 | } 800 | } 801 | } 802 | }, 803 | "is-date-object": { 804 | "version": "1.0.1", 805 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 806 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 807 | "dev": true 808 | }, 809 | "is-descriptor": { 810 | "version": "0.1.6", 811 | "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", 812 | "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", 813 | "dev": true, 814 | "requires": { 815 | "is-accessor-descriptor": "^0.1.6", 816 | "is-data-descriptor": "^0.1.4", 817 | "kind-of": "^5.0.0" 818 | }, 819 | "dependencies": { 820 | "kind-of": { 821 | "version": "5.1.0", 822 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", 823 | "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", 824 | "dev": true 825 | } 826 | } 827 | }, 828 | "is-extendable": { 829 | "version": "0.1.1", 830 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 831 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", 832 | "dev": true 833 | }, 834 | "is-module": { 835 | "version": "1.0.0", 836 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 837 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 838 | "dev": true 839 | }, 840 | "is-number": { 841 | "version": "3.0.0", 842 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", 843 | "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", 844 | "dev": true, 845 | "requires": { 846 | "kind-of": "^3.0.2" 847 | }, 848 | "dependencies": { 849 | "kind-of": { 850 | "version": "3.2.2", 851 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 852 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 853 | "dev": true, 854 | "requires": { 855 | "is-buffer": "^1.1.5" 856 | } 857 | } 858 | } 859 | }, 860 | "is-plain-object": { 861 | "version": "2.0.4", 862 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 863 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 864 | "dev": true, 865 | "requires": { 866 | "isobject": "^3.0.1" 867 | } 868 | }, 869 | "is-reference": { 870 | "version": "1.1.4", 871 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz", 872 | "integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==", 873 | "dev": true, 874 | "requires": { 875 | "@types/estree": "0.0.39" 876 | } 877 | }, 878 | "is-regex": { 879 | "version": "1.0.4", 880 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 881 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 882 | "dev": true, 883 | "requires": { 884 | "has": "^1.0.1" 885 | } 886 | }, 887 | "is-symbol": { 888 | "version": "1.0.2", 889 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 890 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 891 | "dev": true, 892 | "requires": { 893 | "has-symbols": "^1.0.0" 894 | } 895 | }, 896 | "is-windows": { 897 | "version": "1.0.2", 898 | "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", 899 | "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", 900 | "dev": true 901 | }, 902 | "isarray": { 903 | "version": "1.0.0", 904 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 905 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 906 | "dev": true 907 | }, 908 | "isobject": { 909 | "version": "3.0.1", 910 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 911 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 912 | "dev": true 913 | }, 914 | "js-yaml": { 915 | "version": "3.13.1", 916 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 917 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 918 | "dev": true, 919 | "requires": { 920 | "argparse": "^1.0.7", 921 | "esprima": "^4.0.0" 922 | } 923 | }, 924 | "jsonfile": { 925 | "version": "4.0.0", 926 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 927 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 928 | "dev": true, 929 | "requires": { 930 | "graceful-fs": "^4.1.6" 931 | } 932 | }, 933 | "kind-of": { 934 | "version": "6.0.2", 935 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", 936 | "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", 937 | "dev": true 938 | }, 939 | "magic-string": { 940 | "version": "0.25.4", 941 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.4.tgz", 942 | "integrity": "sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw==", 943 | "dev": true, 944 | "requires": { 945 | "sourcemap-codec": "^1.4.4" 946 | } 947 | }, 948 | "map-cache": { 949 | "version": "0.2.2", 950 | "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", 951 | "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", 952 | "dev": true 953 | }, 954 | "map-visit": { 955 | "version": "1.0.0", 956 | "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", 957 | "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", 958 | "dev": true, 959 | "requires": { 960 | "object-visit": "^1.0.0" 961 | } 962 | }, 963 | "mdn-data": { 964 | "version": "2.0.4", 965 | "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", 966 | "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", 967 | "dev": true 968 | }, 969 | "micromatch": { 970 | "version": "3.1.10", 971 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", 972 | "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", 973 | "dev": true, 974 | "requires": { 975 | "arr-diff": "^4.0.0", 976 | "array-unique": "^0.3.2", 977 | "braces": "^2.3.1", 978 | "define-property": "^2.0.2", 979 | "extend-shallow": "^3.0.2", 980 | "extglob": "^2.0.4", 981 | "fragment-cache": "^0.2.1", 982 | "kind-of": "^6.0.2", 983 | "nanomatch": "^1.2.9", 984 | "object.pick": "^1.3.0", 985 | "regex-not": "^1.0.0", 986 | "snapdragon": "^0.8.1", 987 | "to-regex": "^3.0.2" 988 | } 989 | }, 990 | "minimist": { 991 | "version": "0.0.8", 992 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 993 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 994 | "dev": true 995 | }, 996 | "mixin-deep": { 997 | "version": "1.3.2", 998 | "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", 999 | "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", 1000 | "dev": true, 1001 | "requires": { 1002 | "for-in": "^1.0.2", 1003 | "is-extendable": "^1.0.1" 1004 | }, 1005 | "dependencies": { 1006 | "is-extendable": { 1007 | "version": "1.0.1", 1008 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", 1009 | "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", 1010 | "dev": true, 1011 | "requires": { 1012 | "is-plain-object": "^2.0.4" 1013 | } 1014 | } 1015 | } 1016 | }, 1017 | "mkdirp": { 1018 | "version": "0.5.1", 1019 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1020 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1021 | "dev": true, 1022 | "requires": { 1023 | "minimist": "0.0.8" 1024 | } 1025 | }, 1026 | "ms": { 1027 | "version": "2.0.0", 1028 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1029 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 1030 | "dev": true 1031 | }, 1032 | "nanomatch": { 1033 | "version": "1.2.13", 1034 | "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", 1035 | "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", 1036 | "dev": true, 1037 | "requires": { 1038 | "arr-diff": "^4.0.0", 1039 | "array-unique": "^0.3.2", 1040 | "define-property": "^2.0.2", 1041 | "extend-shallow": "^3.0.2", 1042 | "fragment-cache": "^0.2.1", 1043 | "is-windows": "^1.0.2", 1044 | "kind-of": "^6.0.2", 1045 | "object.pick": "^1.3.0", 1046 | "regex-not": "^1.0.0", 1047 | "snapdragon": "^0.8.1", 1048 | "to-regex": "^3.0.1" 1049 | } 1050 | }, 1051 | "nth-check": { 1052 | "version": "1.0.2", 1053 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", 1054 | "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", 1055 | "dev": true, 1056 | "requires": { 1057 | "boolbase": "~1.0.0" 1058 | } 1059 | }, 1060 | "object-copy": { 1061 | "version": "0.1.0", 1062 | "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", 1063 | "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", 1064 | "dev": true, 1065 | "requires": { 1066 | "copy-descriptor": "^0.1.0", 1067 | "define-property": "^0.2.5", 1068 | "kind-of": "^3.0.3" 1069 | }, 1070 | "dependencies": { 1071 | "define-property": { 1072 | "version": "0.2.5", 1073 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", 1074 | "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", 1075 | "dev": true, 1076 | "requires": { 1077 | "is-descriptor": "^0.1.0" 1078 | } 1079 | }, 1080 | "kind-of": { 1081 | "version": "3.2.2", 1082 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 1083 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 1084 | "dev": true, 1085 | "requires": { 1086 | "is-buffer": "^1.1.5" 1087 | } 1088 | } 1089 | } 1090 | }, 1091 | "object-inspect": { 1092 | "version": "1.6.0", 1093 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", 1094 | "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", 1095 | "dev": true 1096 | }, 1097 | "object-keys": { 1098 | "version": "1.1.1", 1099 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 1100 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 1101 | "dev": true 1102 | }, 1103 | "object-visit": { 1104 | "version": "1.0.1", 1105 | "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", 1106 | "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", 1107 | "dev": true, 1108 | "requires": { 1109 | "isobject": "^3.0.0" 1110 | } 1111 | }, 1112 | "object.getownpropertydescriptors": { 1113 | "version": "2.0.3", 1114 | "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", 1115 | "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", 1116 | "dev": true, 1117 | "requires": { 1118 | "define-properties": "^1.1.2", 1119 | "es-abstract": "^1.5.1" 1120 | } 1121 | }, 1122 | "object.pick": { 1123 | "version": "1.3.0", 1124 | "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", 1125 | "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", 1126 | "dev": true, 1127 | "requires": { 1128 | "isobject": "^3.0.1" 1129 | } 1130 | }, 1131 | "object.values": { 1132 | "version": "1.1.0", 1133 | "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", 1134 | "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", 1135 | "dev": true, 1136 | "requires": { 1137 | "define-properties": "^1.1.3", 1138 | "es-abstract": "^1.12.0", 1139 | "function-bind": "^1.1.1", 1140 | "has": "^1.0.3" 1141 | } 1142 | }, 1143 | "pascalcase": { 1144 | "version": "0.1.1", 1145 | "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", 1146 | "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", 1147 | "dev": true 1148 | }, 1149 | "path-parse": { 1150 | "version": "1.0.6", 1151 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 1152 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 1153 | "dev": true 1154 | }, 1155 | "posix-character-classes": { 1156 | "version": "0.1.1", 1157 | "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", 1158 | "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", 1159 | "dev": true 1160 | }, 1161 | "postcss": { 1162 | "version": "7.0.21", 1163 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", 1164 | "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", 1165 | "dev": true, 1166 | "requires": { 1167 | "chalk": "^2.4.2", 1168 | "source-map": "^0.6.1", 1169 | "supports-color": "^6.1.0" 1170 | } 1171 | }, 1172 | "postcss-nesting": { 1173 | "version": "7.0.1", 1174 | "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", 1175 | "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", 1176 | "dev": true, 1177 | "requires": { 1178 | "postcss": "^7.0.2" 1179 | } 1180 | }, 1181 | "q": { 1182 | "version": "1.5.1", 1183 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", 1184 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", 1185 | "dev": true 1186 | }, 1187 | "regex-not": { 1188 | "version": "1.0.2", 1189 | "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", 1190 | "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", 1191 | "dev": true, 1192 | "requires": { 1193 | "extend-shallow": "^3.0.2", 1194 | "safe-regex": "^1.1.0" 1195 | } 1196 | }, 1197 | "repeat-element": { 1198 | "version": "1.1.3", 1199 | "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", 1200 | "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", 1201 | "dev": true 1202 | }, 1203 | "repeat-string": { 1204 | "version": "1.6.1", 1205 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1206 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", 1207 | "dev": true 1208 | }, 1209 | "resolve": { 1210 | "version": "1.12.0", 1211 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", 1212 | "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", 1213 | "dev": true, 1214 | "requires": { 1215 | "path-parse": "^1.0.6" 1216 | } 1217 | }, 1218 | "resolve-url": { 1219 | "version": "0.2.1", 1220 | "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", 1221 | "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", 1222 | "dev": true 1223 | }, 1224 | "ret": { 1225 | "version": "0.1.15", 1226 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", 1227 | "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", 1228 | "dev": true 1229 | }, 1230 | "rollup": { 1231 | "version": "1.26.3", 1232 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.26.3.tgz", 1233 | "integrity": "sha512-8MhY/M8gnv3Q/pQQSWYWzbeJ5J1C5anCNY5BK1kV8Yzw9RFS0FF4lbLt+uyPO3wLKWXSXrhAL5pWL85TZAh+Sw==", 1234 | "dev": true, 1235 | "requires": { 1236 | "@types/estree": "*", 1237 | "@types/node": "*", 1238 | "acorn": "^7.1.0" 1239 | } 1240 | }, 1241 | "rollup-plugin-commonjs": { 1242 | "version": "10.1.0", 1243 | "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", 1244 | "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", 1245 | "dev": true, 1246 | "requires": { 1247 | "estree-walker": "^0.6.1", 1248 | "is-reference": "^1.1.2", 1249 | "magic-string": "^0.25.2", 1250 | "resolve": "^1.11.0", 1251 | "rollup-pluginutils": "^2.8.1" 1252 | } 1253 | }, 1254 | "rollup-plugin-node-resolve": { 1255 | "version": "5.2.0", 1256 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", 1257 | "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", 1258 | "dev": true, 1259 | "requires": { 1260 | "@types/resolve": "0.0.8", 1261 | "builtin-modules": "^3.1.0", 1262 | "is-module": "^1.0.0", 1263 | "resolve": "^1.11.1", 1264 | "rollup-pluginutils": "^2.8.1" 1265 | } 1266 | }, 1267 | "rollup-plugin-typescript2": { 1268 | "version": "0.21.2", 1269 | "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.21.2.tgz", 1270 | "integrity": "sha512-TfX+HLJ99p/P8kYZJdNYp9iGVWFCrj+G/V56LbEYtBqVMVHbGkrSoDH8AJjDtyRp6J9VosaKKmnBDBxhDo7TZw==", 1271 | "dev": true, 1272 | "requires": { 1273 | "fs-extra": "7.0.1", 1274 | "resolve": "1.10.1", 1275 | "rollup-pluginutils": "2.6.0", 1276 | "tslib": "1.9.3" 1277 | }, 1278 | "dependencies": { 1279 | "resolve": { 1280 | "version": "1.10.1", 1281 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", 1282 | "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", 1283 | "dev": true, 1284 | "requires": { 1285 | "path-parse": "^1.0.6" 1286 | } 1287 | }, 1288 | "rollup-pluginutils": { 1289 | "version": "2.6.0", 1290 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.6.0.tgz", 1291 | "integrity": "sha512-aGQwspEF8oPKvg37u3p7h0cYNwmJR1sCBMZGZ5b9qy8HGtETknqjzcxrDRrcAnJNXN18lBH4Q9vZYth/p4n8jQ==", 1292 | "dev": true, 1293 | "requires": { 1294 | "estree-walker": "^0.6.0", 1295 | "micromatch": "^3.1.10" 1296 | } 1297 | } 1298 | } 1299 | }, 1300 | "rollup-pluginutils": { 1301 | "version": "2.8.2", 1302 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 1303 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 1304 | "dev": true, 1305 | "requires": { 1306 | "estree-walker": "^0.6.1" 1307 | } 1308 | }, 1309 | "safe-regex": { 1310 | "version": "1.1.0", 1311 | "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", 1312 | "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", 1313 | "dev": true, 1314 | "requires": { 1315 | "ret": "~0.1.10" 1316 | } 1317 | }, 1318 | "sax": { 1319 | "version": "1.2.4", 1320 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1321 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", 1322 | "dev": true 1323 | }, 1324 | "set-value": { 1325 | "version": "2.0.1", 1326 | "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", 1327 | "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", 1328 | "dev": true, 1329 | "requires": { 1330 | "extend-shallow": "^2.0.1", 1331 | "is-extendable": "^0.1.1", 1332 | "is-plain-object": "^2.0.3", 1333 | "split-string": "^3.0.1" 1334 | }, 1335 | "dependencies": { 1336 | "extend-shallow": { 1337 | "version": "2.0.1", 1338 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 1339 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 1340 | "dev": true, 1341 | "requires": { 1342 | "is-extendable": "^0.1.0" 1343 | } 1344 | } 1345 | } 1346 | }, 1347 | "snapdragon": { 1348 | "version": "0.8.2", 1349 | "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", 1350 | "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", 1351 | "dev": true, 1352 | "requires": { 1353 | "base": "^0.11.1", 1354 | "debug": "^2.2.0", 1355 | "define-property": "^0.2.5", 1356 | "extend-shallow": "^2.0.1", 1357 | "map-cache": "^0.2.2", 1358 | "source-map": "^0.5.6", 1359 | "source-map-resolve": "^0.5.0", 1360 | "use": "^3.1.0" 1361 | }, 1362 | "dependencies": { 1363 | "define-property": { 1364 | "version": "0.2.5", 1365 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", 1366 | "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", 1367 | "dev": true, 1368 | "requires": { 1369 | "is-descriptor": "^0.1.0" 1370 | } 1371 | }, 1372 | "extend-shallow": { 1373 | "version": "2.0.1", 1374 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 1375 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 1376 | "dev": true, 1377 | "requires": { 1378 | "is-extendable": "^0.1.0" 1379 | } 1380 | }, 1381 | "source-map": { 1382 | "version": "0.5.7", 1383 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1384 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", 1385 | "dev": true 1386 | } 1387 | } 1388 | }, 1389 | "snapdragon-node": { 1390 | "version": "2.1.1", 1391 | "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", 1392 | "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", 1393 | "dev": true, 1394 | "requires": { 1395 | "define-property": "^1.0.0", 1396 | "isobject": "^3.0.0", 1397 | "snapdragon-util": "^3.0.1" 1398 | }, 1399 | "dependencies": { 1400 | "define-property": { 1401 | "version": "1.0.0", 1402 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", 1403 | "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", 1404 | "dev": true, 1405 | "requires": { 1406 | "is-descriptor": "^1.0.0" 1407 | } 1408 | }, 1409 | "is-accessor-descriptor": { 1410 | "version": "1.0.0", 1411 | "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", 1412 | "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", 1413 | "dev": true, 1414 | "requires": { 1415 | "kind-of": "^6.0.0" 1416 | } 1417 | }, 1418 | "is-data-descriptor": { 1419 | "version": "1.0.0", 1420 | "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", 1421 | "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", 1422 | "dev": true, 1423 | "requires": { 1424 | "kind-of": "^6.0.0" 1425 | } 1426 | }, 1427 | "is-descriptor": { 1428 | "version": "1.0.2", 1429 | "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", 1430 | "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", 1431 | "dev": true, 1432 | "requires": { 1433 | "is-accessor-descriptor": "^1.0.0", 1434 | "is-data-descriptor": "^1.0.0", 1435 | "kind-of": "^6.0.2" 1436 | } 1437 | } 1438 | } 1439 | }, 1440 | "snapdragon-util": { 1441 | "version": "3.0.1", 1442 | "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", 1443 | "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", 1444 | "dev": true, 1445 | "requires": { 1446 | "kind-of": "^3.2.0" 1447 | }, 1448 | "dependencies": { 1449 | "kind-of": { 1450 | "version": "3.2.2", 1451 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 1452 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 1453 | "dev": true, 1454 | "requires": { 1455 | "is-buffer": "^1.1.5" 1456 | } 1457 | } 1458 | } 1459 | }, 1460 | "source-map": { 1461 | "version": "0.6.1", 1462 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1463 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1464 | "dev": true 1465 | }, 1466 | "source-map-resolve": { 1467 | "version": "0.5.2", 1468 | "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", 1469 | "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", 1470 | "dev": true, 1471 | "requires": { 1472 | "atob": "^2.1.1", 1473 | "decode-uri-component": "^0.2.0", 1474 | "resolve-url": "^0.2.1", 1475 | "source-map-url": "^0.4.0", 1476 | "urix": "^0.1.0" 1477 | } 1478 | }, 1479 | "source-map-url": { 1480 | "version": "0.4.0", 1481 | "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", 1482 | "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", 1483 | "dev": true 1484 | }, 1485 | "sourcemap-codec": { 1486 | "version": "1.4.6", 1487 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", 1488 | "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", 1489 | "dev": true 1490 | }, 1491 | "split-string": { 1492 | "version": "3.1.0", 1493 | "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", 1494 | "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", 1495 | "dev": true, 1496 | "requires": { 1497 | "extend-shallow": "^3.0.0" 1498 | } 1499 | }, 1500 | "sprintf-js": { 1501 | "version": "1.0.3", 1502 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1503 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1504 | "dev": true 1505 | }, 1506 | "stable": { 1507 | "version": "0.1.8", 1508 | "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", 1509 | "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", 1510 | "dev": true 1511 | }, 1512 | "static-extend": { 1513 | "version": "0.1.2", 1514 | "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", 1515 | "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", 1516 | "dev": true, 1517 | "requires": { 1518 | "define-property": "^0.2.5", 1519 | "object-copy": "^0.1.0" 1520 | }, 1521 | "dependencies": { 1522 | "define-property": { 1523 | "version": "0.2.5", 1524 | "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", 1525 | "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", 1526 | "dev": true, 1527 | "requires": { 1528 | "is-descriptor": "^0.1.0" 1529 | } 1530 | } 1531 | } 1532 | }, 1533 | "string.prototype.trimleft": { 1534 | "version": "2.1.0", 1535 | "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", 1536 | "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", 1537 | "dev": true, 1538 | "requires": { 1539 | "define-properties": "^1.1.3", 1540 | "function-bind": "^1.1.1" 1541 | } 1542 | }, 1543 | "string.prototype.trimright": { 1544 | "version": "2.1.0", 1545 | "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", 1546 | "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", 1547 | "dev": true, 1548 | "requires": { 1549 | "define-properties": "^1.1.3", 1550 | "function-bind": "^1.1.1" 1551 | } 1552 | }, 1553 | "supports-color": { 1554 | "version": "6.1.0", 1555 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", 1556 | "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", 1557 | "dev": true, 1558 | "requires": { 1559 | "has-flag": "^3.0.0" 1560 | } 1561 | }, 1562 | "svgo": { 1563 | "version": "1.3.2", 1564 | "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", 1565 | "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", 1566 | "dev": true, 1567 | "requires": { 1568 | "chalk": "^2.4.1", 1569 | "coa": "^2.0.2", 1570 | "css-select": "^2.0.0", 1571 | "css-select-base-adapter": "^0.1.1", 1572 | "css-tree": "1.0.0-alpha.37", 1573 | "csso": "^4.0.2", 1574 | "js-yaml": "^3.13.1", 1575 | "mkdirp": "~0.5.1", 1576 | "object.values": "^1.1.0", 1577 | "sax": "~1.2.4", 1578 | "stable": "^0.1.8", 1579 | "unquote": "~1.1.1", 1580 | "util.promisify": "~1.0.0" 1581 | } 1582 | }, 1583 | "to-object-path": { 1584 | "version": "0.3.0", 1585 | "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", 1586 | "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", 1587 | "dev": true, 1588 | "requires": { 1589 | "kind-of": "^3.0.2" 1590 | }, 1591 | "dependencies": { 1592 | "kind-of": { 1593 | "version": "3.2.2", 1594 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 1595 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 1596 | "dev": true, 1597 | "requires": { 1598 | "is-buffer": "^1.1.5" 1599 | } 1600 | } 1601 | } 1602 | }, 1603 | "to-regex": { 1604 | "version": "3.0.2", 1605 | "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", 1606 | "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", 1607 | "dev": true, 1608 | "requires": { 1609 | "define-property": "^2.0.2", 1610 | "extend-shallow": "^3.0.2", 1611 | "regex-not": "^1.0.2", 1612 | "safe-regex": "^1.1.0" 1613 | } 1614 | }, 1615 | "to-regex-range": { 1616 | "version": "2.1.1", 1617 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", 1618 | "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", 1619 | "dev": true, 1620 | "requires": { 1621 | "is-number": "^3.0.0", 1622 | "repeat-string": "^1.6.1" 1623 | } 1624 | }, 1625 | "tslib": { 1626 | "version": "1.9.3", 1627 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 1628 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 1629 | "dev": true 1630 | }, 1631 | "typescript": { 1632 | "version": "3.6.4", 1633 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", 1634 | "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", 1635 | "dev": true 1636 | }, 1637 | "union-value": { 1638 | "version": "1.0.1", 1639 | "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", 1640 | "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", 1641 | "dev": true, 1642 | "requires": { 1643 | "arr-union": "^3.1.0", 1644 | "get-value": "^2.0.6", 1645 | "is-extendable": "^0.1.1", 1646 | "set-value": "^2.0.1" 1647 | } 1648 | }, 1649 | "universalify": { 1650 | "version": "0.1.2", 1651 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 1652 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", 1653 | "dev": true 1654 | }, 1655 | "unquote": { 1656 | "version": "1.1.1", 1657 | "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", 1658 | "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", 1659 | "dev": true 1660 | }, 1661 | "unset-value": { 1662 | "version": "1.0.0", 1663 | "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", 1664 | "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", 1665 | "dev": true, 1666 | "requires": { 1667 | "has-value": "^0.3.1", 1668 | "isobject": "^3.0.0" 1669 | }, 1670 | "dependencies": { 1671 | "has-value": { 1672 | "version": "0.3.1", 1673 | "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", 1674 | "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", 1675 | "dev": true, 1676 | "requires": { 1677 | "get-value": "^2.0.3", 1678 | "has-values": "^0.1.4", 1679 | "isobject": "^2.0.0" 1680 | }, 1681 | "dependencies": { 1682 | "isobject": { 1683 | "version": "2.1.0", 1684 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 1685 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 1686 | "dev": true, 1687 | "requires": { 1688 | "isarray": "1.0.0" 1689 | } 1690 | } 1691 | } 1692 | }, 1693 | "has-values": { 1694 | "version": "0.1.4", 1695 | "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", 1696 | "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", 1697 | "dev": true 1698 | } 1699 | } 1700 | }, 1701 | "urix": { 1702 | "version": "0.1.0", 1703 | "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", 1704 | "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", 1705 | "dev": true 1706 | }, 1707 | "use": { 1708 | "version": "3.1.1", 1709 | "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", 1710 | "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", 1711 | "dev": true 1712 | }, 1713 | "util.promisify": { 1714 | "version": "1.0.0", 1715 | "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", 1716 | "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", 1717 | "dev": true, 1718 | "requires": { 1719 | "define-properties": "^1.1.2", 1720 | "object.getownpropertydescriptors": "^2.0.3" 1721 | } 1722 | } 1723 | } 1724 | } 1725 | -------------------------------------------------------------------------------- /minimap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Minimap", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "figplug build -O -o=build", 7 | "dev": "figplug build -g -w -v -o=build" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "figplug": "^0.1.12" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /minimap/src/figutil.ts: -------------------------------------------------------------------------------- 1 | 2 | // Helper node types 3 | /** A node that may have children */ 4 | export type ContainerNode = BaseNode & ChildrenMixin 5 | 6 | // [All nodes which extends DefaultShapeMixin] 7 | /** Shape is a node with visible geometry. I.e. may have fill, stroke etc. */ 8 | type Shape = BooleanOperationNode 9 | | EllipseNode 10 | | LineNode 11 | | PolygonNode 12 | | RectangleNode 13 | | StarNode 14 | | TextNode 15 | | VectorNode 16 | 17 | /** Frame of type "FRAME" */ 18 | interface FrameFrameNode extends FrameNode { 19 | type: "FRAME" 20 | clone(): FrameFrameNode 21 | } 22 | 23 | /** Frame of type "GROUP" */ 24 | interface GroupFrameNode extends FrameNode { 25 | type: "GROUP" 26 | clone(): GroupFrameNode 27 | } 28 | 29 | // Type guards 30 | 31 | const shapeNodeTypes = { 32 | BOOLEAN_OPERATION:1, 33 | ELLIPSE:1, 34 | LINE:1, 35 | POLYGON:1, 36 | RECTANGLE:1, 37 | STAR:1, 38 | TEXT:1, 39 | VECTOR:1, 40 | } 41 | const sceneNodeTypes = { 42 | // Shapes 43 | BOOLEAN_OPERATION:1, 44 | ELLIPSE:1, 45 | LINE:1, 46 | POLYGON:1, 47 | RECTANGLE:1, 48 | STAR:1, 49 | TEXT:1, 50 | VECTOR:1, 51 | // + 52 | COMPONENT:1, 53 | FRAME:1, 54 | GROUP:1, 55 | INSTANCE:1, 56 | SLICE:1, 57 | } 58 | const containerNodeTypes = { 59 | DOCUMENT:1, 60 | PAGE:1, 61 | BOOLEAN_OPERATION:1, 62 | COMPONENT:1, 63 | FRAME:1, 64 | GROUP:1, 65 | INSTANCE:1, 66 | } 67 | 68 | export function isBooleanOperation(n :BaseNode|null|undefined): n is BooleanOperationNode { return (n && n.type == "BOOLEAN_OPERATION") as bool } 69 | export function isComponent(n :BaseNode|null|undefined): n is ComponentNode { return (n && n.type == "COMPONENT") as bool } 70 | export function isDocument(n :BaseNode|null|undefined) :n is DocumentNode { return (n && n.type == "DOCUMENT") as bool } 71 | export function isEllipse(n :BaseNode|null|undefined): n is EllipseNode { return (n && n.type == "ELLIPSE") as bool } 72 | export function isFrame(n :BaseNode|null|undefined): n is FrameFrameNode { return (n && n.type == "FRAME") as bool } 73 | export function isGroup(n :BaseNode|null|undefined): n is GroupFrameNode { return (n && n.type == "GROUP") as bool } 74 | export function isInstance(n :BaseNode|null|undefined): n is InstanceNode { return (n && n.type == "INSTANCE") as bool } 75 | export function isLine(n :BaseNode|null|undefined): n is LineNode { return (n && n.type == "LINE") as bool } 76 | export function isPage(n :BaseNode|null|undefined) :n is PageNode { return (n && n.type == "PAGE") as bool } 77 | export function isPolygon(n :BaseNode|null|undefined): n is PolygonNode { return (n && n.type == "POLYGON") as bool } 78 | export function isRectangle(n :BaseNode|null|undefined) :n is RectangleNode { return (n && n.type == "RECTANGLE") as bool } 79 | export function isSlice(n :BaseNode|null|undefined): n is SliceNode { return (n && n.type == "SLICE") as bool } 80 | export function isStar(n :BaseNode|null|undefined): n is StarNode { return (n && n.type == "STAR") as bool } 81 | export function isText(n :BaseNode|null|undefined): n is TextNode { return (n && n.type == "TEXT") as bool } 82 | export function isVector(n :BaseNode|null|undefined): n is VectorNode { return (n && n.type == "VECTOR") as bool } 83 | 84 | // Checks if node is a type with children 85 | export function isContainerNode(n :BaseNode|null|undefined): n is ContainerNode { return (n && n.type in containerNodeTypes) as bool } 86 | // Checks if node is a type of SceneNode 87 | export function isSceneNode(n :BaseNode|null|undefined): n is SceneNode { return (n && n.type in sceneNodeTypes) as bool } 88 | // Checks if node is a Shape 89 | export function isShape(n :BaseNode|null|undefined): n is Shape { return (n && n.type in shapeNodeTypes) as bool } 90 | 91 | 92 | // visit(node :ContainerNode|ReadonlyArray, visitor :NodePredicate) :Promise 93 | export function visit(node :ContainerNode, chunkTimeLimit :int, visitor :(n:BaseNode)=>any) :Promise { 94 | return new Promise(resolve => { 95 | let branches = [ node ] 96 | function visitBranches() { 97 | let startTime = Date.now() 98 | while (true) { 99 | if (Date.now() - startTime > chunkTimeLimit) { 100 | // we've locked the UI for a long time -- yield 101 | return setTimeout(visitBranches, 0) 102 | } 103 | let b = branches.shift() 104 | if (!b) { 105 | return resolve() 106 | } 107 | for (let n of b.children) { 108 | let r = visitor(n) 109 | if (r || r === undefined) { 110 | if ((n as any).children) { 111 | branches.push(n as ContainerNode) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | visitBranches() 118 | }) 119 | } 120 | 121 | -------------------------------------------------------------------------------- /minimap/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { isSceneNode, visit } from "./figutil" 2 | import { 3 | Msg, 4 | MapUpdateMsg, 5 | SetViewportMsg, 6 | UpdateViewportMsg, 7 | FocusNodesMsg, 8 | 9 | CanvasBounds, 10 | NodeInfo, 11 | Viewport, 12 | Rect, 13 | Matrix2D, 14 | } from "./structs" 15 | 16 | 17 | let isUpdatingMap = false 18 | let updateMapTimer :any = null 19 | 20 | // cache of figma.currentPage.selection. Maps to absolute rect (canvas space) 21 | let selectedNodes = new Map() 22 | 23 | // timestamp (Date.now) of last update 24 | let updateSelectedNodesTimestamp = 0 25 | 26 | // max age of selectedNodes data to consider fresh 27 | let maxSelectedNodesAge = 1000 28 | 29 | // viewport info last sent to UI 30 | let viewport :Viewport = { 31 | x: Infinity, 32 | y: Infinity, 33 | zoom: 0, 34 | } 35 | 36 | // length of longest "bounding edge" of canvas 37 | let canvasSize = 0 38 | 39 | 40 | let viewportSetSignal = false // set to true when viewport was updated by us 41 | 42 | 43 | function checkViewportChanged() :bool { 44 | let vp = figma.viewport 45 | let x = vp.center.x 46 | let y = vp.center.y 47 | if (x == viewport.x && y == viewport.y && vp.zoom == viewport.zoom) { 48 | return false 49 | } 50 | viewport.x = x 51 | viewport.y = y 52 | viewport.zoom = vp.zoom 53 | return true 54 | } 55 | 56 | 57 | const idleViewportPollTime = 500 58 | let viewportPollTime = 0 59 | 60 | 61 | function sendViewportIfChanged() :bool { 62 | return ( 63 | checkViewportChanged() && 64 | (sendToUI({ 65 | type: "update-viewport", 66 | viewport, 67 | }), 68 | true) 69 | ) 70 | } 71 | 72 | 73 | function pollViewport() { 74 | if (sendViewportIfChanged()) { 75 | if (viewportPollTime == 0) { 76 | // initial check 77 | viewportPollTime = idleViewportPollTime 78 | } else if (viewportSetSignal) { 79 | // viewport was set by user in the plugin UI. 80 | // Don't reset poll time, but clear signal instead. 81 | viewportSetSignal = false 82 | } else { 83 | viewportPollTime = 1 84 | } 85 | } else { 86 | // back off slowly 87 | // This way we stay responsive when the user is actively moving the viewport 88 | // but without burning CPU when the viewport stays unchanged. 89 | viewportPollTime = Math.min(idleViewportPollTime, viewportPollTime * 1.1) 90 | } 91 | setTimeout(pollViewport, viewportPollTime) 92 | } 93 | 94 | 95 | let updateMapAgainImmediately = false 96 | 97 | 98 | async function initCanvasSize() { 99 | let canvas :CanvasBounds = { minX:Infinity, minY:Infinity, maxX:-Infinity, maxY:-Infinity } 100 | await forEachTopLevelNode(n => { 101 | let t = getAbsoluteTransform(n) 102 | let tx = t[4], ty = t[5] 103 | canvas.minX = Math.min(canvas.minX, tx) 104 | canvas.minY = Math.min(canvas.minY, ty) 105 | canvas.maxX = Math.max(canvas.maxX, tx + n.width) 106 | canvas.maxY = Math.max(canvas.maxY, ty + n.height) 107 | }) 108 | canvasSize = Math.max(canvas.maxX - canvas.minX, canvas.maxY - canvas.minY) 109 | } 110 | 111 | 112 | async function forEachTopLevelNode(f :(n:SceneNode)=>void) { 113 | return visit(figma.currentPage, 10, n => { 114 | if ((n as any).visible && isSceneNode(n)) { 115 | f(n) 116 | } 117 | return false // don't traverse children 118 | }) 119 | } 120 | 121 | 122 | async function updateMap() { 123 | if (isUpdatingMap) { 124 | updateMapAgainImmediately = true 125 | return 126 | } 127 | let timeStarted = Date.now() 128 | isUpdatingMap = true 129 | clearTimeout(updateMapTimer) 130 | 131 | let canvas :CanvasBounds = { minX:Infinity, minY:Infinity, maxX:-Infinity, maxY:-Infinity } 132 | let nodes :NodeInfo[] = [] 133 | 134 | if (Date.now() - updateSelectedNodesTimestamp > maxSelectedNodesAge) { 135 | updateSelectedNodes() 136 | } 137 | 138 | const updateCanvasBounds = (ni :NodeInfo) => { 139 | let tx = ni.transform[4], ty = ni.transform[5] 140 | canvas.minX = Math.min(canvas.minX, tx) 141 | canvas.minY = Math.min(canvas.minY, ty) 142 | canvas.maxX = Math.max(canvas.maxX, tx + ni.width) 143 | canvas.maxY = Math.max(canvas.maxY, ty + ni.height) 144 | } 145 | 146 | // add selected nodes 147 | for (let [n, ni] of selectedNodes) { 148 | nodes.push(ni) 149 | updateCanvasBounds(ni) 150 | } 151 | 152 | // add top-level nodes 153 | await forEachTopLevelNode(n => { 154 | if (!selectedNodes.has(n)) { 155 | let ni :NodeInfo = { 156 | nodeId: n.id, 157 | width: n.width, 158 | height: n.height, 159 | transform: getAbsoluteTransform(n), 160 | name: n.name, 161 | } 162 | nodes.push(ni) 163 | updateCanvasBounds(ni) 164 | } 165 | }) 166 | 167 | // check viewport 168 | checkViewportChanged() 169 | 170 | // update canvasSize 171 | canvasSize = Math.max(canvas.maxX - canvas.minX, canvas.maxY - canvas.minY) 172 | 173 | sendToUI({ 174 | type: "map/update", 175 | nodes, 176 | canvas, 177 | viewport, 178 | }) 179 | 180 | isUpdatingMap = false 181 | if (updateMapAgainImmediately) { 182 | updateMapAgainImmediately = false 183 | updateMapTimer = setTimeout(updateMap, 0) 184 | } else { 185 | let timeSpent = Date.now() - timeStarted 186 | // update every ~500ms, however when this function takes a long time, 187 | // bakc off for at least 100ms. 188 | updateMapTimer = setTimeout(updateMap, Math.max(100, 500 - timeSpent)) 189 | } 190 | 191 | // updateWindowSize((canvas.maxX - canvas.minX) / (canvas.maxY - canvas.minY)) 192 | } 193 | 194 | 195 | // let isLandscape = true 196 | 197 | // function updateWindowSize(aspectRatio :number) { 198 | // if (aspectRatio > 1 != isLandscape) { 199 | // isLandscape = !isLandscape 200 | // if (isLandscape) { 201 | // dlog("change orientation to landscape") 202 | // figma.ui.resize(320, 240) 203 | // } else { 204 | // dlog("change orientation to portrait") 205 | // figma.ui.resize(240, 320) 206 | // } 207 | // } 208 | // } 209 | 210 | 211 | function getAbsoluteTransform(n :SceneNode) :Matrix2D { 212 | // Convert Figma.Transform to a flat vector 213 | // type Figma.Transform = [ 214 | // [a, c, tx], // [0][0] [0][1] [0][2] 215 | // [b, d, ty], // [1][0] [1][1] [1][2] 216 | // ] -> [a b c d tx ty] 217 | // ID: [ 218 | // [1, 0, 0] 219 | // [0, 1, 0] 220 | // [0, 0, 1] <-- this row is implicit and not represented by Figma.Transform 221 | // ] 222 | // 223 | // dlog("rotation:", Math.round(Math.asin(t[0][1]) * (180/Math.PI))) 224 | // let t = (n as LayoutMixin).relativeTransform 225 | // 226 | let t = (n as LayoutMixin).absoluteTransform 227 | return ( 228 | t ? [ t[0][0], t[1][0], t[0][1], t[1][1], t[0][2], t[1][2] ] 229 | : [ 1, 0, 0, 1, n.x, n.y ] 230 | ) 231 | } 232 | 233 | 234 | function updateSelectedNodes() { 235 | selectedNodes.clear() 236 | const addNode = ( 237 | n :SceneNode, 238 | selected :"direct" | "indirect" | undefined, 239 | name :string|undefined, 240 | ) => { 241 | selectedNodes.set(n, { 242 | nodeId: n.id, 243 | width: n.width, 244 | height: n.height, 245 | transform: getAbsoluteTransform(n), 246 | selected, 247 | name, 248 | }) 249 | } 250 | for (let n of figma.currentPage.selection) { 251 | addNode(n, "direct", n.name) 252 | if (n.parent && n.parent.type != "PAGE") { 253 | // add top-level parent as well. 254 | // This helps in large files where a small thing might become tiny on the map. 255 | let parent = n.parent as SceneNode & ChildrenMixin 256 | let parentM = parent // topmost parent with at least 2 children 257 | while (parent.parent && parent.parent.type != "PAGE") { 258 | parent = parent.parent as SceneNode & ChildrenMixin 259 | if (parent.children && parent.children.length > 1) { 260 | parentM = parent 261 | } 262 | } 263 | addNode(parent, "indirect", parent.name) 264 | // add siblings (first child with multiple nodes) 265 | // only include siblings which are larger than 0.5% of the canvas bounds. 266 | const minSize = canvasSize / 200 267 | for (let cn of parentM.children) { 268 | if (cn.width > minSize && cn.height > minSize && !selectedNodes.has(cn)) { 269 | // Note: Don't set name 270 | addNode(cn, undefined, undefined) 271 | } 272 | } 273 | } 274 | } 275 | updateSelectedNodesTimestamp = Date.now() 276 | } 277 | 278 | 279 | function setViewport(msg :SetViewportMsg) { 280 | viewportSetSignal = true // signal that we set the viewport (to pollViewport) 281 | figma.viewport.center = msg.position 282 | } 283 | 284 | 285 | // function onZoomMessage(msg :ZoomMsg) { 286 | // switch (msg.what) { 287 | // case "+": 288 | // figma.viewport.zoom = figma.viewport.zoom * 1.5 289 | // break 290 | // case "-": 291 | // figma.viewport.zoom = figma.viewport.zoom * 0.5 292 | // break 293 | // } 294 | // } 295 | 296 | 297 | function onFocusNodes(msg :FocusNodesMsg) { 298 | if (msg.nodeIds.length == 0) { 299 | figma.viewport.zoom = 1 300 | } else { 301 | let nodes = msg.nodeIds.map(id => 302 | figma.getNodeById(id) 303 | ).filter(n => !!n && isSceneNode(n)) as ReadonlyArray 304 | figma.currentPage.selection = nodes 305 | // save viewport center 306 | let center = figma.viewport.center 307 | // make use of scrollAndZoomIntoView to set the most appropriate zoom level for nodes 308 | figma.viewport.scrollAndZoomIntoView(nodes) 309 | // restore viewport center 310 | figma.viewport.center = center 311 | } 312 | sendViewportIfChanged() 313 | } 314 | 315 | 316 | function sendToUI(msg :M) { 317 | figma.ui.postMessage(msg) 318 | } 319 | 320 | 321 | async function main() { 322 | figma.showUI(__html__, { 323 | width: 240, 324 | height: 240, 325 | }) 326 | 327 | // figma.on("currentpagechange", () => { 328 | // dlog("page changed") 329 | // }) 330 | 331 | figma.on("selectionchange", () => { 332 | updateSelectedNodes() 333 | updateMap() 334 | // TODO: investigate why this ugly workaround is needed for when the canvas bounds changes 335 | // from creation of new stuff. 336 | setTimeout(updateMap, 1) 337 | }) 338 | 339 | // compute canvas size initially 340 | await initCanvasSize() 341 | 342 | // update selected nodes 343 | updateSelectedNodes() 344 | 345 | // start map update loop 346 | updateMap() 347 | 348 | // viewport update loop 349 | pollViewport() 350 | 351 | figma.ui.onmessage = (msg :Msg) => { 352 | assert(msg.type, "message without type") 353 | switch (msg.type) { 354 | 355 | case "set-viewport": 356 | setViewport(msg as SetViewportMsg) 357 | break 358 | 359 | case "focus-nodes": 360 | onFocusNodes(msg as FocusNodesMsg) 361 | break 362 | 363 | default: 364 | print(`[plugin] got unexpected message`, msg) 365 | } 366 | } 367 | } 368 | 369 | 370 | main() 371 | -------------------------------------------------------------------------------- /minimap/src/structs.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------------------------------------- 2 | // IPC messages 3 | 4 | export interface Msg { 5 | type :string 6 | } 7 | 8 | export interface MapUpdateMsg extends Msg { 9 | type :"map/update" 10 | nodes :NodeInfo[] 11 | canvas :CanvasBounds 12 | viewport :Viewport 13 | } 14 | 15 | export interface UpdateViewportMsg extends Msg { 16 | type :"update-viewport" 17 | viewport :Viewport 18 | } 19 | 20 | export interface SetViewportMsg extends Msg { 21 | type :"set-viewport" 22 | position :Point 23 | } 24 | 25 | export interface FocusNodesMsg extends Msg { 26 | type :"focus-nodes" 27 | nodeIds :string[] 28 | } 29 | 30 | // -------------------------------------------------------------------- 31 | // Data 32 | 33 | export interface CanvasBounds { 34 | minX :number 35 | minY :number 36 | maxX :number 37 | maxY :number 38 | } 39 | 40 | export interface Viewport extends Point { 41 | zoom :number 42 | } 43 | 44 | export interface NodeInfo extends Size { 45 | nodeId :string 46 | transform :Matrix2D 47 | selected? :"direct" | "indirect" 48 | name? :string 49 | } 50 | 51 | export type Matrix2D = [ number,number,number,number,number,number ] // [a b c d tx ty] 52 | 53 | export interface Point { 54 | x :number 55 | y :number 56 | } 57 | 58 | export interface Size { 59 | width :number 60 | height :number 61 | } 62 | 63 | export interface Rect extends Point, Size { 64 | } 65 | -------------------------------------------------------------------------------- /minimap/src/ui.css: -------------------------------------------------------------------------------- 1 | @import url("https://rsms.me/inter/inter.css"); 2 | 3 | /* reset */ 4 | * { font-family: inherit; line-height: inherit; font-synthesis: none; } 5 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, 6 | body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, 7 | em, embed, fieldset, figcaption, figure, footer, form, grid, h1, h2, h3, h4, h5, 8 | h6, header, hgroup, hr, html, i, iframe, img, ins, kbd, label, legend, li, main, 9 | mark, menu, nav, noscript, object, ol, output, p, pre, q, s, samp, section, 10 | small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, 11 | thead, time, tr, tt, u, ul, var, video { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | vertical-align: baseline; 16 | } 17 | blockquote, q { quotes: none; } 18 | blockquote:before, blockquote:after, q:before, q:after { 19 | content: ""; 20 | content: none; 21 | } 22 | table { 23 | border-collapse: collapse; 24 | border-spacing: 0; 25 | } 26 | a, a:active, a:visited { color: inherit; } 27 | /* end of reset */ 28 | 29 | :root { 30 | --fontFamily: "Inter"; 31 | --figmaBlue: #18A0FB; 32 | --viewportColorR: 255; 33 | --viewportColorG: 40; 34 | --viewportColorB: 0; 35 | } 36 | 37 | @supports (font-variation-settings: normal) { 38 | :root { --fontFamily: "Inter var"; } 39 | } 40 | 41 | body { 42 | background: transparent; 43 | color: #222; 44 | font: 11px/16px var(--fontFamily), system-ui, -system-ui, sans-serif; 45 | font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1; 46 | 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | overflow: hidden; 51 | } 52 | 53 | 54 | #info { 55 | position: fixed; 56 | left:0; right:0; bottom:0; 57 | padding: 8px; 58 | pointer-events: none; 59 | color: rgba(var(--viewportColorR), var(--viewportColorG), var(--viewportColorB), 0.5); 60 | display: flex; 61 | 62 | & .zoom { 63 | flex: 1 0 auto; 64 | text-align: right; 65 | } 66 | } 67 | 68 | 69 | #map { 70 | position: fixed; 71 | /*background: #eee;*/ 72 | 73 | /* initially absolutely positioned */ 74 | &.init { 75 | position:absolute; 76 | top:0; right:0; bottom:0; left:0; 77 | } 78 | 79 | & .viewport { 80 | position: absolute; 81 | left:0; top:0; 82 | box-sizing: border-box; 83 | border: 1.5px solid rgba(var(--viewportColorR), var(--viewportColorG), var(--viewportColorB), 0.5); 84 | transform: translate(0,0); 85 | z-index:2; 86 | border-radius:1px; 87 | pointer-events: none; 88 | /*transition: 60ms transform ease-out;*/ 89 | } 90 | 91 | & .rects { 92 | position:absolute; 93 | top:0; right:0; bottom:0; left:0; 94 | 95 | & .node { 96 | position: absolute; 97 | left:0; top:0; 98 | box-sizing: border-box; 99 | border: 1px solid rgba(0,0,0,0.2); 100 | transform-origin: 0 0; 101 | transform: translate(0,0); 102 | user-select:none; -webkit-user-select:none; 103 | /* Note: We allow pointer events to that elementFromPoint works */ 104 | 105 | &.selected { 106 | border-color: var(--figmaBlue); 107 | /*border: none;*/ 108 | /*background: var(--figmaBlue);*/ 109 | } 110 | &.selected.direct { 111 | background: var(--figmaBlue); 112 | z-index:2; 113 | } 114 | 115 | & .label { 116 | font-size: 9px; 117 | line-height: 11px; 118 | padding-left: 2px; 119 | letter-spacing: 0.02em; 120 | color: rgba(0,0,0,0.3); 121 | font-weight: 440; 122 | overflow: hidden; 123 | text-overflow: ellipsis; 124 | white-space: nowrap; 125 | user-select:none; -webkit-user-select:none; 126 | pointer-events:none; 127 | } 128 | &.selected .label { 129 | color: var(--figmaBlue); 130 | } 131 | &.selected.direct .label { 132 | color: white; 133 | font-weight:500; 134 | letter-spacing: 0.015em; /* counter-act line lenght from font weight change */ 135 | } 136 | } 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /minimap/src/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
8 |
9 |
10 |
100%
11 |
12 | 13 | -------------------------------------------------------------------------------- /minimap/src/ui.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Msg, 3 | MapUpdateMsg, 4 | SetViewportMsg, 5 | UpdateViewportMsg, 6 | FocusNodesMsg, 7 | 8 | NodeInfo, 9 | CanvasBounds, 10 | Viewport, 11 | Point, 12 | Size, 13 | Rect, 14 | Matrix2D, 15 | } from "./structs" 16 | 17 | const hasPointerEvents = typeof PointerEvent != "undefined" 18 | const captureEvent = {passive:false,capture:true} 19 | const ZeroPoint :Point = {x:0,y:0} 20 | 21 | // Viewport size approximation based on window size and known Figma UI elements 22 | const figmaSidebarWidth = 240 // not perfect; left sidebar is resizeable 23 | const figmaToolbarHeight = 40 // height of toolbar 24 | const figmaDesktopAppYChrome = 40 // height of figma desktop app vertical chrome 25 | const browserYChrome = 48 // guesstimate of vertical browser chrome 26 | const chromeWidth = figmaSidebarWidth * 2 27 | const chromeHeight = ( 28 | figmaToolbarHeight + 29 | (navigator.userAgent.indexOf("Figma") == -1 ? browserYChrome : figmaDesktopAppYChrome) 30 | ) 31 | 32 | // moveThreshold 33 | // When the user's pointer has moved at least this far since pointerdown 34 | // (measured in euclidean distance) the pointer session is considered to be a move 35 | // and the viewport will start to move along with the pointer. 36 | const moveThreshold = 2.5 //dp 37 | 38 | // doubleClickTimeThreshold 39 | // When the time between two pointerdown events is smaller or equal to this duration, 40 | // and there was no pointer movement according to moveThreshold, then the pointerdown 41 | // event is considered a "double click". 42 | const doubleClickTimeThreshold = 200 //ms 43 | 44 | // const $ = (q :string, el? :HTMLElement) :HTMLElement|null => 45 | // (el || document).querySelector(q) 46 | 47 | // const $$ = (q :string, el? :HTMLElement) :HTMLElement[] => { 48 | // let o = (el || document).querySelectorAll(q) 49 | // ;(o as any).__proto__ = Array.prototype 50 | // return o as any as HTMLElement[] 51 | // } 52 | 53 | 54 | const map = new class { 55 | el :HTMLDivElement 56 | rectsEl :HTMLDivElement 57 | viewportEl :HTMLDivElement 58 | infoEl :HTMLDivElement 59 | zoomInfoEl :HTMLDivElement 60 | 61 | // map 62 | width :int // current map width in dps (map space) 63 | height :int // current map height in dps (map space) 64 | maxWidth :int // max map width in dps (map space) 65 | maxHeight :int // max map height in dps (map space) 66 | mapOffsetX :int = 0 // dp offset in DOM document 67 | mapOffsetY :int = 0 // dp offset in DOM document 68 | paddingX :int = 8 // horizontal map padding (in map space) 69 | paddingY :int = 8 // vertical map padding (in map space) 70 | viewport :Rect = {x:0,y:0,width:0,height:0} // current viewport in map space 71 | nodes :NodeInfo[] = [] // current nodes displayed in map 72 | 73 | // canvas 74 | minX :int = 0 // min X value of canvas 75 | minY :int = 0 // min Y value of canvas 76 | scaleX :int = 0 // scale of canvas 77 | scaleY :int = 0 // scale of canvas 78 | 79 | // pointer tracking 80 | pdownTime :number = 0 // timestamp of last pointerdown event 81 | pdownPos = ZeroPoint // position of last pointerdown event 82 | isMoving = false // true after viewport has been moved beyond move-vs-click threshold 83 | wasMoving = false // sticky value of last `isMoving` 84 | 85 | // etc 86 | pxRatio :int = 1 // copy of window.devicePixelRatio 87 | timeLastSetFigmaViewport :int = 0 // last time map was updated _by_ UI 88 | 89 | constructor() { 90 | this.el = document.getElementById("map") as HTMLDivElement 91 | this.rectsEl = this.el.querySelector(".rects") as HTMLDivElement 92 | this.viewportEl = this.el.querySelector(".viewport") as HTMLDivElement 93 | this.infoEl = document.getElementById("info") as HTMLDivElement 94 | this.zoomInfoEl = this.infoEl.querySelector(".zoom") as HTMLDivElement 95 | this.width = this.maxWidth = this.el.clientWidth - this.paddingX * 2 96 | this.height = this.maxHeight = this.el.clientHeight - this.paddingY * 2 97 | this.el.classList.remove("init") 98 | 99 | if (hasPointerEvents) { 100 | document.addEventListener("pointerdown", this.onPointerDown, captureEvent) 101 | document.addEventListener("pointerup", this.onPointerUp, captureEvent) 102 | } else { 103 | // Note: Safari <=12 (ships with macOS 10.14) does not have pointer events. 104 | // Pointer events arrived in Safari 13 (macOS 10.15). 105 | document.addEventListener("mousedown", this.onPointerDown, captureEvent) 106 | document.addEventListener("mouseup", this.onPointerUp, captureEvent) 107 | } 108 | } 109 | 110 | // rectToMapSpace converts a rect that is in canvas space to map space 111 | // 112 | rectToMapSpace(r :Rect) :Rect { 113 | const m = this 114 | return { 115 | x: m.px(m.width * ((r.x - m.minX) / m.scaleX)), 116 | y: m.px(m.height * ((r.y - m.minY) / m.scaleY)), 117 | width: m.px(m.width * (r.width / m.scaleX)), 118 | height: m.px(m.height * (r.height / m.scaleY)), 119 | } 120 | } 121 | 122 | // pointToMapSpace converts a point that is in canvas space to map space 123 | // 124 | pointToMapSpace(p :Point) :Point { 125 | const m = this 126 | return { 127 | x: m.px(m.width * ((p.x - m.minX) / m.scaleX)), 128 | y: m.px(m.height * ((p.y - m.minY) / m.scaleY)), 129 | } 130 | } 131 | 132 | // sizeToMapSpace converts two lengths in canvas space to map space 133 | sizeToMapSpace(s :Size) :Size { 134 | const m = this 135 | return { 136 | width: m.px(m.width * (s.width / m.scaleX)), 137 | height: m.px(m.height * (s.height / m.scaleY)), 138 | } 139 | } 140 | 141 | // pointToCanvasSpace converts a point that is in map space to canvas space 142 | // 143 | pointToCanvasSpace(p :Point) :Point { 144 | const m = this 145 | return { 146 | x: ((p.x / m.width) * m.scaleX) + m.minX, 147 | y: ((p.y / m.height) * m.scaleY) + m.minY, 148 | } 149 | } 150 | 151 | // moves the viewport visualization (but does not send messages to plugin) 152 | // returns the center point in map space 153 | // 154 | moveViewportFromPointerEvent(ev :PointerEvent) :Point { 155 | const m = this 156 | // convert document coordinates to map space 157 | let p = { 158 | x: Math.min(m.width, Math.max(0, ev.clientX - m.mapOffsetX)), 159 | y: Math.min(m.height, Math.max(0, ev.clientY - m.mapOffsetY)), 160 | } 161 | m.moveViewport( 162 | p.x - m.viewport.width / 2, 163 | p.y - m.viewport.height / 2, 164 | ) 165 | return p 166 | } 167 | 168 | onPointerDown = (ev :PointerEvent) => { 169 | // dlog("onPointerDown", ev) 170 | const m = this 171 | ev.preventDefault() 172 | ev.stopPropagation() 173 | 174 | assert(m.isMoving == false) 175 | 176 | // is double-click? 177 | if (!m.wasMoving && ev.timeStamp - m.pdownTime <= doubleClickTimeThreshold) { 178 | // treat as double-click 179 | m.onDoubleClick(ev) 180 | return 181 | } 182 | 183 | m.pdownTime = ev.timeStamp 184 | m.pdownPos = { x: ev.clientX, y: ev.clientY } 185 | 186 | if (hasPointerEvents) { 187 | document.body.onpointermove = m.onPointerMove 188 | document.body.setPointerCapture(ev.pointerId) 189 | } else { 190 | document.body.onmousemove = m.onPointerMove 191 | document.body.onmouseup = m.onPointerUp 192 | } 193 | let p = m.moveViewportFromPointerEvent(ev) 194 | m.setFigmaViewport(p, ev.timeStamp) 195 | } 196 | 197 | onPointerMove = (ev :PointerEvent) => { 198 | // dlog("onPointerMove", ev) 199 | const m = this 200 | if (!m.isMoving) { 201 | let d = Math.abs(distance(m.pdownPos, { x: ev.clientX, y: ev.clientY })) 202 | if (d < moveThreshold) { 203 | return 204 | } 205 | m.isMoving = true 206 | } 207 | let p = m.moveViewportFromPointerEvent(ev) 208 | m.setFigmaViewport(p, ev.timeStamp) 209 | } 210 | 211 | onPointerUp = (ev :PointerEvent) => { 212 | // dlog("onPointerUp", ev) 213 | const m = this 214 | if (hasPointerEvents) { 215 | document.body.onpointermove = null 216 | document.body.releasePointerCapture(ev.pointerId) 217 | } else { 218 | document.body.onmousemove = null 219 | document.body.onmouseup = null 220 | } 221 | m.wasMoving = m.isMoving 222 | m.isMoving = false 223 | if (m.wasMoving) { 224 | let p = m.moveViewportFromPointerEvent(ev) 225 | m.setFigmaViewport(p, ev.timeStamp) 226 | } 227 | } 228 | 229 | onDoubleClick = (ev :PointerEvent) => { 230 | let el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement 231 | let nodeId = el && el.dataset ? el.dataset.nodeId : "" 232 | dlog("onDoubleClick", el, { nodeId }) 233 | sendToPlugin({ type: "focus-nodes", nodeIds: nodeId ? [ nodeId ] : [] }) 234 | } 235 | 236 | 237 | // setFigmaViewport sends a message to the plugin to set the viewport in Figma. 238 | // p is in map space 239 | // 240 | setFigmaViewport(p :Point, timestamp? :int) { 241 | const m = this 242 | // send message to plugin to change viewport 243 | sendToPlugin({ 244 | type: "set-viewport", 245 | position: m.pointToCanvasSpace(p), 246 | }) 247 | if (timestamp) { 248 | m.timeLastSetFigmaViewport = timestamp 249 | } 250 | } 251 | 252 | 253 | px(n :number) :number { 254 | return Math.round(n * this.pxRatio) / this.pxRatio 255 | } 256 | 257 | updateMapSize(aspectRatio :number) { 258 | const m = this 259 | if (aspectRatio > 1) { // landscape 260 | m.width = m.px(m.maxWidth) 261 | m.height = m.px(m.width / aspectRatio) 262 | } else { // portrait or square 263 | m.width = m.px(m.height * aspectRatio) 264 | m.height = m.px(m.maxHeight) 265 | } 266 | m.el.style.width = m.width + "px" 267 | m.el.style.height = m.height + "px" 268 | let r = m.el.getBoundingClientRect() 269 | m.mapOffsetX = r.left 270 | m.mapOffsetY = r.top 271 | } 272 | 273 | // x and y should be in map space 274 | moveViewport(x :int, y :int) { 275 | const m = this 276 | m.viewportEl.style.transform = `translate(${x}px, ${y}px)` 277 | m.viewport.x = x 278 | m.viewport.y = y 279 | } 280 | 281 | updateViewport(vp :Viewport, timestamp :number) { 282 | const m = this 283 | 284 | if (m.isMoving || 285 | (m.timeLastSetFigmaViewport > 0 && timestamp - m.timeLastSetFigmaViewport < 100) 286 | ) { 287 | // skip updating the viewport if the user is either 288 | // - moving the viewport in the minimap, or 289 | // - just recently moved it manually in the minimap. 290 | return 291 | } 292 | 293 | let z = vp.zoom 294 | let ww = (window.outerWidth - chromeWidth) / z 295 | let wh = (window.outerHeight - chromeHeight) / z 296 | 297 | let r = m.rectToMapSpace({ 298 | x: vp.x - ww/2, 299 | y: vp.y - wh/2, 300 | width: ww, 301 | height: wh, 302 | }) 303 | 304 | let s = m.viewportEl.style 305 | s.width = r.width + "px" 306 | s.height = r.height + "px" 307 | m.viewport.width = r.width 308 | m.viewport.height = r.height 309 | 310 | // clamp viewport to map 311 | // TODO: do something more fun here when the viewport is outside the canvas bounds, 312 | // like draw a line at the edge or something. 313 | let halfViewportWidth = r.width / 2 314 | let halfViewportHeight = r.height / 2 315 | let x = Math.min(m.width - halfViewportWidth, Math.max(-halfViewportWidth, r.x)) 316 | let y = Math.min(m.height - halfViewportHeight, Math.max(-halfViewportHeight, r.y)) 317 | 318 | m.moveViewport(x, y) 319 | 320 | this.zoomInfoEl.innerText = `${(vp.zoom*100).toFixed(0)}%` 321 | } 322 | 323 | 324 | updateCanvasBounds(canvas :CanvasBounds) { 325 | const m = this 326 | 327 | // update pxRatio (if window moved to a different display or display scale changed) 328 | m.pxRatio = window.devicePixelRatio || 1 329 | 330 | // note: canvas.width and .height represent maxX and maxY (not width and height) 331 | m.minX = canvas.minX 332 | m.minY = canvas.minY 333 | m.scaleX = canvas.maxX - m.minX 334 | m.scaleY = canvas.maxY - m.minY 335 | 336 | let aspectRatio = m.scaleX / m.scaleY 337 | // dlog({ aspectRatio, scaleX: m.scaleX, scaleY: m.scaleY }) 338 | m.updateMapSize(aspectRatio) 339 | } 340 | 341 | 342 | update(msg :MapUpdateMsg, timestamp :number) { 343 | const m = this 344 | 345 | m.updateCanvasBounds(msg.canvas) 346 | m.updateViewport(msg.viewport, timestamp) 347 | m.nodes = msg.nodes 348 | 349 | // let intervals :[number,NodeInfo[]][] = [] 350 | // for (let n of m.nodes) { 351 | // } 352 | 353 | m.el.style.visibility = "hidden" 354 | try { 355 | m.rectsEl.innerText = "" 356 | 357 | for (let n of m.nodes) { 358 | let nel = document.createElement("div") 359 | nel.className = "node" 360 | nel.dataset.nodeId = n.nodeId 361 | 362 | if (n.selected) { 363 | nel.classList.add("selected") 364 | nel.classList.add(n.selected) 365 | } 366 | 367 | let s = nel.style 368 | let size = m.sizeToMapSpace(n) 369 | s.width = `${size.width}px` 370 | s.height = `${size.height}px` 371 | 372 | let t = n.transform 373 | // t here is a flat version of Figma.Transform which is ordered like this: 374 | // [ a, b, c, d, tx, ty ] 375 | // ID is: 376 | // [ 1, 0, 0, 1, 0, 0 ] 377 | // Rotation matrix: 378 | // [cos(a) sin(a) -sin(a) cos(a) 0 0] // a = angle 379 | // This format matches the CSS matrix() function. 380 | // 381 | 382 | // scale x and y to map space 383 | let tr = m.pointToMapSpace({ x: t[4], y: t[5] }) 384 | let [ a, b, c, d ] = t 385 | s.transform = `matrix(${a}, ${b}, ${c}, ${d}, ${tr.x}, ${tr.y})` 386 | 387 | if (n.name && 388 | size.width > 16 && 389 | size.height > 10 && 390 | ((a == 1 && d == 1) || !maybeMirrored(t)) 391 | ) { 392 | let label = document.createElement("div") 393 | label.className = "label" 394 | label.innerText = n.name 395 | nel.appendChild(label) 396 | } 397 | 398 | m.rectsEl.appendChild(nel) 399 | } 400 | 401 | } finally { 402 | m.el.style.visibility = null 403 | } 404 | } 405 | 406 | } 407 | 408 | 409 | 410 | // maybeMirrored returns false if transformation t is _definitely_ not rotated. 411 | // However, it returns true if t _might_ be mirrored. 412 | // 413 | // Note: I'm not sure how to decompose a matrix perfectly, but for the purpose of what we need 414 | // this for (labeling), it is enough. 415 | // 416 | function maybeMirrored(t :Matrix2D) :bool { 417 | const skewX = -Math.atan2(-t[2], t[3]) 418 | const skewY = Math.atan2(t[1], t[0]) 419 | const delta = Math.abs(skewX + skewY) 420 | return !(delta < 0.00001 || Math.abs(Math.PI * 2 - delta) < 0.00001) 421 | } 422 | 423 | 424 | function distance(p1 :Point, p2 :Point) :number { 425 | let x = p1.x - p2.x 426 | let y = p1.y - p2.y 427 | return Math.sqrt(x*x + y*y) 428 | } 429 | 430 | 431 | function sendToPlugin(msg :M) { 432 | parent.postMessage({ pluginMessage: msg }, '*') 433 | } 434 | 435 | 436 | // measureFPS uses requestAnimationFrame to measure frame times and reports 437 | // the average frames per second. 438 | // 439 | function measureFPS(report :(fps:number)=>void) { 440 | const samplesSize = 120 // total number of frame times look at (window size) 441 | const samples :number[] = [] // ring buffer; sliding window 442 | const reportAt = Math.round(samplesSize / 4) 443 | 444 | let samplesIndex = 0 // next index in samples 445 | let prevTime = 0 // last time value; frameTime = prevTime - time 446 | 447 | const maxMissedReports = 2 448 | const reportTimeMissThreshold = (reportAt/60) * maxMissedReports * 1000 449 | let lastReportTime = 0 450 | // When a tab goes idle, this function is not called for a while. 451 | // Then when the tab goes active, it's called with a huge time delta, pulling down the 452 | // FPS considerably. To counter for this, we record the real time when we report. 453 | // Before we make a report, we look to see if we missed more than maxMissedReports reports, 454 | // and if so, we reset and start over. 455 | 456 | const sample = (time :number) => { 457 | samples[samplesIndex++] = time - prevTime 458 | prevTime = time 459 | if (samples.length == samplesSize) { 460 | if (samplesIndex == samplesSize) { 461 | samplesIndex = 0 462 | } 463 | if (samplesIndex % reportAt == 0) { 464 | // report 465 | let now = Date.now() 466 | if (lastReportTime != 0 && now - lastReportTime > reportTimeMissThreshold) { 467 | // tab went idle for a while and missed some reports. Reset. 468 | samples.length = 0 469 | lastReportTime = 0 470 | samplesIndex = 0 471 | } else { 472 | lastReportTime = now 473 | let avgFrameTime = samples.reduce((v, a) => a + v) / samplesSize 474 | report(1000/avgFrameTime) 475 | } 476 | } 477 | } 478 | requestAnimationFrame(sample) 479 | } 480 | requestAnimationFrame(time => { 481 | prevTime = time 482 | requestAnimationFrame(sample) 483 | }) 484 | } 485 | 486 | 487 | function main() { 488 | if (DEBUG) { 489 | // Note: Even though this could be nice in production, it burns CPU at a steady rate 490 | // which is not great. So, for now, just enable the FPS meter in debug builds. 491 | let fpsEl = document.createElement("div") 492 | fpsEl.className = "fps" 493 | map.infoEl.insertBefore(fpsEl, map.infoEl.firstChild) 494 | fpsEl.innerText = "∞ FPS" 495 | measureFPS(fps => { 496 | fpsEl.innerText = (fps > 0 ? fps.toFixed(0) : parseFloat(fps.toFixed(2))) + " FPS" 497 | }) 498 | } 499 | 500 | window.onmessage = (ev :MessageEvent) => { 501 | let msg = ev.data.pluginMessage as Msg 502 | assert(typeof msg.type == "string") 503 | switch (msg.type as string) { 504 | 505 | case "map/update": 506 | map.update(msg as MapUpdateMsg, ev.timeStamp) 507 | break 508 | 509 | case "update-viewport": 510 | map.updateViewport((msg as UpdateViewportMsg).viewport, ev.timeStamp) 511 | break 512 | 513 | default: 514 | print(`[ui] got unexpected message`, msg) 515 | } 516 | } 517 | } 518 | 519 | 520 | main() 521 | -------------------------------------------------------------------------------- /minimap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2017", 5 | "lib": [ 6 | "es2017", 7 | "dom" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /misc/optimize-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | cd "$(dirname "$0")/.." 3 | 4 | pwd 5 | 6 | OIFS="$IFS" 7 | IFS=$'\n' 8 | 9 | for f in $(\ 10 | find . -type f -name '*.png' \ 11 | -not -path "*/node_modules/*" \ 12 | -not -path "./.git/*" \ 13 | -not -path "*/_*" ) 14 | do 15 | echo "$f" 16 | TMPNAME=$(dirname "$f")/.$(basename "$f").tmp 17 | (pngcrush -q "$f" "$TMPNAME" && mv -f "$TMPNAME" "$f") & 18 | done 19 | 20 | for f in $(\ 21 | find . -type f -name '*.svg' \ 22 | -not -path "*/node_modules/*" \ 23 | -not -path "./.git/*" \ 24 | -not -path "*/_*" ) 25 | do 26 | echo "$f" 27 | svgo --multipass -q "$f" & 28 | done 29 | 30 | IFS="$OIFS" 31 | 32 | wait 33 | --------------------------------------------------------------------------------