├── .editorconfig ├── .gitattributes ├── .gitignore ├── ControlPanelLayer.coffee ├── LICENSE ├── README.md ├── example.coffee ├── example.framer ├── app.coffee └── modules │ └── ControlPanelLayer.coffee ├── module.json ├── package.json └── thumb.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /ControlPanelLayer.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # USING THE CONTROLPANELLAYER MODULE 3 | 4 | # Require the module 5 | ControlPanelLayer = require "ControlPanelLayer" 6 | 7 | myControlPanel = new ControlPanelLayer 8 | scaleFactor: 9 | specs: 10 | draggable: 11 | textColor: (hex or rgba) 12 | backgroundColor: (hex or rgba) 13 | inputTextColor: (hex or rgba) 14 | inputBackgroundColor: (hex or rgba) 15 | buttonTextColor: (hex or rgba) 16 | buttonColor: (hex or rgba) 17 | commitAction: -> 18 | closeAction: -> 19 | 20 | # The specs object 21 | 22 | # The ControlPanelLayer requires your behavior specifications to be organized in key-value object form. Each item must include a `label` and `value`. Optionally you may include an explanatory `tip`. Additional keys will be ignored. 23 | 24 | # Specs object values can include strings, numbers and booleans. 25 | 26 | exampleSpecs = 27 | defaultText: 28 | label: "Default text" 29 | value: "hello" 30 | tip: "Initial text to display." 31 | animationTime: 32 | label: "Animation time" 33 | value: 5 34 | tip: "How long the animation will run." 35 | autoplay: 36 | label: "Should autoplay" 37 | value: false 38 | 39 | # Referring to a particular spec using such an object is done with the usual dot notation or bracket notation, e.g. `exampleSpecs.animationTime.value` or `exampleSpecs["animationTime"]["value"]` or `exampleSpecs["animationTime"].value`. 40 | 41 | # The commit action 42 | 43 | # The ControlPanelLayer features a Commit button which can be customized to perform any action. You will want to at least overwrite your specs object with any changes effected via the ControlPanelLayer. 44 | 45 | myControlPanel = new ControlPanelLayer 46 | specs: exampleSpecs 47 | commitAction: -> exampleSpecs = this.specs 48 | 49 | # The close action 50 | 51 | # The panel close button works to hide the panel, but you may supply it with additional functionality. 52 | 53 | myControlPanel = new ControlPanelLayer 54 | specs: exampleSpecs 55 | closeAction: -> print "panel closed" 56 | 57 | # Integration with QueryInterface (https://github.com/marckrenn/framer-QueryInterface/) 58 | 59 | {QueryInterface} = require 'QueryInterface' 60 | 61 | querySpecs = new QueryInterface 62 | key: "specs" 63 | default: exampleSpecs 64 | 65 | myControlPanel = new ControlPanelLayer 66 | specs: querySpecs.value 67 | commitAction: -> querySpecs.value = this.specs; window.location.reload(false) 68 | 69 | # Show or hide the ControlPanelLayer 70 | myControlPanel.show() 71 | myControlPanel.hide() 72 | myControlPanel.hidden (, returns whether the ControlPanelLayer is currently hidden) 73 | 74 | # Known issues 75 | 76 | # Creating multiple ControlPanelLayers with different scale factors will result in unexpected input field effects. 77 | ### 78 | 79 | defaults = 80 | specs: {} 81 | scaleFactor: 1 82 | draggable: true 83 | textColor: "white" 84 | inputBackgroundColor: "rgba(255,255,255,0.8)" 85 | inputTextColor: "black" 86 | backgroundColor: "rgba(0,0,0,0.5)" 87 | buttonTextColor: "black" 88 | buttonColor: "white" 89 | width: 350 90 | showGuides: false 91 | hidden: false 92 | commitAction: () -> 93 | closeAction: () -> 94 | 95 | class ControlPanelLayer extends Layer 96 | constructor: (@options={}) -> 97 | @options = _.assign({}, defaults, @options) 98 | super @options 99 | 100 | rowHeight = 32 * @options.scaleFactor 101 | panelTopMargin = 15 * @options.scaleFactor 102 | panelBottomMargin = 15 * @options.scaleFactor 103 | panelSideMargin = 30 * @options.scaleFactor 104 | panelButtonMargin = 15 * @options.scaleFactor 105 | panelRowHeight = 34 * @options.scaleFactor 106 | minimumPanelHeight = 2 * panelRowHeight + panelTopMargin + panelBottomMargin 107 | panelLabelSize = 16 * @options.scaleFactor 108 | panelLabelMargin = 6 * @options.scaleFactor 109 | panelTipSize = 12 * @options.scaleFactor 110 | panelTipMargin = -10 * @options.scaleFactor 111 | radioButtonSize = 20 * @options.scaleFactor 112 | radioButtonMarkSize = 12 * @options.scaleFactor 113 | radioButtonTopMargin = 6 * @options.scaleFactor 114 | inputWidth = 50 * @options.scaleFactor 115 | inputTopMargin = panelLabelSize/4 116 | inputTopOffet = if @options.scaleFactor == 1 then -3 else 0 117 | svgTopOffset = if @options.scaleFactor == 1 then 2 else 0 118 | commitButtonHeight = 30 * @options.scaleFactor 119 | commitButtonTopMargin = 5 * @options.scaleFactor 120 | codeVariableColor = "#ed6a43" 121 | codeBracketColor = "#a71d5d" 122 | codeBodyColor = if @options.buttonTextColor == "black" then "#24292e" else @options.buttonTextColor 123 | closeButtonSize = 24 * @options.scaleFactor 124 | closeButtonMargin = 8 * @options.scaleFactor 125 | closeButtonGlyphMargin = 4 * @options.scaleFactor 126 | closeGlyphHeight = 2 * @options.scaleFactor 127 | closeGlyphWidth = closeButtonSize - closeButtonGlyphMargin * 2 128 | closeGlyphTop = closeButtonSize/2 - 1 * @options.scaleFactor 129 | closeGlyphRotationX = closeButtonSize/2 130 | closeGlyphRotationY = closeButtonSize/2 131 | inputInsetShadowColor = new Color(@options.inputBackgroundColor).darken(30) 132 | alertString = "

Add specs with specs: <mySpecs>

" 133 | commitString = "

Commit

" 134 | 135 | rowCount = Object.keys(@options.specs).length + 1 # allow for Commit button 136 | rows = [] 137 | @.name = "controlPanel" 138 | @.width = @options.width * @options.scaleFactor 139 | @.height = panelRowHeight * rowCount + panelTopMargin + panelBottomMargin 140 | @.borderRadius = 10 * @options.scaleFactor 141 | @.shadowBlur = 20 * @options.scaleFactor 142 | @.shadowColor = "rgba(0,0,0,0.3)" 143 | @.backgroundColor = @options.backgroundColor 144 | @.draggable = @options.draggable 145 | @.draggable.momentum = false 146 | 147 | labelWidth = @.width - 125 * @options.scaleFactor 148 | 149 | inputCSS = """ 150 | input[type='text'] { 151 | color: #{@options.inputTextColor}; 152 | background-color: #{@options.inputBackgroundColor}; 153 | font-family: -apple-system, Helvetica, Arial, sans-serif; 154 | font-weight: 500; 155 | text-align: right; 156 | font-size: #{panelLabelSize}px; 157 | margin-top: #{inputTopMargin}px; 158 | padding: #{panelLabelSize/8}px; 159 | appearance: none; 160 | width: #{inputWidth - panelLabelSize/8}px; 161 | box-shadow: inset 0px 1px 2px 0 #{inputInsetShadowColor}; 162 | border-radius: #{3 * @options.scaleFactor}px; 163 | position: relative; 164 | top: #{inputTopOffet}px; 165 | }""" 166 | 167 | Utils.insertCSS(inputCSS) 168 | 169 | keyIndex = 0 170 | for row of @options.specs 171 | do (row) => 172 | tipRequired = if @options.specs[row].tip != "" and @options.specs[row].tip != undefined then true else false 173 | 174 | rowBlock = new Layer 175 | name: "row" + keyIndex 176 | parent: @ 177 | x: panelSideMargin 178 | y: if keyIndex > 0 then rows[keyIndex - 1].maxY else panelTopMargin + keyIndex * panelRowHeight 179 | height: if tipRequired then panelRowHeight * 1.5 else panelRowHeight 180 | width: labelWidth 181 | backgroundColor: "clear" 182 | 183 | rows.push(rowBlock) 184 | 185 | label = new Layer 186 | name: _.camelCase(@options.specs[row].label + "Label") 187 | parent: rowBlock 188 | height: panelRowHeight 189 | width: labelWidth 190 | y: panelLabelMargin 191 | backgroundColor: "clear" 192 | html: "

#{@options.specs[row].label}

" 193 | style: 194 | "font-size": panelLabelSize + "px" 195 | "font-weight": "500" 196 | "text-align": "right" 197 | "color": @options.textColor 198 | 199 | @[label.name] = label 200 | 201 | if tipRequired 202 | tip = new Layer 203 | name: _.camelCase(@options.specs[row].label + "Tip") 204 | parent: rowBlock 205 | height: panelRowHeight * 0.4 206 | width: labelWidth 207 | y: label.maxY + panelTipMargin 208 | backgroundColor: "clear" 209 | html: "

#{@options.specs[row].tip}

" 210 | style: 211 | "font-size": panelTipSize + "px" 212 | "font-weight": "500" 213 | "text-align": "right" 214 | "color": @options.textColor 215 | 216 | @[tip.name] = tip 217 | 218 | idString = _.camelCase(@options.specs[row].label + "Input") 219 | switch typeof @options.specs[row].value 220 | when "boolean" 221 | input = new Layer 222 | name: idString 223 | parent: rowBlock 224 | x: Align.right(inputWidth) 225 | y: Align.top(radioButtonTopMargin) 226 | height: radioButtonSize 227 | width: radioButtonSize 228 | borderRadius: radioButtonSize/2 229 | borderColor: @options.textColor 230 | borderWidth: 1 * @options.scaleFactor 231 | backgroundColor: "clear" 232 | 233 | inputMark = new Layer 234 | name: "mark" 235 | parent: input 236 | width: radioButtonMarkSize 237 | height: radioButtonMarkSize 238 | x: (radioButtonSize-radioButtonMarkSize)/2 239 | y: (radioButtonSize-radioButtonMarkSize)/2 240 | borderRadius: radioButtonMarkSize/2 241 | backgroundColor: @options.textColor 242 | visible: @options.specs[row].value 243 | 244 | input.mark = inputMark 245 | input.row = row 246 | 247 | input.onClick => 248 | if @options.specs[input.row].value == true 249 | @options.specs[input.row].value = false 250 | input.mark.visible = false 251 | else 252 | @options.specs[input.row].value = true 253 | input.mark.visible = true 254 | 255 | else 256 | input = new Layer 257 | name: idString 258 | parent: rowBlock 259 | x: Align.right((@.width - labelWidth)/2) 260 | y: Align.top 261 | color: @options.textColor 262 | html: "" 263 | height: panelRowHeight 264 | width: inputWidth 265 | backgroundColor: "clear" 266 | 267 | @[input.name] = input 268 | 269 | if @options.showGuides == true 270 | guide = new Layer 271 | name: "guide" 272 | parent: rowBlock 273 | width: @.width 274 | x: -panelSideMargin 275 | backgroundColor: "red" 276 | height: 1 277 | y: panelLabelSize * 1.3 278 | opacity: 0.5 279 | 280 | ++keyIndex 281 | 282 | 283 | @.height = Math.max(minimumPanelHeight, @.contentFrame().height + panelTopMargin + panelBottomMargin + commitButtonHeight + commitButtonTopMargin) 284 | 285 | closeButton = new Layer 286 | name: "closeButton" 287 | parent: @ 288 | width: closeButtonSize 289 | height: closeButtonSize 290 | borderRadius: closeButtonSize/2 291 | backgroundColor: "rgba(0,0,0,0.15)" 292 | borderWidth: 1 * @options.scaleFactor 293 | borderColor: "rgba(255,255,255,0.5)" 294 | x: closeButtonMargin 295 | y: closeButtonMargin 296 | 297 | @.closeButton = closeButton 298 | 299 | closeButton.onClick => 300 | @.hide() 301 | @options.closeAction() 302 | 303 | closeButtonStroke1 = new Layer 304 | name: "closeButtonStroke1" 305 | parent: closeButton 306 | width: closeGlyphWidth 307 | height: closeGlyphHeight 308 | x: (closeButtonSize-closeGlyphWidth)/2 309 | y: (closeButtonSize-closeGlyphHeight)/2 310 | rotation: 45 311 | borderRadius: closeGlyphHeight/2 312 | backgroundColor: "white" 313 | 314 | closeButtonStroke2 = new Layer 315 | name: "closeButtonStroke2" 316 | parent: closeButton 317 | width: closeGlyphWidth 318 | height: closeGlyphHeight 319 | x: (closeButtonSize-closeGlyphWidth)/2 320 | y: (closeButtonSize-closeGlyphHeight)/2 321 | rotation: -45 322 | borderRadius: closeGlyphHeight/2 323 | backgroundColor: "white" 324 | 325 | commitButton = new Layer 326 | name: "commitButton" 327 | parent: @ 328 | width: @.width - panelButtonMargin * 2 329 | height: commitButtonHeight 330 | x: Align.center 331 | y: Align.bottom(- panelBottomMargin) 332 | backgroundColor: @options.buttonColor 333 | html: if Object.keys(@options.specs).length == 0 then alertString else commitString 334 | borderRadius: 5 * @options.scaleFactor 335 | 336 | @.commitButton = commitButton 337 | 338 | commitButton.onClick => 339 | for row of @options.specs 340 | do(row) => 341 | idString = _.camelCase(@options.specs[row].label + "Input") 342 | switch typeof @options.specs[row].value 343 | when "string" 344 | typedValue = document.getElementById(idString).value 345 | when "number" 346 | typedValue = +document.getElementById(idString).value 347 | when "boolean" 348 | typedValue = @options.specs[row].value 349 | else 350 | typedValue = document.getElementById(idString).value 351 | @options.specs[row].value = typedValue 352 | @options.commitAction() 353 | 354 | if @options.hidden == true 355 | @.hide() 356 | 357 | hide: () => 358 | @options.hidden = true 359 | @.animate 360 | properties: 361 | opacity: 0 362 | time: 363 | 0.25 364 | Utils.delay 0.25, => 365 | @.visible = false 366 | 367 | show: () => 368 | @.visible = true 369 | @options.hidden = false 370 | @.animate 371 | properties: 372 | opacity: 1 373 | time: 374 | 0.25 375 | 376 | @define 'specs', get: () -> @options.specs 377 | @define 'hidden', get: () -> @options.hidden 378 | module.exports = ControlPanelLayer 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Black Pixel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ControlPanelLayer Framer Module 2 | 3 | [![license](https://img.shields.io/github/license/bpxl-labs/RemoteLayer.svg)](https://opensource.org/licenses/MIT) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](.github/CONTRIBUTING.md) 5 | [![Maintenance](https://img.shields.io/maintenance/yes/2017.svg)]() 6 | 7 | Install with Framer Modules 8 | 9 | The ControlPanelLayer module makes it easy to construct a developer panel for controlling aspects of your prototype within the prototype itself. 10 | 11 | ControlPanelLayer preview 12 | 13 | ### Installation 14 | 15 | #### NPM Installation 16 | 17 | ```bash 18 | $ cd /your/framer/project 19 | $ npm i @blackpixel/framer-controlpanellayer 20 | ``` 21 | 22 | #### Manual installation 23 | 24 | Copy or save the `ControlPanelLayer.coffee` file into your project's `modules` folder. 25 | 26 | ### Adding It to Your Project 27 | 28 | In your Framer project, add the following: 29 | 30 | ```coffeescript 31 | # If you manually installed 32 | ControlPanelLayer = require "ControlPanelLayer" 33 | # else 34 | ControlPanelLayer = require "@blackpixel/framer-controlpanellayer" 35 | ``` 36 | 37 | ### API 38 | 39 | #### `new ControlPanelLayer` 40 | 41 | Instantiates a new instance of ControlPanelLayer. 42 | 43 | #### Available options 44 | 45 | ```coffeescript 46 | myControlPanel = new ControlPanelLayer 47 | scaleFactor: 48 | specs: 49 | draggable: 50 | textColor: (hex or rgba) 51 | backgroundColor: (hex or rgba) 52 | inputTextColor: (hex or rgba) 53 | inputBackgroundColor: (hex or rgba) 54 | buttonTextColor: (hex or rgba) 55 | buttonColor: (hex or rgba) 56 | commitAction: -> 57 | closeAction: -> 58 | ``` 59 | 60 | #### The specs object 61 | 62 | The ControlPanelLayer requires your behavior specifications to be organized in key-value object form. Each item must include a `label` and `value`. Optionally, you may include an explanatory `tip`. Additional keys will be ignored. 63 | 64 | Specs object values can include strings, numbers, and booleans. 65 | 66 | ```coffeescript 67 | exampleSpecs = 68 | defaultText: 69 | label: "Default text" 70 | value: "hello" 71 | tip: "Initial text to display." 72 | animationTime: 73 | label: "Animation time" 74 | value: 5 75 | tip: "How long the animation will run." 76 | autoplay: 77 | label: "Should autoplay" 78 | value: false 79 | ``` 80 | 81 | Referring to a particular spec is done with the usual dot or bracket notation, e.g., `exampleSpecs.animationTime.value` or `exampleSpecs["animationTime"]["value"]` or `exampleSpecs["animationTime"].value`. 82 | 83 | #### The commit action 84 | 85 | The ControlPanelLayer features a Commit button that can be customized to perform any action. You will want to at least overwrite your specs object with any changes that resulted from using the ControlPanelLayer. 86 | 87 | ```coffeescript 88 | myControlPanel = new ControlPanelLayer 89 | specs: exampleSpecs 90 | commitAction: -> exampleSpecs = this.specs 91 | ``` 92 | 93 | #### The close action 94 | 95 | The panel close button works to hide the panel, but you may supply it with additional functionality. 96 | 97 | ```coffeescript 98 | myControlPanel = new ControlPanelLayer 99 | specs: exampleSpecs 100 | closeAction: -> print "panel closed" 101 | ``` 102 | 103 | #### Example of integration with [QueryInterface](https://github.com/marckrenn/framer-QueryInterface/) 104 | 105 | Using ControlPanelLayer in conjunction with QueryInterface provides a way to maintain settings across a reload or link to your prototype with custom settings included in the URL. 106 | 107 | ```coffeescript 108 | {QueryInterface} = require 'QueryInterface' 109 | 110 | querySpecs = new QueryInterface 111 | key: "specs" 112 | default: exampleSpecs 113 | 114 | myControlPanel = new ControlPanelLayer 115 | specs: querySpecs.value 116 | commitAction: -> querySpecs.value = this.specs; window.location.reload(false) 117 | ``` 118 | 119 | #### `myControlPanel.show()` 120 | 121 | Show the ControlPanelLayer instance. 122 | 123 | #### `myControlPanel.hide()` 124 | 125 | Hide the ControlPanelLayer instance. 126 | 127 | #### `myControlPanel.hidden` 128 | 129 | > readonly 130 | 131 | **Returns** 132 | 133 | _(Boolean)_: Whether or not the ControlPanelLayer is currently hidden. 134 | 135 | ### Example project 136 | [Download](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/bpxl-labs/ControlPanelLayer/tree/master/example.framer) the example to try it for yourself. 137 | 138 | --- 139 | 140 | Website: [blackpixel.com](https://blackpixel.com)  ·  141 | GitHub: [@bpxl-labs](https://github.com/bpxl-labs/)  ·  142 | Twitter: [@blackpixel](https://twitter.com/blackpixel)  ·  143 | Medium: [@bpxl-craft](https://medium.com/bpxl-craft) 144 | -------------------------------------------------------------------------------- /example.coffee: -------------------------------------------------------------------------------- 1 | ############################################ 2 | # Example usage. 3 | # For all features, please check the README. 4 | ############################################ 5 | 6 | exampleSpecs = 7 | defaultText: 8 | label: "Default text" 9 | value: "hello" 10 | tip: "Initial text to display." 11 | animationTime: 12 | label: "Animation time" 13 | value: 5 14 | tip: "How long the animation will run." 15 | autoplay: 16 | label: "Should autoplay" 17 | value: false 18 | 19 | myControlPanel = new ControlPanelLayer 20 | specs: exampleSpecs 21 | commitAction: -> commitAction: -> exampleSpecs = this.specs 22 | -------------------------------------------------------------------------------- /example.framer/app.coffee: -------------------------------------------------------------------------------- 1 | ControlPanelLayer = require "ControlPanelLayer" 2 | 3 | ############################################ 4 | # Example usage. 5 | # For all features, please check the README. 6 | ############################################ 7 | 8 | exampleSpecs = 9 | defaultText: 10 | label: "Default text" 11 | value: "hello" 12 | tip: "Initial text to display." 13 | animationTime: 14 | label: "Animation time" 15 | value: 5 16 | tip: "How long the animation will run." 17 | autoplay: 18 | label: "Should autoplay" 19 | value: false 20 | 21 | myControlPanel = new ControlPanelLayer 22 | specs: exampleSpecs 23 | commitAction: -> commitAction: -> exampleSpecs = this.specs 24 | -------------------------------------------------------------------------------- /example.framer/modules/ControlPanelLayer.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # USING THE CONTROLPANELLAYER MODULE 3 | 4 | # Require the module 5 | ControlPanelLayer = require "ControlPanelLayer" 6 | 7 | myControlPanel = new ControlPanelLayer 8 | scaleFactor: 9 | specs: 10 | draggable: 11 | textColor: (hex or rgba) 12 | backgroundColor: (hex or rgba) 13 | inputTextColor: (hex or rgba) 14 | inputBackgroundColor: (hex or rgba) 15 | buttonTextColor: (hex or rgba) 16 | buttonColor: (hex or rgba) 17 | commitAction: -> 18 | closeAction: -> 19 | 20 | # The specs object 21 | 22 | # The ControlPanelLayer requires your behavior specifications to be organized in key-value object form. Each item must include a `label` and `value`. Optionally you may include an explanatory `tip`. Additional keys will be ignored. 23 | 24 | # Specs object values can include strings, numbers and booleans. 25 | 26 | exampleSpecs = 27 | defaultText: 28 | label: "Default text" 29 | value: "hello" 30 | tip: "Initial text to display." 31 | animationTime: 32 | label: "Animation time" 33 | value: 5 34 | tip: "How long the animation will run." 35 | autoplay: 36 | label: "Should autoplay" 37 | value: false 38 | 39 | # Referring to a particular spec using such an object is done with the usual dot notation or bracket notation, e.g. `exampleSpecs.animationTime.value` or `exampleSpecs["animationTime"]["value"]` or `exampleSpecs["animationTime"].value`. 40 | 41 | # The commit action 42 | 43 | # The ControlPanelLayer features a Commit button which can be customized to perform any action. You will want to at least overwrite your specs object with any changes effected via the ControlPanelLayer. 44 | 45 | myControlPanel = new ControlPanelLayer 46 | specs: exampleSpecs 47 | commitAction: -> exampleSpecs = this.specs 48 | 49 | # The close action 50 | 51 | # The panel close button works to hide the panel, but you may supply it with additional functionality. 52 | 53 | myControlPanel = new ControlPanelLayer 54 | specs: exampleSpecs 55 | closeAction: -> print "panel closed" 56 | 57 | # Integration with QueryInterface (https://github.com/marckrenn/framer-QueryInterface/) 58 | 59 | {QueryInterface} = require 'QueryInterface' 60 | 61 | querySpecs = new QueryInterface 62 | key: "specs" 63 | default: exampleSpecs 64 | 65 | myControlPanel = new ControlPanelLayer 66 | specs: querySpecs.value 67 | commitAction: -> querySpecs.value = this.specs; window.location.reload(false) 68 | 69 | # Show or hide the ControlPanelLayer 70 | myControlPanel.show() 71 | myControlPanel.hide() 72 | myControlPanel.hidden (, returns whether the ControlPanelLayer is currently hidden) 73 | 74 | # Known issues 75 | 76 | # Creating multiple ControlPanelLayers with different scale factors will result in unexpected input field effects. 77 | ### 78 | 79 | defaults = 80 | specs: {} 81 | scaleFactor: 1 82 | draggable: true 83 | textColor: "white" 84 | inputBackgroundColor: "rgba(255,255,255,0.8)" 85 | inputTextColor: "black" 86 | backgroundColor: "rgba(0,0,0,0.5)" 87 | buttonTextColor: "black" 88 | buttonColor: "white" 89 | width: 350 90 | showGuides: false 91 | hidden: false 92 | commitAction: () -> 93 | closeAction: () -> 94 | 95 | class ControlPanelLayer extends Layer 96 | constructor: (@options={}) -> 97 | @options = _.assign({}, defaults, @options) 98 | super @options 99 | 100 | rowHeight = 32 * @options.scaleFactor 101 | panelTopMargin = 15 * @options.scaleFactor 102 | panelBottomMargin = 15 * @options.scaleFactor 103 | panelSideMargin = 30 * @options.scaleFactor 104 | panelButtonMargin = 15 * @options.scaleFactor 105 | panelRowHeight = 34 * @options.scaleFactor 106 | minimumPanelHeight = 2 * panelRowHeight + panelTopMargin + panelBottomMargin 107 | panelLabelSize = 16 * @options.scaleFactor 108 | panelLabelMargin = 6 * @options.scaleFactor 109 | panelTipSize = 12 * @options.scaleFactor 110 | panelTipMargin = -10 * @options.scaleFactor 111 | radioButtonSize = 20 * @options.scaleFactor 112 | radioButtonMarkSize = 12 * @options.scaleFactor 113 | radioButtonTopMargin = 6 * @options.scaleFactor 114 | inputWidth = 50 * @options.scaleFactor 115 | inputTopMargin = panelLabelSize/4 116 | inputTopOffet = if @options.scaleFactor == 1 then -3 else 0 117 | svgTopOffset = if @options.scaleFactor == 1 then 2 else 0 118 | commitButtonHeight = 30 * @options.scaleFactor 119 | commitButtonTopMargin = 5 * @options.scaleFactor 120 | codeVariableColor = "#ed6a43" 121 | codeBracketColor = "#a71d5d" 122 | codeBodyColor = if @options.buttonTextColor == "black" then "#24292e" else @options.buttonTextColor 123 | closeButtonSize = 24 * @options.scaleFactor 124 | closeButtonMargin = 8 * @options.scaleFactor 125 | closeButtonGlyphMargin = 4 * @options.scaleFactor 126 | closeGlyphHeight = 2 * @options.scaleFactor 127 | closeGlyphWidth = closeButtonSize - closeButtonGlyphMargin * 2 128 | closeGlyphTop = closeButtonSize/2 - 1 * @options.scaleFactor 129 | closeGlyphRotationX = closeButtonSize/2 130 | closeGlyphRotationY = closeButtonSize/2 131 | inputInsetShadowColor = new Color(@options.inputBackgroundColor).darken(30) 132 | alertString = "

Add specs with specs: <mySpecs>

" 133 | commitString = "

Commit

" 134 | 135 | rowCount = Object.keys(@options.specs).length + 1 # allow for Commit button 136 | rows = [] 137 | @.name = "controlPanel" 138 | @.width = @options.width * @options.scaleFactor 139 | @.height = panelRowHeight * rowCount + panelTopMargin + panelBottomMargin 140 | @.borderRadius = 10 * @options.scaleFactor 141 | @.shadowBlur = 20 * @options.scaleFactor 142 | @.shadowColor = "rgba(0,0,0,0.3)" 143 | @.backgroundColor = @options.backgroundColor 144 | @.draggable = @options.draggable 145 | @.draggable.momentum = false 146 | 147 | labelWidth = @.width - 125 * @options.scaleFactor 148 | 149 | inputCSS = """ 150 | input[type='text'] { 151 | color: #{@options.inputTextColor}; 152 | background-color: #{@options.inputBackgroundColor}; 153 | font-family: -apple-system, Helvetica, Arial, sans-serif; 154 | font-weight: 500; 155 | text-align: right; 156 | font-size: #{panelLabelSize}px; 157 | margin-top: #{inputTopMargin}px; 158 | padding: #{panelLabelSize/8}px; 159 | appearance: none; 160 | width: #{inputWidth - panelLabelSize/8}px; 161 | box-shadow: inset 0px 1px 2px 0 #{inputInsetShadowColor}; 162 | border-radius: #{3 * @options.scaleFactor}px; 163 | position: relative; 164 | top: #{inputTopOffet}px; 165 | }""" 166 | 167 | Utils.insertCSS(inputCSS) 168 | 169 | keyIndex = 0 170 | for row of @options.specs 171 | do (row) => 172 | tipRequired = if @options.specs[row].tip != "" and @options.specs[row].tip != undefined then true else false 173 | 174 | rowBlock = new Layer 175 | name: "row" + keyIndex 176 | parent: @ 177 | x: panelSideMargin 178 | y: if keyIndex > 0 then rows[keyIndex - 1].maxY else panelTopMargin + keyIndex * panelRowHeight 179 | height: if tipRequired then panelRowHeight * 1.5 else panelRowHeight 180 | width: labelWidth 181 | backgroundColor: "clear" 182 | 183 | rows.push(rowBlock) 184 | 185 | label = new Layer 186 | name: _.camelCase(@options.specs[row].label + "Label") 187 | parent: rowBlock 188 | height: panelRowHeight 189 | width: labelWidth 190 | y: panelLabelMargin 191 | backgroundColor: "clear" 192 | html: "

#{@options.specs[row].label}

" 193 | style: 194 | "font-size": panelLabelSize + "px" 195 | "font-weight": "500" 196 | "text-align": "right" 197 | "color": @options.textColor 198 | 199 | @[label.name] = label 200 | 201 | if tipRequired 202 | tip = new Layer 203 | name: _.camelCase(@options.specs[row].label + "Tip") 204 | parent: rowBlock 205 | height: panelRowHeight * 0.4 206 | width: labelWidth 207 | y: label.maxY + panelTipMargin 208 | backgroundColor: "clear" 209 | html: "

#{@options.specs[row].tip}

" 210 | style: 211 | "font-size": panelTipSize + "px" 212 | "font-weight": "500" 213 | "text-align": "right" 214 | "color": @options.textColor 215 | 216 | @[tip.name] = tip 217 | 218 | idString = _.camelCase(@options.specs[row].label + "Input") 219 | switch typeof @options.specs[row].value 220 | when "boolean" 221 | input = new Layer 222 | name: idString 223 | parent: rowBlock 224 | x: Align.right(inputWidth) 225 | y: Align.top(radioButtonTopMargin) 226 | height: radioButtonSize 227 | width: radioButtonSize 228 | borderRadius: radioButtonSize/2 229 | borderColor: @options.textColor 230 | borderWidth: 1 * @options.scaleFactor 231 | backgroundColor: "clear" 232 | 233 | inputMark = new Layer 234 | name: "mark" 235 | parent: input 236 | width: radioButtonMarkSize 237 | height: radioButtonMarkSize 238 | x: (radioButtonSize-radioButtonMarkSize)/2 239 | y: (radioButtonSize-radioButtonMarkSize)/2 240 | borderRadius: radioButtonMarkSize/2 241 | backgroundColor: @options.textColor 242 | visible: @options.specs[row].value 243 | 244 | input.mark = inputMark 245 | input.row = row 246 | 247 | input.onClick => 248 | if @options.specs[input.row].value == true 249 | @options.specs[input.row].value = false 250 | input.mark.visible = false 251 | else 252 | @options.specs[input.row].value = true 253 | input.mark.visible = true 254 | 255 | else 256 | input = new Layer 257 | name: idString 258 | parent: rowBlock 259 | x: Align.right((@.width - labelWidth)/2) 260 | y: Align.top 261 | color: @options.textColor 262 | html: "" 263 | height: panelRowHeight 264 | width: inputWidth 265 | backgroundColor: "clear" 266 | 267 | @[input.name] = input 268 | 269 | if @options.showGuides == true 270 | guide = new Layer 271 | name: "guide" 272 | parent: rowBlock 273 | width: @.width 274 | x: -panelSideMargin 275 | backgroundColor: "red" 276 | height: 1 277 | y: panelLabelSize * 1.3 278 | opacity: 0.5 279 | 280 | ++keyIndex 281 | 282 | 283 | @.height = Math.max(minimumPanelHeight, @.contentFrame().height + panelTopMargin + panelBottomMargin + commitButtonHeight + commitButtonTopMargin) 284 | 285 | closeButton = new Layer 286 | name: "closeButton" 287 | parent: @ 288 | width: closeButtonSize 289 | height: closeButtonSize 290 | borderRadius: closeButtonSize/2 291 | backgroundColor: "rgba(0,0,0,0.15)" 292 | borderWidth: 1 * @options.scaleFactor 293 | borderColor: "rgba(255,255,255,0.5)" 294 | x: closeButtonMargin 295 | y: closeButtonMargin 296 | 297 | @.closeButton = closeButton 298 | 299 | closeButton.onClick => 300 | @.hide() 301 | @options.closeAction() 302 | 303 | closeButtonStroke1 = new Layer 304 | name: "closeButtonStroke1" 305 | parent: closeButton 306 | width: closeGlyphWidth 307 | height: closeGlyphHeight 308 | x: (closeButtonSize-closeGlyphWidth)/2 309 | y: (closeButtonSize-closeGlyphHeight)/2 310 | rotation: 45 311 | borderRadius: closeGlyphHeight/2 312 | backgroundColor: "white" 313 | 314 | closeButtonStroke2 = new Layer 315 | name: "closeButtonStroke2" 316 | parent: closeButton 317 | width: closeGlyphWidth 318 | height: closeGlyphHeight 319 | x: (closeButtonSize-closeGlyphWidth)/2 320 | y: (closeButtonSize-closeGlyphHeight)/2 321 | rotation: -45 322 | borderRadius: closeGlyphHeight/2 323 | backgroundColor: "white" 324 | 325 | commitButton = new Layer 326 | name: "commitButton" 327 | parent: @ 328 | width: @.width - panelButtonMargin * 2 329 | height: commitButtonHeight 330 | x: Align.center 331 | y: Align.bottom(- panelBottomMargin) 332 | backgroundColor: @options.buttonColor 333 | html: if Object.keys(@options.specs).length == 0 then alertString else commitString 334 | borderRadius: 5 * @options.scaleFactor 335 | 336 | @.commitButton = commitButton 337 | 338 | commitButton.onClick => 339 | for row of @options.specs 340 | do(row) => 341 | idString = _.camelCase(@options.specs[row].label + "Input") 342 | switch typeof @options.specs[row].value 343 | when "string" 344 | typedValue = document.getElementById(idString).value 345 | when "number" 346 | typedValue = +document.getElementById(idString).value 347 | when "boolean" 348 | typedValue = @options.specs[row].value 349 | else 350 | typedValue = document.getElementById(idString).value 351 | @options.specs[row].value = typedValue 352 | @options.commitAction() 353 | 354 | if @options.hidden == true 355 | @.hide() 356 | 357 | hide: () => 358 | @options.hidden = true 359 | @.animate 360 | properties: 361 | opacity: 0 362 | time: 363 | 0.25 364 | Utils.delay 0.25, => 365 | @.visible = false 366 | 367 | show: () => 368 | @.visible = true 369 | @options.hidden = false 370 | @.animate 371 | properties: 372 | opacity: 1 373 | time: 374 | 0.25 375 | 376 | @define 'specs', get: () -> @options.specs 377 | @define 'hidden', get: () -> @options.hidden 378 | module.exports = ControlPanelLayer 379 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ControlPanelLayer", 3 | "description": "Generates a developer panel to expose advanced features of your prototype.", 4 | "author": "John Marstall @ BPXL", 5 | 6 | "require": "ControlPanelLayer = require 'ControlPanelLayer'", 7 | "install": [ 8 | "ControlPanelLayer.coffee", 9 | "README.md" 10 | ], 11 | "example": "example.coffee", 12 | 13 | "thumb": "thumb.png" 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@blackpixel/framer-controlpanellayer", 3 | "version": "1.0.3", 4 | "description": "Produce a developer panel for controlling aspects of your prototype within the prototype itself.", 5 | "homepage": "https://github.com/bpxl-labs/ControlPanelLayer", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bpxl-labs/ControlPanelLayer.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/bpxl-labs/ControlPanelLayer/issues" 12 | }, 13 | "main": "ControlPanelLayer.coffee", 14 | "author": "Black Pixel", 15 | "license": "MIT", 16 | "keywords": [ 17 | "framerjs", 18 | "framer", 19 | "coffeescript", 20 | "settings" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "dependencies": { 26 | "coffeeify": "^2.1.0" 27 | }, 28 | "browserify": { 29 | "transform": [ 30 | "coffeeify" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gohypergiant/ControlPanelLayer/3ee504de1723e7e0df56e32dadbb1b2ae17791c9/thumb.png --------------------------------------------------------------------------------