├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── analytics.js ├── dist ├── fonts │ ├── MaterialIcons-Regular.eot │ ├── MaterialIcons-Regular.svg │ ├── MaterialIcons-Regular.ttf │ ├── MaterialIcons-Regular.woff │ ├── MaterialIcons-Regular.woff2 │ ├── Roboto-Bold.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-Regular.ttf │ └── Roboto-Thin.ttf ├── icons │ ├── mac │ │ └── icon.png │ ├── png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png │ └── win │ │ └── icon.png ├── img │ ├── logo.svg │ └── logo_inv.svg ├── index.html └── templates │ ├── modules │ ├── joystick.coffee │ └── joystick │ │ ├── App.coffee │ │ ├── Background.coffee │ │ ├── Broadcaster.coffee │ │ ├── FocusSystem.coffee │ │ ├── Focusable.coffee │ │ ├── Gamepad.coffee │ │ ├── Grid.coffee │ │ ├── Transitions.coffee │ │ ├── View.coffee │ │ └── stores │ │ ├── ActionStore.coffee │ │ ├── FocusStore.coffee │ │ └── ViewStore.coffee │ ├── project-coffeescript │ ├── Main.coffee │ ├── app.js │ ├── components │ │ └── CardCarousel.component.coffee │ ├── framer │ │ ├── framer.js │ │ ├── framer.js.map │ │ └── images │ │ │ ├── cursor-active.png │ │ │ ├── cursor-active@2x.png │ │ │ ├── cursor.png │ │ │ └── cursor@2x.png │ ├── generic │ │ └── .gitkeep │ ├── images │ │ ├── Icon.png │ │ └── background.png │ ├── index.html │ └── views │ │ └── Home.view.coffee │ └── project-javascript │ ├── Main.js │ ├── app.js │ ├── components │ └── CardCarousel.component.js │ ├── framer │ ├── framer.js │ ├── framer.js.map │ └── images │ │ ├── cursor-active.png │ │ ├── cursor-active@2x.png │ │ ├── cursor.png │ │ └── cursor@2x.png │ ├── generic │ └── .gitkeep │ ├── images │ ├── Icon.png │ └── background.png │ ├── index.html │ └── views │ └── Home.view.js ├── docs └── Joystick.md ├── main.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── components │ ├── Button.tsx │ ├── Editor.tsx │ ├── Icon.tsx │ ├── Popup.tsx │ ├── Sidebar.tsx │ ├── Tabs.tsx │ ├── Welcome.tsx │ ├── _Editor.scss │ ├── _Popup.scss │ ├── _Sidebar.scss │ ├── _Tabs.scss │ └── _Welcome.scss ├── index.tsx ├── misc │ ├── app-icon.png │ └── framer-theme.json ├── scss │ ├── _fonts.scss │ ├── _generic.scss │ ├── _reset.scss │ ├── _variables.scss │ └── main.scss ├── snippets.ts ├── stores │ ├── EditorStore.ts │ ├── PopupStore.ts │ ├── ProjectStore.ts │ └── index.ts └── types.ts ├── tsconfig.json ├── webpack.config.js ├── yarn-error.log └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | 4 | /dist/types/ 5 | 6 | /dist/*.js 7 | /dist/**.js 8 | 9 | /dist/*.js.map 10 | /dist/**.js.map 11 | 12 | /dist/*.json 13 | /dist/**.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode9.4 5 | language: node_js 6 | node_js: "10" 7 | env: 8 | - ELECTRON_CACHE=$HOME/.cache/electron 9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 10 | cache: 11 | directories: 12 | - node_modules 13 | - $HOME/.cache/electron 14 | - $HOME/.cache/electron-builder 15 | script: yarn release 16 | before_cache: 17 | - rm -rf $HOME/.cache/electron-builder/wine 18 | 19 | branches: 20 | except: 21 | - "/^v\\d+\\.\\d+\\.\\d+$/" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Emil Widlund 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 | # Carousel 2 | [![Build Status](https://travis-ci.org/emilwidlund/carousel.svg?branch=master)](https://travis-ci.org/emilwidlund/carousel) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/emilwidlund/carousel.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/emilwidlund/carousel/alerts/) 5 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/emilwidlund/carousel.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/emilwidlund/carousel/context:javascript) 6 | 7 | Carousel is a prototyping tool powered by [Framer Library](https://github.com/koenbok/Framer). It provides a simple way to build large-scale, Gamepad-driven prototypes on both macOS and Windows. Prototypes are built using the [Framer Library API](https://classic.framer.com/docs) and may be written in JavaScript (ES6) or CoffeeScript. 8 | 9 | New Carousel-projects comes pre-loaded with [Framer Joystick](https://blog.framer.com/introducing-framer-joystick-28359287bef0) and simple boilerplate code which makes it very easy to get up and running without setting things up. 10 | 11 | [Here's a quick demo](https://www.youtube.com/watch?v=5UR9pkTTJvY&feature=youtu.be) 12 | 13 | ### Download 14 | You can find download-links for the latest release [here](https://github.com/emilwidlund/carousel/releases/latest). If you'd like to see all releases, you can find them [here](https://github.com/emilwidlund/carousel/releases). 15 | Download the .dmg file for macOS or the .exe file for Windows. 16 | 17 | #### Important Notice 18 | Code Signing Certificates has not yet been integrated with the builds for neither Windows or macOS. This means that both Windows and macOS (and probably a couple of anti-virus programs) will flag the Carousel installer as untrusted. Carousel will however run fine if you proceed despite the warnings. 19 | 20 | This is of course on our radar, and we'll hopefully have certificates integrated with builds on both platforms soon. 21 | 22 | ### Documentation 23 | - [Joystick Documentation](https://github.com/emilwidlund/carousel/blob/master/docs/Joystick.md) 24 | - Joystick Guides - Coming Soon 25 | 26 | ### Issues & Contributing 27 | If you find an issue or a bug, please let us know by [submitting an issue](https://github.com/emilwidlund/carousel/issues). You could also [open a pull request](https://github.com/emilwidlund/carousel/pulls). If you have ideas of improvement and feature-requests, open an issue and add the "enhancement"-label. 28 | 29 | ### Contact 30 | For any questions, highfives or concerns - contact me on [Twitter](https://twitter.com/emilwidlund). 31 | -------------------------------------------------------------------------------- /analytics.js: -------------------------------------------------------------------------------- 1 | const ua = require('universal-analytics'); 2 | const uuid = require('uuid/v4'); 3 | const Store = require('electron-store'); 4 | const store = new Store(); 5 | 6 | // Setup User Analytics 7 | const userId = store.get('userId') || uuid(); 8 | store.set('userId', userId); 9 | 10 | const user = ua('UA-75056533-4', userId); 11 | 12 | 13 | 14 | const trackEvent = (category, action, label, value) => { 15 | user.event({ 16 | ec: category, 17 | ea: action, 18 | el: label, 19 | ev: value 20 | }) 21 | .send(); 22 | } 23 | 24 | 25 | module.exports = { 26 | trackEvent 27 | } -------------------------------------------------------------------------------- /dist/fonts/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /dist/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /dist/fonts/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /dist/fonts/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /dist/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /dist/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /dist/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /dist/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /dist/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /dist/icons/mac/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/mac/icon.png -------------------------------------------------------------------------------- /dist/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/128x128.png -------------------------------------------------------------------------------- /dist/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/16x16.png -------------------------------------------------------------------------------- /dist/icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/24x24.png -------------------------------------------------------------------------------- /dist/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/256x256.png -------------------------------------------------------------------------------- /dist/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/32x32.png -------------------------------------------------------------------------------- /dist/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/48x48.png -------------------------------------------------------------------------------- /dist/icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/512x512.png -------------------------------------------------------------------------------- /dist/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/64x64.png -------------------------------------------------------------------------------- /dist/icons/png/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/png/96x96.png -------------------------------------------------------------------------------- /dist/icons/win/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/icons/win/icon.png -------------------------------------------------------------------------------- /dist/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /dist/img/logo_inv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Carousel 4 | 5 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /dist/templates/modules/joystick.coffee: -------------------------------------------------------------------------------- 1 | {App} = require './joystick/App.coffee' 2 | {Focusable} = require './joystick/Focusable.coffee' 3 | {GamepadSystem} = require './joystick/Gamepad.coffee' 4 | {Transitions} = require './joystick/Transitions.coffee' 5 | {View} = require './joystick/View.coffee' 6 | {Grid} = require './joystick/Grid.coffee' 7 | 8 | {focusStore} = require './joystick/stores/FocusStore.coffee' 9 | {viewStore} = require './joystick/stores/ViewStore.coffee' 10 | {actionStore} = require './joystick/stores/ActionStore.coffee' 11 | 12 | joystick = 13 | App: App 14 | Focusable: Focusable 15 | Gamepad: new GamepadSystem(false) 16 | Transitions: Transitions 17 | View: View 18 | Grid: Grid 19 | FocusStore: focusStore 20 | ViewStore: viewStore 21 | ActionStore: actionStore 22 | 23 | module.exports = joystick -------------------------------------------------------------------------------- /dist/templates/modules/joystick/App.coffee: -------------------------------------------------------------------------------- 1 | {FocusSystem} = require './FocusSystem.coffee' 2 | {Focusable} = require './Focusable.coffee' 3 | {Background} = require './Background.coffee' 4 | {viewStore} = require './stores/ViewStore.coffee' 5 | {actionStore} = require './stores/ActionStore.coffee' 6 | _ = Framer._ 7 | 8 | Utils.isFramerStudio = () -> true 9 | 10 | Device = new DeviceComponent 11 | hideBezel: true 12 | 13 | Device.setupContext() 14 | 15 | Device.customize 16 | screenWidth: 1920 17 | screenHeight: 1080 18 | devicePixelRatio: 1 19 | deviceZoom: 1 20 | 21 | Device.screenBackground.visible = false 22 | 23 | # Disable Hints 24 | Framer.Extras.Hints.disable() 25 | 26 | class exports.App extends FlowComponent 27 | 28 | constructor: (properties={}) -> 29 | background = new Background 30 | 31 | super _.defaults properties, 32 | backgroundColor: 'transparent' 33 | 34 | @focusSystem = new FocusSystem() 35 | @background = background 36 | @setupTransitionListener() 37 | 38 | # Manipulate FlowComponent's ScrollComponent content insets 39 | @on 'transitionstart', (previousView, nextView, direction) => 40 | if @_wrappedLayer 41 | sc = @_wrappedLayer nextView 42 | else 43 | sc = @_tempScroll 44 | sc.contentInset = 0 45 | 46 | @on 'transitionend', (previousView, nextView, direction) => 47 | if @_wrappedLayer 48 | sc = @_wrappedLayer nextView 49 | else 50 | sc = @_tempScroll 51 | sc.draggable.enabled = false 52 | 53 | viewStore.transition properties.view 54 | 55 | setupTransitionListener: -> 56 | viewStore.on 'transitionEvent', (transitionEvent) => 57 | if transitionEvent.viewTransition 58 | @transition transitionEvent.view, transitionEvent.viewTransition 59 | else 60 | @showNext transitionEvent.view -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Background.coffee: -------------------------------------------------------------------------------- 1 | {viewStore} = require './stores/ViewStore.coffee' 2 | _ = Framer._ 3 | 4 | class exports.Background extends Layer 5 | constructor: (properties={}) -> 6 | super properties 7 | 8 | @bgLayer = new Layer 9 | parent: @ 10 | size: Screen.size 11 | backgroundColor: '#000' 12 | 13 | @gradientLayer = {} 14 | 15 | viewStore.on 'transitionEvent', (transitionEvent) => 16 | if (transitionEvent.view.background) 17 | @populateBackground(transitionEvent.view.background) 18 | 19 | populateBackground: (background) -> 20 | 21 | tempVar = new Layer 22 | parent: @ 23 | size: Screen.size 24 | image: background.image || null 25 | backgroundColor: background.backgroundColor || '#000' 26 | blur: background.blur || null 27 | opacity: 0 28 | 29 | tempVar.animate 30 | opacity: 1 31 | options: 32 | time: .4 33 | 34 | Utils.delay .4, => 35 | @bgLayer.destroy() 36 | @bgLayer = tempVar -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Broadcaster.coffee: -------------------------------------------------------------------------------- 1 | _ = Framer._ 2 | 3 | class BroadcasterSystem extends Framer.EventEmitter 4 | viewTransitionEvent: (view) -> 5 | @emit 'viewTransitionEvent', view 6 | 7 | focusEvent: (focusable) -> 8 | @emit 'focusEvent', focusable 9 | 10 | exports.Broadcaster = new BroadcasterSystem() -------------------------------------------------------------------------------- /dist/templates/modules/joystick/FocusSystem.coffee: -------------------------------------------------------------------------------- 1 | {GamepadSystem} = require './Gamepad.coffee' 2 | {focusStore} = require './stores/FocusStore.coffee' 3 | _ = Framer._ 4 | 5 | exports.Gamepad = Gamepad = new GamepadSystem(true) 6 | 7 | class exports.FocusSystem 8 | constructor: () -> 9 | 10 | # Setup event listeners for navigation 11 | Gamepad.on 'gamepadevent', (event) => 12 | if event.keyCode > 36 && event.keyCode < 41 && focusStore.focusableElements.length 13 | @navigate event.keyCode 14 | else if event.keyCode > 11 && event.keyCode < 16 && focusStore.focusableElements.length 15 | @navigate event.keyCode 16 | 17 | window.addEventListener 'keydown', (event) => 18 | if event.keyCode > 36 && event.keyCode < 41 && focusStore.focusableElements.length 19 | @navigate(event.keyCode) 20 | 21 | navigate: (keyCode) -> 22 | 23 | focusedPosition = 24 | x: focusStore.focusedElement.screenFrame.x + (focusStore.focusedElement.screenFrame.width / 2), 25 | y: focusStore.focusedElement.screenFrame.y + (focusStore.focusedElement.screenFrame.height / 2) 26 | 27 | relevantFocusables = _.filter focusStore.focusableElements, (focusable) -> 28 | 29 | if focusable.parent.parent.visible == false || focusable.parent.parent.opacity == 0 30 | return false 31 | if focusable.visible == false || focusable.opacity == 0 32 | return false 33 | 34 | switch keyCode 35 | when 13 36 | focusablePosition = 37 | x: focusable.screenFrame.x + focusable.screenFrame.width 38 | y: focusable.screenFrame.y + (focusable.screenFrame.height / 2) 39 | 40 | if focusablePosition.x < focusedPosition.x 41 | return true 42 | else 43 | return false 44 | 45 | when 11 46 | focusablePosition = 47 | x: focusable.screenFrame.x + (focusable.screenFrame.width / 2) 48 | y: focusable.screenFrame.y + focusable.screenFrame.height 49 | 50 | if focusablePosition.y < focusedPosition.y 51 | return true 52 | else 53 | return false 54 | 55 | when 14 56 | focusablePosition = 57 | x: focusable.screenFrame.x 58 | y: focusable.screenFrame.y + (focusable.screenFrame.height / 2) 59 | 60 | if focusablePosition.x > focusedPosition.x 61 | return true 62 | else 63 | return false 64 | 65 | when 12 66 | focusablePosition = 67 | x: focusable.screenFrame.x + (focusable.screenFrame.width / 2) 68 | y: focusable.screenFrame.y 69 | 70 | if focusablePosition.y > focusedPosition.y 71 | return true 72 | else 73 | return false 74 | 75 | when 37 76 | focusablePosition = 77 | x: focusable.screenFrame.x + focusable.screenFrame.width 78 | y: focusable.screenFrame.y + (focusable.screenFrame.height / 2) 79 | 80 | if focusablePosition.x < focusedPosition.x 81 | return true 82 | else 83 | return false 84 | 85 | when 38 86 | focusablePosition = 87 | x: focusable.screenFrame.x + (focusable.screenFrame.width / 2) 88 | y: focusable.screenFrame.y + focusable.screenFrame.height 89 | 90 | if focusablePosition.y < focusedPosition.y 91 | return true 92 | else 93 | return false 94 | 95 | when 39 96 | focusablePosition = 97 | x: focusable.screenFrame.x, 98 | y: focusable.screenFrame.y + (focusable.screenFrame.height / 2) 99 | 100 | if focusablePosition.x > focusedPosition.x 101 | return true 102 | else 103 | return false 104 | 105 | when 40 106 | focusablePosition = 107 | x: focusable.screenFrame.x + (focusable.screenFrame.width / 2) 108 | y: focusable.screenFrame.y 109 | 110 | if focusablePosition.y > focusedPosition.y 111 | return true 112 | else 113 | return false 114 | 115 | 116 | sortedFocusables = _.sortBy relevantFocusables, (focusable) -> 117 | 118 | switch keyCode 119 | when 14 120 | angleOffset = 90 121 | break 122 | 123 | when 12 124 | angleOffset = 0 125 | break 126 | 127 | when 15 128 | angleOffset = 90 129 | break 130 | 131 | when 13 132 | angleOffset = 180 133 | break 134 | 135 | when 37 136 | angleOffset = 90 137 | break 138 | 139 | when 38 140 | angleOffset = 0 141 | break 142 | 143 | when 39 144 | angleOffset = 90 145 | break 146 | 147 | when 40 148 | angleOffset = 180 149 | break 150 | 151 | 152 | focusablePosition = 153 | x: focusable.screenFrame.x + (focusable.screenFrame.width / 2) 154 | y: focusable.screenFrame.y + (focusable.screenFrame.height / 2) 155 | 156 | Y = Math.abs(focusedPosition.y - focusablePosition.y) 157 | X = Math.abs(focusedPosition.x - focusablePosition.x) 158 | 159 | distance = Math.sqrt(Math.abs(X*X + Y*Y)) 160 | angle = Math.abs(Math.atan2(focusedPosition.x - focusablePosition.x, focusedPosition.y - focusablePosition.y) * 180 / Math.PI) 161 | score = distance + Math.abs(angleOffset - angle) 162 | return score 163 | 164 | if sortedFocusables.length 165 | focusStore.focus sortedFocusables[0] -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Focusable.coffee: -------------------------------------------------------------------------------- 1 | _ = Framer._ 2 | 3 | class exports.Focusable extends Layer 4 | constructor: (properties={}) -> 5 | super properties 6 | 7 | @meta = properties.meta 8 | @actions = properties.actions || [] 9 | 10 | @states.default.animationOptions = properties.animationOptions 11 | @states.focused = properties.focusProperties -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Gamepad.coffee: -------------------------------------------------------------------------------- 1 | _ = Framer._ 2 | 3 | Function::define = (prop, desc) -> 4 | Object.defineProperty this.prototype, prop, desc 5 | 6 | class exports.GamepadSystem extends Framer.EventEmitter 7 | constructor: (throttle) -> 8 | 9 | super() 10 | 11 | @connectedGamepad = undefined 12 | @loopRequest = undefined 13 | @pollingGP = undefined 14 | @buttonsPressed = [] 15 | 16 | # Poll loop X times a second for new states 17 | @loopInterval = 500 18 | 19 | # Threshold for approved axis values - Values above X will be registered as an input 20 | @axisSensitivity = if throttle then .7 else .2 21 | 22 | # Amount of button events occuring in a sequence 23 | @eventsInSequence = 0 24 | 25 | # Should events be throttled? 26 | @throttled = throttle 27 | 28 | if navigator.getGamepads()[0] 29 | @connectedGamepad = navigator.getGamepads()[0] 30 | @loopRequest = window.requestAnimationFrame @eventLoop.bind(@) 31 | 32 | window.addEventListener 'gamepadconnected', (e) => 33 | @connectedGamepad = navigator.getGamepads()[0] 34 | @loopRequest = window.requestAnimationFrame @eventLoop.bind(@) 35 | 36 | window.addEventListener 'gamepaddisconnected', (e) => 37 | @connectedGamepad = null 38 | window.cancelAnimationFrame @loopRequest 39 | 40 | @define 'throttle', 41 | set: (bool) -> 42 | @throttled = bool 43 | 44 | if bool == true 45 | @axisSensitivity = .7 46 | else 47 | @axisSensitivity = .2 48 | 49 | eventLoop: () => 50 | 51 | setTimeout () => 52 | 53 | @pollingGP = navigator.getGamepads()[0] 54 | 55 | for button, index in @pollingGP.buttons 56 | button.type = 'button' 57 | button.keyCode = index 58 | 59 | @buttonsPressed = _.filter @pollingGP.buttons, {pressed: true} 60 | 61 | for axis, index in @pollingGP.axes 62 | 63 | if (index <= 3) 64 | activeAxis = {} 65 | 66 | if axis > @axisSensitivity || axis < -@axisSensitivity 67 | activeAxis.type = 'axis' 68 | activeAxis.value = axis 69 | 70 | switch index 71 | when 0 72 | if axis > 0 73 | activeAxis.keyCode = 39 74 | @buttonsPressed.push activeAxis 75 | else 76 | activeAxis.keyCode = 37 77 | @buttonsPressed.push activeAxis 78 | when 1 79 | if axis > 0 80 | activeAxis.keyCode = 40 81 | @buttonsPressed.push activeAxis 82 | else 83 | activeAxis.keyCode = 38 84 | @buttonsPressed.push activeAxis 85 | when 2 86 | if axis > 0 87 | activeAxis.keyCode = 44 88 | @buttonsPressed.push activeAxis 89 | else 90 | activeAxis.keyCode = 42 91 | @buttonsPressed.push activeAxis 92 | when 3 93 | if axis > 0 94 | activeAxis.keyCode = 43 95 | @buttonsPressed.push activeAxis 96 | else 97 | activeAxis.keyCode = 41 98 | @buttonsPressed.push activeAxis 99 | 100 | if @buttonsPressed.length 101 | for buttonPressed, index in @buttonsPressed 102 | @emit 'gamepadevent', buttonPressed 103 | 104 | if @throttled 105 | switch @eventsInSequence 106 | when 0 107 | @loopInterval = 3 108 | when 1 109 | @loopInterval = 8 110 | else 111 | @loopInterval = 1000 112 | 113 | @eventsInSequence++ 114 | else 115 | if @throttled 116 | @eventsInSequence = 0 117 | @loopInterval = 500 118 | else 119 | @eventsInSequence = 0 120 | @loopInterval = 1000 121 | 122 | @loopRequest = window.requestAnimationFrame @eventLoop.bind(@) 123 | 124 | , 1000 / @loopInterval -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Grid.coffee: -------------------------------------------------------------------------------- 1 | _ = Framer._ 2 | 3 | class GridSystem 4 | constructor: -> 5 | @safezoneBounds = 6 | width: Screen.width * .9 7 | height: Screen.height * .9 8 | 9 | @dangerzoneBounds = 10 | width: (Screen.width * .1) / 2 11 | height: (Screen.height * .1) / 2 12 | 13 | @columnCount = 24 14 | @columnGutterCount = @columnCount - 1 15 | @columnGutter = 10 16 | @columnWidth = (@safezoneBounds.width - (@columnGutterCount * @columnGutter)) / @columnCount 17 | 18 | @rowHeight = 10 19 | 20 | getWidth: (cells) -> 21 | return (@columnWidth * cells) + (@columnGutter * (cells - 1)) 22 | 23 | getHeight: (rows) -> 24 | return @rowHeight * rows 25 | 26 | getSafezone: (parent) -> 27 | return new Layer 28 | parent: parent 29 | name: 'safezone' 30 | width: @safezoneBounds.width 31 | height: @safezoneBounds.height 32 | y: Align.center 33 | x: Align.center 34 | backgroundColor: 'transparent' 35 | 36 | exports.Grid = new GridSystem() -------------------------------------------------------------------------------- /dist/templates/modules/joystick/Transitions.coffee: -------------------------------------------------------------------------------- 1 | exports.Transitions = 2 | goIn: -> 3 | options = 4 | curve: 'cubic-bezier(0.645, 0.045, 0.355, 1)' 5 | time: .4 6 | 7 | return ( 8 | layerA: 9 | show: 10 | opacity: 1 11 | scale: 1 12 | options: options 13 | hide: 14 | opacity: 0 15 | scale: 1.2 16 | options: options 17 | layerB: 18 | show: 19 | opacity: 1 20 | scale: 1 21 | options: options 22 | hide: 23 | opacity: 0 24 | scale: .8 25 | options: options 26 | options: options 27 | ) 28 | 29 | goOut: -> 30 | options = 31 | curve: 'cubic-bezier(0.645, 0.045, 0.355, 1)' 32 | time: .4 33 | 34 | return ( 35 | layerA: 36 | show: 37 | opacity: 1 38 | scale: 1 39 | options: options 40 | hide: 41 | opacity: 0 42 | scale: .8 43 | options: options 44 | layerB: 45 | show: 46 | opacity: 1 47 | scale: 1 48 | options: options 49 | hide: 50 | opacity: 0 51 | scale: 1.2 52 | options: options 53 | options: options 54 | ) 55 | 56 | fade: -> 57 | options = 58 | curve: 'cubic-bezier(0.645, 0.045, 0.355, 1)' 59 | time: .4 60 | 61 | return ( 62 | layerA: 63 | show: 64 | opacity: 1 65 | options: options 66 | hide: 67 | opacity: 0 68 | options: options 69 | layerB: 70 | show: 71 | opacity: 1 72 | options: options 73 | hide: 74 | opacity: 0 75 | options: options 76 | options: options 77 | ) -------------------------------------------------------------------------------- /dist/templates/modules/joystick/View.coffee: -------------------------------------------------------------------------------- 1 | {Grid} = require './Grid.coffee' 2 | _ = Framer._ 3 | 4 | class exports.View extends Layer 5 | constructor: (properties={}) -> 6 | 7 | properties.backgroundColor = 'transparent' 8 | 9 | super _.defaults properties, 10 | width: Screen.width 11 | height: Screen.height 12 | 13 | @safezone = Grid.getSafezone(@) 14 | 15 | @actions = properties.actions || [] 16 | @background = properties.background || {} -------------------------------------------------------------------------------- /dist/templates/modules/joystick/stores/ActionStore.coffee: -------------------------------------------------------------------------------- 1 | {Gamepad} = require '../FocusSystem.coffee'; 2 | {viewStore} = require './ViewStore.coffee' 3 | {focusStore} = require './FocusStore.coffee' 4 | _ = Framer._ 5 | 6 | Function::define = (prop, desc) -> 7 | Object.defineProperty this.prototype, prop, desc 8 | 9 | class ActionStore extends Framer.EventEmitter 10 | constructor: -> 11 | super() 12 | 13 | @focusableActions = [] 14 | @viewActions = [] 15 | 16 | window.addEventListener 'keydown', (event) => 17 | if event.keyCode == 13 18 | action = _.find @actions, {keyCode: 0} 19 | if action 20 | action.function() 21 | else if event.keyCode == 8 22 | action = _.find @actions, {keyCode: 1} 23 | if action 24 | action.function() 25 | 26 | Gamepad.on 'gamepadevent', (event) => 27 | action = _.find @actions, {keyCode: event.keyCode} 28 | if action 29 | action.function() 30 | 31 | viewStore.on 'transitionEvent', (transitionEvent) => 32 | @populateViewActions(transitionEvent.view.actions) 33 | 34 | focusStore.on 'focusEvent', (focusEvent) => 35 | if focusEvent.focusedElement 36 | @populateFocusableActions(focusEvent.focusedElement.actions) 37 | 38 | focusStore.on 'focusedElementCleared', => 39 | @clearFocusableActions() 40 | 41 | @define 'actions', 42 | get: -> @focusableActions.concat @viewActions 43 | 44 | populateFocusableActions: (focusableActions) -> 45 | @focusableActions = focusableActions 46 | @emit 'focusableActionsPopulated', focusableActions 47 | 48 | clearFocusableActions: (viewActions) -> 49 | @focusableActions = [] 50 | @emit 'focusableActionsCleared' 51 | 52 | populateViewActions: (viewActions) -> 53 | @viewActions = viewActions 54 | @emit 'viewActionsPopulated', viewActions 55 | 56 | exports.actionStore = new ActionStore -------------------------------------------------------------------------------- /dist/templates/modules/joystick/stores/FocusStore.coffee: -------------------------------------------------------------------------------- 1 | {Gamepad} = require '../Gamepad.coffee' 2 | 3 | class FocusStore extends Framer.EventEmitter 4 | constructor: -> 5 | super() 6 | 7 | @focusableElements = [] 8 | @focusedElement = null 9 | @previouslyFocusedElement = null 10 | 11 | focus: (focusable) -> 12 | 13 | # If an element is focused, set it as previouslyFocused and change state to default 14 | # Loop through descendant elements and update their states as well 15 | 16 | if @focusedElement 17 | @previouslyFocusedElement = @focusedElement 18 | @previouslyFocusedElement.animate 'default' 19 | 20 | @previouslyFocusedElement.descendants.map (desc, index) -> 21 | if desc.states.focused 22 | desc.animate 'default' 23 | 24 | # Focus focusable and change state to focused 25 | # Loop through descendant elements and update their states as well 26 | 27 | @focusedElement = focusable 28 | @focusedElement.animate 'focused' 29 | 30 | for desc, index in @focusedElement.descendants 31 | if desc.states.focused 32 | desc.animate 'focused' 33 | 34 | @emit 'focusEvent', 35 | previouslyFocusedElement: @previouslyFocusedElement 36 | focusedElement: @focusedElement 37 | 38 | addFocusable: (focusable) -> 39 | @focusableElements.push focusable 40 | 41 | clearFocusables: -> 42 | @focusableElements = [] 43 | @emit 'focusableElementsCleared' 44 | 45 | clearFocus: -> 46 | if @focusedElement 47 | @previouslyFocusedElement = @focusedElement 48 | @previouslyFocusedElement.animate 'default' 49 | 50 | @previouslyFocusedElement.descendants.map (desc, index) -> 51 | if desc.states.focused 52 | desc.animate 'default' 53 | 54 | @focusedElement = null 55 | @emit 'focusedElementCleared' 56 | 57 | exports.focusStore = new FocusStore -------------------------------------------------------------------------------- /dist/templates/modules/joystick/stores/ViewStore.coffee: -------------------------------------------------------------------------------- 1 | {focusStore} = require './FocusStore.coffee' 2 | {Focusable} = require '../Focusable.coffee' 3 | {Transitions} = require '../Transitions.coffee' 4 | 5 | class ViewStore extends Framer.EventEmitter 6 | constructor: -> 7 | super() 8 | 9 | @currentView = null 10 | @previousView = null 11 | @historyStack = [] 12 | @viewTransition = null 13 | 14 | transition: (view, transition) -> 15 | @previousView = @currentView 16 | @currentView = view 17 | @viewTransition = transition 18 | 19 | focusStore.clearFocusables() 20 | 21 | # Handle the new view 22 | 23 | viewObj = 24 | view: @currentView 25 | transition: @viewTransition 26 | 27 | @historyStack.push viewObj 28 | 29 | view.descendants.map (child, index) -> 30 | if child instanceof Focusable 31 | focusStore.addFocusable child 32 | 33 | if focusStore.focusableElements.length 34 | focusStore.focus focusStore.focusableElements[0] 35 | else 36 | focusStore.clearFocus() 37 | 38 | @emit 'transitionEvent', 39 | view: view 40 | previousView: @previousView 41 | viewTransition: transition 42 | 43 | goBack: -> 44 | @previousView = @currentView 45 | 46 | if @historyStack[@historyStack.length - 1].transition == Transitions.goIn 47 | @viewTransition = Transitions.goOut 48 | else if @historyStack[@historyStack.length - 1].transition == Transitions.goOut 49 | @viewTransition = Transitions.goIn 50 | else if @historyStack[@historyStack.length - 1].transition == Transitions.fade 51 | @viewTransition = Transitions.fade 52 | 53 | @historyStack.pop() 54 | @currentView = @historyStack[@historyStack.length - 1].view 55 | 56 | focusStore.clearFocusables() 57 | 58 | # Handle the new view 59 | 60 | @currentView.descendants.map (child, index) -> 61 | if child instanceof Focusable 62 | focusStore.addFocusable child 63 | 64 | if focusStore.focusableElements.length 65 | focusStore.focus focusStore.focusableElements[0] 66 | else 67 | focusStore.clearFocus() 68 | 69 | @emit 'transitionEvent', 70 | view: if @historyStack.length then @historyStack[@historyStack.length - 1].view else undefined 71 | previousView: @previousView 72 | viewTransition: @viewTransition 73 | 74 | exports.viewStore = new ViewStore -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/Main.coffee: -------------------------------------------------------------------------------- 1 | {HomeView} = require './views/Home.view' 2 | 3 | app = new App 4 | view: HomeView -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/app.js: -------------------------------------------------------------------------------- 1 | const joystick = require('./modules/joystick'); 2 | 3 | Object.keys(joystick).forEach((k, i) => { 4 | window[k] = joystick[k]; 5 | }); 6 | 7 | require('./Main'); -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/components/CardCarousel.component.coffee: -------------------------------------------------------------------------------- 1 | class exports.CardCarousel extends ScrollComponent 2 | constructor: (props={}) -> 3 | super(props) 4 | 5 | @content.clip = props.clip 6 | @content.draggable.enabled = false 7 | 8 | FocusStore.on 'focusEvent', (focusEvent) => 9 | if focusEvent.focusedElement && focusEvent.focusedElement.parent == @content 10 | @scrollToCard focusEvent.focusedElement 11 | 12 | scrollToCard: (focusable) -> 13 | if (focusable.screenFrame.x + focusable.screenFrame.width) > (@screenFrame.x + @screenFrame.width) 14 | @scrollToLayer(focusable, 1, 0, true) 15 | else if focusable.screenFrame.x < @screenFrame.x 16 | @scrollToLayer(focusable, 0, 0, true) -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/framer/images/cursor-active.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/framer/images/cursor.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/generic/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/generic/.gitkeep -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/images/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/images/Icon.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-coffeescript/images/background.png -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /dist/templates/project-coffeescript/views/Home.view.coffee: -------------------------------------------------------------------------------- 1 | {CardCarousel} = require '../components/CardCarousel.component' 2 | 3 | view = new View 4 | size: Screen.size 5 | background: 6 | backgroundColor: '#000' 7 | 8 | cc = new CardCarousel 9 | parent: view 10 | width: Grid.getWidth(23) 11 | height: Grid.getHeight(60) 12 | y: Align.center() 13 | x: Align.center() 14 | clip: false 15 | 16 | for i in [0..10] 17 | l = new Focusable 18 | parent: cc.content 19 | width: 400 20 | height: 600 21 | x: (400 + 20) * i 22 | backgroundColor: 'rgba(255, 255, 255, .1)' 23 | focusProperties: 24 | backgroundColor: '#fff' 25 | scale: 1.1 26 | options: 27 | time: .2 28 | animationOptions: 29 | time: .2 30 | 31 | 32 | 33 | exports.HomeView = view -------------------------------------------------------------------------------- /dist/templates/project-javascript/Main.js: -------------------------------------------------------------------------------- 1 | import HomeView from './views/Home.view'; 2 | 3 | const app = new App({ 4 | view: HomeView 5 | }); -------------------------------------------------------------------------------- /dist/templates/project-javascript/app.js: -------------------------------------------------------------------------------- 1 | const joystick = require('./modules/joystick'); 2 | 3 | Object.keys(joystick).forEach((k, i) => { 4 | window[k] = joystick[k]; 5 | }); 6 | 7 | require('./Main'); -------------------------------------------------------------------------------- /dist/templates/project-javascript/components/CardCarousel.component.js: -------------------------------------------------------------------------------- 1 | export default class CardCarousel extends ScrollComponent { 2 | constructor(props={}) { 3 | super(props); 4 | 5 | this.content.clip = props.clip; 6 | this.content.draggable.enabled = false; 7 | 8 | FocusStore.on('focusEvent', (focusEvent) => { 9 | if (focusEvent.focusedElement && focusEvent.focusedElement.parent == this.content) { 10 | this.scrollToCard(focusEvent.focusedElement); 11 | } 12 | }); 13 | } 14 | 15 | scrollToCard(focusable) { 16 | if ((focusable.screenFrame.x + focusable.screenFrame.width) > (this.screenFrame.x + this.screenFrame.width)) { 17 | this.scrollToLayer(focusable, 1, 0, true); 18 | } 19 | else if (focusable.screenFrame.x < this.screenFrame.x) { 20 | this.scrollToLayer(focusable, 0, 0, true); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /dist/templates/project-javascript/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/framer/images/cursor-active.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/framer/images/cursor.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/generic/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/generic/.gitkeep -------------------------------------------------------------------------------- /dist/templates/project-javascript/images/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/images/Icon.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/dist/templates/project-javascript/images/background.png -------------------------------------------------------------------------------- /dist/templates/project-javascript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /dist/templates/project-javascript/views/Home.view.js: -------------------------------------------------------------------------------- 1 | import CardCarousel from '../components/CardCarousel.component'; 2 | 3 | const view = new View({ 4 | size: Screen.size, 5 | background: { 6 | backgroundColor: '#000' 7 | } 8 | }); 9 | 10 | const cc = new CardCarousel({ 11 | parent: view, 12 | width: Grid.getWidth(23), 13 | height: Grid.getHeight(60), 14 | y: Align.center(), 15 | x: Align.center(), 16 | clip: false 17 | }); 18 | 19 | Array(10).fill(0).forEach((x, i) => { 20 | const l = new Focusable({ 21 | parent: cc.content, 22 | width: 400, 23 | height: 600, 24 | x: (400 + 20) * i, 25 | backgroundColor: 'rgba(255, 255, 255, .1)', 26 | focusProperties: { 27 | backgroundColor: '#fff', 28 | scale: 1.1, 29 | options: { 30 | time: .2 31 | } 32 | }, 33 | animationOptions: { 34 | time: .2 35 | } 36 | }); 37 | }); 38 | 39 | export default view; 40 | 41 | -------------------------------------------------------------------------------- /docs/Joystick.md: -------------------------------------------------------------------------------- 1 | # Joystick Documentation 2 | 3 | #### Table of Contents 4 | - [App](#app) 5 | - [View](#view) 6 | - [Focusable](#focusable) 7 | - [Grid](#grid) 8 | - [Gamepad](#gamepad) 9 | - [FocusStore](#focusstore) 10 | - [ViewStore](#viewstore) 11 | 12 | ## App 13 | An app-instance is the brain of the prototype. It handles view-transitions, manages focusables and takes care of all views. 14 | The App class extends the FlowComponent class, which means that all applicable properties & methods for FlowComponents are supported on App-instances. 15 | 16 | ``` 17 | app = new App 18 | view: myView 19 | ``` 20 | 21 | ### Properties 22 | All regular FlowComponent-properties are applicable on an App. 23 | - view: `View` - The initial view to show. 24 | 25 | 26 | ## View 27 | A View is exactly what you think it is. It is a layer that covers the screen and holds content. 28 | The View class extends the Layer class, which means that all applicable properties & methods for Layers are supported on View-instances. 29 | 30 | ``` 31 | myView = new View 32 | background: 33 | backgroundColor: '#f66' 34 | actions: [ 35 | { 36 | keyCode: 41 37 | function: () -> 38 | print 'Something happens when moving the right stick up on my Gamepad, on this view' 39 | } 40 | ] 41 | 42 | l = new Layer 43 | parent: myView.safezone 44 | backgroundColor: '#fff' 45 | ``` 46 | 47 | ### Properties 48 | All regular Layer-properties are applicable on Views. 49 | - safezone: `Layer` - Returns the safezone-layer that covers 90% of the view. Safezones are widely used in TV-applications to prevent clipping of content. You should usually add children to this property. For example: `parent: myView.safezone`. 50 | - background?: `object` - An object containing some properties on what the background should be. Important to use this instead of setting the backgroundColor/image on the View-instance. 51 | - image?: `string` - Path to image to use as background. 52 | - backgroundColor?: `string` - Color to use as background. 53 | - blur?: `number` - Useful if you want to blur the specified image. 54 | - actions?: `object[]` - An array of objects defining what should happen when a key-event occurs. Used to trigger functions when a pressing a button on a Gamepad or a keyboard. 55 | - keyCode: `number` - The key-code that should trigger the action. Can be a Keyboard-keycode or Gamepad-keycode. 56 | - function: `function` - The function to execute when desired keycode is pressed. 57 | 58 | 59 | ## Focusable 60 | A Focusable is a navigatable entity. It has a default state and a focused state defined, which are cycled between depending on if it's focused or not. 61 | Focusables are navigatable with keyboard-arrows and gamepads. 62 | The Focusable class extends the Layer class, which means that all applicable properties & methods for Layers are supported on Focusable-instances. 63 | 64 | ``` 65 | foc = new Focusable 66 | parent: myView.safezone 67 | width: Grid.getWidth(4) 68 | height: Grid.getHeight(45) 69 | backgroundColor: 'rgba(255, 255, 255, .5)' 70 | focusProperties: 71 | scale: 1.1 72 | backgroundColor: '#fff' 73 | animationOptions: 74 | time: .2 75 | animationOptions: 76 | time: .2 77 | actions: [ 78 | { 79 | keyCode: 41 80 | function: () -> 81 | print 'Something happens when moving the right stick up on my Gamepad, while focusing this focusable' 82 | } 83 | ] 84 | ``` 85 | 86 | ### Properties 87 | All regular Layer-properties are applicable on Focusables. 88 | - focusProperties: `object` - An object containing properties to use when the Focusable is focused. This is pretty much just a state. This can hold animationOptions if you want to define the animation going from the default state to the focused state. 89 | - animationOptions?: `object` - An object that defines the animation options when going from the focused state to the default state. 90 | - actions?: `object[]` - An array of objects defining what should happen when a key-event occurs. Used to trigger functions when a pressing a button on a Gamepad or a keyboard. 91 | - keyCode: `number` - The key-code that should trigger the action. Can be a Keyboard-keycode or Gamepad-keycode. 92 | - function: `function` - The function to execute when desired keycode is pressed. 93 | 94 | ### Tips 95 | - If a focusable has opacity set to 0 or has visible set to false, that Focusable won't be navigatable. This means that it is handy to set one of these properties if you need to temporarily disable the navigation to a Focusable. 96 | 97 | 98 | ## Grid 99 | The Grid is used to achieve coherent and perfectly aligned prototypes. It primarly utilizes two methods that should be used whenever you want to define sizes & margins. 100 | 101 | The Grid contain 24 columns, 23 gutters and infinite vertical rows. Gutters & Rows are always 10px. Columns have a variable width, depending on how big the Screen is. When you want to define a size for an element, you can simply do like this: 102 | 103 | ``` 104 | new Layer 105 | width: Grid.getWidth(4) # Returns the width of 4 columns + 3 gutters 106 | height: Grid.getHeight(15) # Returns the height of 15 rows 107 | ``` 108 | 109 | ### Properties 110 | All these properties are readonly. 111 | - safezoneBounds: `object` - Returns width & height of the Grid safezone 112 | - dangerzoneBounds: `object` - Returns the width of the dangergap between the left screen edge and the horizontal safezone start point & height of the dangergap between the top screen edge and the vertical safezone start point. 113 | - columnCount: `number` - Returns the amount of columns in the Grid 114 | - columnGutterCount: `number` - Returns the amount of column gutters in the Grid 115 | - columnGutter: `number` - Returns the column gutter width 116 | - columnWidth: `number` - Returns the width of a single column 117 | - rowHeight: `number` - Returns the height of a row 118 | 119 | ### Methods 120 | - getWidth(columnCount: number) - Returns the pixels of the amount of columns + (amount of columns - 1) gutters 121 | - getHeight(rowCount: number) - Returns the pixels of rowCount * 10px 122 | 123 | 124 | ## Gamepad 125 | The Gamepad instance emits gamepadevents whenever you trigger a button on your gamepad. 126 | The Gamepad is an EventEmitter under the hood. To handle gamepad events, simply do like this: 127 | 128 | ``` 129 | # If you want throttled events, uncomment the line below 130 | # Gamepad.throttle = true 131 | 132 | Gamepad.on 'gamepadevent', (e) -> 133 | # Gamepad Right Stick Going Up 134 | if e.keyCode == 41 135 | print 'Up' 136 | 137 | # Gamepad Right Stick Going Left 138 | if e.keyCode == 42 139 | print 'Left' 140 | 141 | # Gamepad Right Stick Going Down 142 | if e.keyCode == 43 143 | print 'Down' 144 | 145 | # Gamepad Right Stick Going Right 146 | if e.keyCode == 44 147 | print 'Right' 148 | ``` 149 | 150 | ### Properties 151 | - throttle: `boolean` - By default, all gamepad events are emitted multiple times a second. Setting this value to true will limit the events to be emitted less frequently. 152 | 153 | 154 | ## FocusStore 155 | The FocusStore holds the state of all focusables within the current view. It emits events when focusing occurs. 156 | The FocusStore is an EventEmitter under the hood. 157 | 158 | ``` 159 | FocusStore.on 'focusEvent', (focusEvent) -> 160 | # The focusEvent contain the previously focused focusable and the new, current focused focusable 161 | ``` 162 | 163 | ## ViewStore 164 | The ViewStore holds the state of all focusables within the current view. It emits events when view-transitions occurs. 165 | The ViewStore is an EventEmitter under the hood. 166 | 167 | ``` 168 | # Listen for transition events 169 | ViewStore.on 'transitionEvent', (transitionEvent) -> 170 | # The transitionEvent contain the previous view, the current view and the transition that was used 171 | 172 | # Transition to a view using the goIn transition 173 | ViewStore.transition(myView, Transitions.goIn) 174 | 175 | # Pop the history stack and go back to the previous screen 176 | ViewStore.goBack() 177 | ``` 178 | 179 | ### Methods 180 | - transition(view: View, transition?: Transition) - Transitions to the specified view. Transition can be specified. You can use [these pre-defined transitions](https://github.com/emilwidlund/carousel/blob/master/dist/templates/modules/joystick/Transitions.coffee) or read more about them [here](https://medium.com/the-school-of-do/framer-cheat-sheet-flow-components-f7ca94632227#c7f7). 181 | - goBack() - Pops the history stack and goes back to the previous screen 182 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {app, ipcMain, dialog, BrowserWindow} = require('electron'); 2 | const path = require('path'); 3 | 4 | const server = require('./server'); 5 | 6 | // Setup User Analytics 7 | const {trackEvent} = require('./analytics'); 8 | global.trackEvent = trackEvent; 9 | 10 | let mainWindow; 11 | let previewWindow; 12 | let projectServerRunning; 13 | 14 | app.on('window-all-closed', () => { 15 | if (process.platform != 'darwin') { 16 | app.quit(); 17 | } 18 | 19 | trackEvent('Window', 'All Windows Closed'); 20 | }); 21 | 22 | app.on('ready', () => { 23 | mainWindow = new BrowserWindow({ 24 | title: 'Carousel', 25 | width: 1360, 26 | height: 800, 27 | icon: path.join(__dirname, 'dist/icons/png/64x64.png') 28 | }); 29 | trackEvent('Window', 'Open', 'Main Window'); 30 | 31 | mainWindow.loadFile('./dist/index.html'); 32 | 33 | if (!app.isPackaged) { 34 | mainWindow.openDevTools(); 35 | trackEvent('Window', 'Open Developer Tools', 'Main Window'); 36 | } 37 | 38 | mainWindow.on('close', (e) => { 39 | e.preventDefault(); 40 | 41 | if (projectServerRunning) { 42 | previewWindow.destroy(); 43 | } 44 | 45 | dialog.showMessageBox({ 46 | type: 'warning', 47 | buttons: ['Save', 'Don\'t Save', 'Cancel'], 48 | message: 'Your project has not been saved. How do you want to proceed?', 49 | noLink: true 50 | }, (response) => { 51 | if (response === 0) { 52 | mainWindow.webContents.send('save-project'); 53 | trackEvent('Window', 'Save Project On Close', 'Main Window'); 54 | } else if (response === 1) { 55 | mainWindow.destroy(); 56 | trackEvent('Window', 'Don\'t Save Project On Close', 'Main Window'); 57 | } else { 58 | trackEvent('Window', 'Cancel On Close', 'Main Window'); 59 | } 60 | }); 61 | 62 | }); 63 | 64 | mainWindow.on('close', () => { 65 | if (previewWindow) { 66 | previewWindow.destroy(); 67 | trackEvent('Window', 'Inheritly Closed', 'Preview Window'); 68 | } 69 | }); 70 | 71 | mainWindow.on('closed', () => { 72 | mainWindow = null; 73 | trackEvent('Window', 'Closed', 'Main Window'); 74 | }); 75 | }); 76 | 77 | ipcMain.on('start-preview', (event, arg) => { 78 | if (projectServerRunning) return; 79 | 80 | server.start(arg, () => { 81 | projectServerRunning = true; 82 | trackEvent('Project Server', 'Start'); 83 | 84 | previewWindow = new BrowserWindow({ 85 | title: 'Preview', 86 | width: 640, 87 | height: 360, 88 | alwaysOnTop: true, 89 | experimentalFeatures: true, 90 | autoHideMenuBar: true, 91 | icon: path.join(__dirname, 'dist/icons/png/64x64.png') 92 | }); 93 | trackEvent('Window', 'Open', 'Preview Window'); 94 | 95 | previewWindow.loadURL('http://localhost:8010'); 96 | 97 | if (!app.isPackaged) { 98 | previewWindow.openDevTools(); 99 | trackEvent('Window', 'Open Developer Tools', 'Preview Window'); 100 | } 101 | 102 | previewWindow.on('closed', () => { 103 | previewWindow = null; 104 | trackEvent('Window', 'Closed', 'Preview Window'); 105 | 106 | projectServerRunning = false; 107 | server.close(); 108 | trackEvent('Project Server', 'Closed', 'Preview Window'); 109 | }); 110 | 111 | mainWindow.on('closed', () => { 112 | projectServerRunning = false; 113 | server.close(); 114 | trackEvent('Project Server', 'Closed', 'Main Window'); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carousel", 3 | "productName": "Carousel", 4 | "version": "0.1.2", 5 | "description": "", 6 | "main": "main.js", 7 | "scripts": { 8 | "start": "electron .", 9 | "server": "./node_modules/.bin/webpack-dev-server", 10 | "watch": "webpack --watch", 11 | "bundle": "webpack", 12 | "build": "electron-builder", 13 | "release": "webpack && electron-builder -m -w" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/emilwidlund/carousel.git" 18 | }, 19 | "build": { 20 | "appId": "com.carousel.framer", 21 | "win": { 22 | "icon": "dist/icons/win/icon.png" 23 | }, 24 | "mac": { 25 | "category": "public.app-category.graphics-design", 26 | "icon": "dist/icons/mac/icon.png" 27 | }, 28 | "directories": { 29 | "output": "build" 30 | }, 31 | "fileAssociations": [ 32 | { 33 | "ext": [ 34 | "crsl" 35 | ], 36 | "name": "Carousel", 37 | "description": "Carousel Project File" 38 | } 39 | ] 40 | }, 41 | "files": [ 42 | "*.js", 43 | "dist", 44 | "node_modules" 45 | ], 46 | "keywords": [], 47 | "author": "Emil Widlund", 48 | "license": "ISC", 49 | "bugs": { 50 | "url": "https://github.com/emilwidlund/carousel/issues" 51 | }, 52 | "homepage": "https://github.com/emilwidlund/carousel#readme", 53 | "dependencies": { 54 | "@babel/core": "^7.1.2", 55 | "@babel/plugin-proposal-class-properties": "^7.1.0", 56 | "@babel/plugin-proposal-decorators": "^7.1.2", 57 | "@babel/preset-env": "^7.1.0", 58 | "@types/classnames": "^2.2.6", 59 | "@types/electron-store": "^1.3.0", 60 | "@types/react": "^16.4.14", 61 | "@types/react-dom": "^16.0.8", 62 | "archiver": "^3.0.0", 63 | "babel-cli": "^6.26.0", 64 | "babel-loader": "^8.0.4", 65 | "classnames": "^2.2.6", 66 | "coffee-loader": "^0.9.0", 67 | "coffeescript": "^2.3.2", 68 | "css-loader": "^1.0.0", 69 | "electron-store": "^2.0.0", 70 | "mobx": "^5.5.0", 71 | "mobx-react": "^5.2.8", 72 | "monaco-editor": "^0.14.3", 73 | "monaco-editor-webpack-plugin": "^1.5.4", 74 | "node-sass": "^4.9.3", 75 | "react": "^16.5.2", 76 | "react-dom": "^16.5.2", 77 | "sass-loader": "^7.1.0", 78 | "style-loader": "^0.23.0", 79 | "temp": "^0.8.3", 80 | "universal-analytics": "^0.4.17", 81 | "unzip": "^0.1.11", 82 | "unzip-stream": "^0.3.0", 83 | "uuid": "^3.3.2", 84 | "walk": "^2.3.14", 85 | "webpack": "^4.20.2", 86 | "webpack-cli": "^3.1.1", 87 | "webpack-dev-server": "^3.1.9" 88 | }, 89 | "devDependencies": { 90 | "awesome-typescript-loader": "^5.2.1", 91 | "electron": "^3.0.2", 92 | "electron-builder": "^20.28.4", 93 | "source-map-loader": "^0.2.4", 94 | "typescript": "^3.1.1" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackDevServer = require('webpack-dev-server'); 3 | const path = require('path'); 4 | require('babel-loader'); 5 | 6 | const PORT = 8010; 7 | 8 | let server; 9 | 10 | module.exports = { 11 | start: (servePath, cb) => { 12 | 13 | const config = { 14 | entry: { 15 | app: [path.resolve(__dirname, 'node_modules/webpack/hot/dev-server'), `${servePath}/app.js`] 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.coffee'] 19 | }, 20 | mode: 'development', 21 | target: 'web', 22 | output: { 23 | path: servePath, 24 | filename: 'bundle.js' 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | loader: path.resolve(__dirname, 'node_modules/babel-loader'), 31 | options: { 32 | babelrc: false, 33 | presets: [path.resolve(__dirname, 'node_modules/@babel/preset-env')], 34 | plugins: [ 35 | [path.resolve(__dirname, 'node_modules/@babel/plugin-proposal-decorators'), {legacy: true}], 36 | path.resolve(__dirname, 'node_modules/@babel/plugin-proposal-class-properties') 37 | ] 38 | } 39 | }, 40 | { 41 | test: /\.coffee$/, 42 | use: [path.resolve(__dirname, 'node_modules/coffee-loader')] 43 | } 44 | ] 45 | }, 46 | plugins: [new webpack.HotModuleReplacementPlugin()] 47 | } 48 | 49 | const options = { 50 | contentBase: servePath 51 | }; 52 | 53 | server = new WebpackDevServer(webpack(config), options); 54 | 55 | 56 | return server.listen(PORT, 'localhost', (err) => { 57 | if (err) return console.error(err); 58 | 59 | console.log('Serrver running on port', PORT); 60 | 61 | if (cb) cb(); 62 | }); 63 | }, 64 | close: (cb) => { 65 | server.close(cb); 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import {IButtonProps} from '../types'; 5 | 6 | export default class Button extends React.Component { 7 | render() { 8 | return ( 9 | 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as monaco from 'monaco-editor'; 3 | import {reaction} from 'mobx'; 4 | import {inject, observer} from 'mobx-react'; 5 | 6 | import {IEditorProps} from '../types'; 7 | 8 | @inject('ProjectStore', 'EditorStore') 9 | @observer 10 | export default class Editor extends React.Component { 11 | 12 | _node: HTMLElement; 13 | _editor: monaco.editor.IStandaloneCodeEditor; 14 | 15 | componentDidMount() { 16 | const {options} = this.props; 17 | this._editor = monaco.editor.create(this._node, options); 18 | this.props.EditorStore.editor = this._editor; 19 | 20 | this._editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => { 21 | this.props.ProjectStore.saveProject(); 22 | }, ''); 23 | 24 | reaction( 25 | () => this.props.EditorStore.selectedFile, 26 | (file) => { 27 | this._editor.setModel(file.model); 28 | } 29 | ); 30 | } 31 | 32 | componentWillUnmount() { 33 | this._editor && this._editor.dispose(); 34 | } 35 | 36 | renderMeta() { 37 | return ( 38 |
39 |

{this.props.EditorStore.selectedFile.shortName}

40 | {this.props.EditorStore.selectedFile.name} 41 |
42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | {this.props.EditorStore.selectedFile ? this.renderMeta() : null} 49 |
this._node = c} 52 | /> 53 |
54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import {IIconProps} from '../types'; 5 | 6 | const Icon: React.SFC = (props) => ( 7 | 16 | {props.name} 17 | 18 | ); 19 | 20 | export default Icon; -------------------------------------------------------------------------------- /src/components/Popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class Popup extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 | {this.props.children} 9 |
10 |
11 | ); 12 | } 13 | } -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classnames from 'classnames'; 3 | import {inject, observer} from 'mobx-react'; 4 | 5 | import {remote, ipcRenderer} from 'electron'; 6 | const shell = remote.shell; 7 | 8 | import Tabs from './Tabs'; 9 | import Icon from './Icon'; 10 | import Button from './Button'; 11 | import snippets from '../snippets'; 12 | 13 | import { 14 | ISidebarProps, 15 | IProjectFileCategoryHeaderProps, 16 | IProjectFileItemProps, 17 | IProjectFile, 18 | ProjectFileType 19 | } from '../types'; 20 | 21 | @inject('ProjectStore', 'PopupStore') 22 | class CreateFilePopup extends React.Component { 23 | 24 | state = { 25 | filename: '' 26 | } 27 | 28 | render() { 29 | 30 | let title; 31 | 32 | switch(this.props.type) { 33 | case ProjectFileType.View: 34 | title = 'Create New View'; 35 | break; 36 | case ProjectFileType.Component: 37 | title = 'Create New Component'; 38 | break; 39 | case ProjectFileType.Generic: 40 | title = 'Create New Generic File'; 41 | break; 42 | default: 43 | title = 'Create New Generic File'; 44 | } 45 | 46 | return ( 47 |
48 |

{title}

49 | { 54 | this.setState({ 55 | filename: e.target.value.trim() 56 | }); 57 | }} 58 | /> 59 |
60 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | @inject('PopupStore') 82 | class ProjectFileCategoryHeader extends React.Component { 83 | render() { 84 | return ( 85 |
88 |

{this.props.title}

89 | { 90 | this.props.type === ProjectFileType.Image ? 91 | null : 92 | { 95 | this.props.PopupStore.displayPopup(); 96 | }} 97 | /> 98 | } 99 |
100 | ); 101 | } 102 | } 103 | 104 | 105 | @inject('EditorStore') 106 | @observer 107 | class ProjectFileItem extends React.Component { 108 | handleClick() { 109 | this.props.EditorStore.selectFile(this.props.file); 110 | } 111 | 112 | render() { 113 | 114 | let iconName; 115 | 116 | switch (this.props.file.type) { 117 | case ProjectFileType.Main: 118 | iconName = 'all_inclusive'; 119 | break; 120 | case ProjectFileType.View: 121 | iconName = 'panorama_horizontal'; 122 | break; 123 | case ProjectFileType.Component: 124 | iconName = 'bubble_chart'; 125 | break; 126 | case ProjectFileType.Image: 127 | iconName = 'image'; 128 | break; 129 | default: 130 | iconName = 'description'; 131 | } 132 | 133 | return ( 134 |
141 | 142 | 143 | { 144 | this.props.file.type === ProjectFileType.Generic || this.props.file.type === ProjectFileType.Image ? 145 | this.props.file.name : 146 | this.props.file.shortName 147 | } 148 | 149 |
150 | ); 151 | } 152 | } 153 | 154 | 155 | 156 | @inject('ProjectStore', 'EditorStore') 157 | @observer 158 | class ProjectPanelNavigator extends React.Component { 159 | render() { 160 | return ( 161 |
162 |

Project Navigator

163 |
164 | 168 |
169 |
170 | 174 | {this.props.ProjectStore.viewFiles.map((file: IProjectFile, index: number) => { 175 | return ( 176 | 181 | ); 182 | })} 183 |
184 |
185 | 189 | {this.props.ProjectStore.componentFiles.map((file: IProjectFile, index: number) => { 190 | return ( 191 | 196 | ); 197 | })} 198 |
199 |
200 | 204 | {this.props.ProjectStore.imageFiles.map((file: IProjectFile, index: number) => { 205 | return ( 206 | 211 | ); 212 | })} 213 |
214 |
215 | 219 | {this.props.ProjectStore.genericFiles.map((file: IProjectFile, index: number) => { 220 | return ( 221 | 226 | ); 227 | })} 228 |
229 |
230 | ); 231 | } 232 | } 233 | 234 | 235 | class ProjectPanel extends React.Component { 236 | render() { 237 | return ( 238 |
239 | 240 |
241 | ); 242 | } 243 | } 244 | 245 | 246 | 247 | @inject('ProjectStore', 'EditorStore') 248 | @observer 249 | class SnippetItem extends React.Component { 250 | handleClick() { 251 | if (this.props.ProjectStore.fileFormat == 'js') { 252 | this.props.EditorStore.pasteSnippet(this.props.snippet.jsSnippet()); 253 | } else if (this.props.ProjectStore.fileFormat == 'coffee') { 254 | this.props.EditorStore.pasteSnippet(this.props.snippet.csSnippet()); 255 | } 256 | } 257 | 258 | render() { 259 | return ( 260 |
264 | 265 | {this.props.snippet.name} 266 |
267 | ); 268 | } 269 | } 270 | 271 | 272 | 273 | class SnippetPanel extends React.Component { 274 | render() { 275 | return ( 276 |
277 |

Snippets

278 |
279 | {snippets.map((s, i) => { 280 | return ; 281 | })} 282 |
283 |
284 | ); 285 | } 286 | } 287 | 288 | 289 | @inject('ProjectStore') 290 | @observer 291 | export default class Sidebar extends React.Component { 292 | componentDidMount() { 293 | ipcRenderer.on('save-project', () => { 294 | this.props.ProjectStore.saveProject(() => { 295 | remote.getCurrentWindow().destroy(); 296 | }); 297 | }); 298 | } 299 | 300 | render() { 301 | return ( 302 | 318 | ); 319 | } 320 | } -------------------------------------------------------------------------------- /src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import Icon from './Icon'; 5 | 6 | class Tab extends React.Component { 7 | render() { 8 | const classNames = classnames([ 9 | 'tab', 10 | this.props.active ? 'active' : null 11 | ]); 12 | 13 | return ( 14 |
this.props.selectTab(this.props.index)} 17 | > 18 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | class TabContent extends React.Component { 27 | render() { 28 | return ( 29 |
30 | {this.props.content} 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default class Tabs extends React.Component { 37 | 38 | state = { 39 | selectedIndex: 0 40 | } 41 | 42 | constructor(props: any) { 43 | super(props); 44 | 45 | this.selectTab = this.selectTab.bind(this); 46 | } 47 | 48 | selectTab(index: number) { 49 | this.setState({ 50 | selectedIndex: index 51 | }); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
58 | {this.props.tabs.map((tab: any, index: number) => { 59 | return ( 60 | 67 | ); 68 | })} 69 |
70 | 74 |
75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /src/components/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {inject, observer} from 'mobx-react'; 3 | import classnames from 'classnames'; 4 | import {remote} from 'electron'; 5 | const dialog = remote.dialog; 6 | const shell = remote.shell; 7 | const os = remote.require('os'); 8 | const path = remote.require('path'); 9 | const trackEvent = remote.getGlobal('trackEvent'); 10 | 11 | import Button from './Button'; 12 | import Icon from './Icon'; 13 | import {IRecentProjectsProps, IRecentProject} from '../types'; 14 | 15 | const version = require('../../package.json').version; 16 | 17 | 18 | @inject('ProjectStore', 'PopupStore') 19 | @observer 20 | class NewProjectPopup extends React.Component { 21 | 22 | defaultFilename: string = 'Untitled.crsl'; 23 | 24 | state = { 25 | filename: path.resolve(os.homedir(), 'Documents', this.defaultFilename), 26 | projectType: 'js' 27 | } 28 | 29 | handleProjectTypeChange(e: any) { 30 | this.setState({ 31 | projectType: e.target.value 32 | }); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 |

Create New Project

39 |
40 | { 44 | this.setState({ 45 | filename: e.target.value 46 | }); 47 | }} 48 | /> 49 |
62 |
63 |
64 | 70 | JavaScript 71 |
72 |
73 | 79 | CoffeeScript 80 |
81 |
82 |
83 |
101 |
102 | ); 103 | } 104 | } 105 | 106 | 107 | 108 | @inject('ProjectStore') 109 | @observer 110 | class RecentProjects extends React.Component { 111 | render() { 112 | return ( 113 |
114 | {this.props.ProjectStore.recentProjects.map((p: IRecentProject, i: number) => { 115 | return ( 116 |
{ 120 | this.props.ProjectStore.initializeProject(p.path); 121 | }} 122 | > 123 |

{p.name}.crsl

124 | {p.path} 125 |
126 | ); 127 | })} 128 |
129 | ); 130 | } 131 | } 132 | 133 | 134 | @inject('ProjectStore', 'PopupStore') 135 | @observer 136 | export default class Welcome extends React.Component { 137 | 138 | handleNewProject() { 139 | this.props.PopupStore.displayPopup(); 140 | trackEvent('Popup', 'Display', 'New Project'); 141 | } 142 | 143 | handleOpenProject() { 144 | dialog.showOpenDialog({ 145 | filters: [ 146 | { 147 | name: 'Carousel', 148 | extensions: ['crsl'] 149 | } 150 | ] 151 | }, (filepaths: string[]) => { 152 | if (!filepaths) return; 153 | this.props.ProjectStore.initializeProject(filepaths[0]); 154 | }); 155 | } 156 | 157 | openLink(url: string) { 158 | shell.openExternal(url); 159 | trackEvent('Outbound Link', 'Click', url); 160 | } 161 | 162 | render() { 163 | return ( 164 |
165 |
166 |
167 | 168 |

Welcome to Carousel

169 |

Version {version}

170 |
171 |
175 | 179 |

New Project

180 |
181 |
185 | 189 |

Open Project

190 |
191 |
192 |
193 |
194 |

this.openLink('https://twitter.com/emilwidlund')}>Built by Emil Widlund

195 |

this.openLink('https://github.com/koenbok/Framer')}>Powered by Framer Library

196 |
197 |
198 | 199 |
200 | ); 201 | } 202 | } -------------------------------------------------------------------------------- /src/components/_Editor.scss: -------------------------------------------------------------------------------- 1 | #editor-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | flex-grow: 1; 5 | background-color: #000; 6 | width: 100%; 7 | height: 100%; 8 | 9 | .editor-meta { 10 | display: flex; 11 | flex-direction: column; 12 | padding: 30px 30px; 13 | // border-bottom: 1px solid $color-divider; 14 | 15 | h2 { 16 | margin-top: 10px; 17 | } 18 | } 19 | 20 | .editor { 21 | display: flex; 22 | flex-direction: column; 23 | flex-grow: 1; 24 | flex-shrink: 1; 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/_Popup.scss: -------------------------------------------------------------------------------- 1 | .popup-container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | background-color: rgba(0, 0, 0, .8); 12 | z-index: 999999; 13 | 14 | .popup-content { 15 | display: flex; 16 | flex-direction: column; 17 | min-width: 500px; 18 | min-height: 250px; 19 | border-radius: 20px; 20 | background-color: #050505; 21 | padding: 40px; 22 | text-align: center; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/_Sidebar.scss: -------------------------------------------------------------------------------- 1 | $sidebar-padding-horizontal: 20px; 2 | 3 | #sidebar { 4 | display: flex; 5 | flex-direction: column; 6 | flex-grow: 0; 7 | flex-shrink: 0; 8 | overflow-y: auto; 9 | width: 380px; 10 | background-color: $color-bg-sidebar; 11 | border-right: 1px solid $color-divider; 12 | user-select: none; 13 | 14 | .project-panel-navigator { 15 | display: flex; 16 | flex-direction: column; 17 | flex-grow: 1; 18 | padding: 10px 0 0; 19 | 20 | h3 { 21 | padding: 5px $sidebar-padding-horizontal; 22 | } 23 | 24 | .views, .components, .images, .generic { 25 | padding: 10px 0; 26 | 27 | .file-category-header { 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | align-items: center; 32 | padding: 0 $sidebar-padding-horizontal; 33 | 34 | .icon { 35 | color: #555; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .snippet-panel { 42 | padding: 10px 0 0; 43 | 44 | h3 { 45 | padding: 5px $sidebar-padding-horizontal; 46 | } 47 | 48 | .snippet-items { 49 | display: flex; 50 | flex-direction: column; 51 | 52 | .sidebar-interactive-item { 53 | span { 54 | color: #fff; 55 | } 56 | } 57 | } 58 | } 59 | 60 | .sidebar-interactive-item { 61 | display: flex; 62 | align-items: center; 63 | padding: 10px $sidebar-padding-horizontal; 64 | 65 | span { 66 | margin-left: 15px; 67 | } 68 | 69 | .icon { 70 | color: $color-accent; 71 | } 72 | 73 | &.active { 74 | color: #fff; 75 | background-color: rgba(255, 255, 255, .05); 76 | 77 | &:hover { 78 | background-color: rgba(255, 255, 255, .05); 79 | } 80 | } 81 | 82 | &:hover { 83 | background-color: rgba(255, 255, 255, .05); 84 | } 85 | } 86 | } 87 | 88 | .create-new-file-popup { 89 | display: flex; 90 | flex-direction: column; 91 | align-items: center; 92 | 93 | input { 94 | margin-top: 20px; 95 | width: 200px; 96 | } 97 | 98 | .new-file-actions { 99 | display: flex; 100 | flex-direction: row; 101 | justify-content: center; 102 | user-select: none; 103 | margin-top: 40px; 104 | 105 | .secondary { 106 | margin-left: 20px; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/components/_Tabs.scss: -------------------------------------------------------------------------------- 1 | .tabs { 2 | display: flex; 3 | flex-direction: row; 4 | flex-grow: 1; 5 | 6 | .tabs-header { 7 | display: flex; 8 | flex-direction: column; 9 | cursor: default; 10 | border-right: 1px solid $color-divider; 11 | 12 | .tab { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | width: 65px; 18 | height: 65px; 19 | 20 | .icon { 21 | color: rgba(255, 255, 255, .33); 22 | } 23 | 24 | &.active { 25 | background-color: $color-accent; 26 | 27 | .icon { 28 | color: #fff; 29 | } 30 | 31 | &:hover { 32 | background-color: $color-accent; 33 | } 34 | } 35 | 36 | &:hover { 37 | background-color: rgba(255, 255, 255, .1); 38 | } 39 | } 40 | } 41 | 42 | .tab-content { 43 | display: flex; 44 | flex-direction: column; 45 | overflow-y: auto; 46 | flex-grow: 1; 47 | } 48 | } -------------------------------------------------------------------------------- /src/components/_Welcome.scss: -------------------------------------------------------------------------------- 1 | #welcome { 2 | display: flex; 3 | justify-content: center; 4 | flex-grow: 1; 5 | background-color: $color-bg-welcome; 6 | user-select: none; 7 | 8 | .welcome-info { 9 | display: flex; 10 | align-items: center; 11 | flex-direction: column; 12 | flex-grow: 1; 13 | 14 | .welcome-info-content { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | text-align: center; 20 | flex-grow: 1; 21 | 22 | img { 23 | height: 150px; 24 | } 25 | 26 | .welcome-actions { 27 | display: flex; 28 | flex-direction: row; 29 | justify-content: space-between; 30 | width: 400px; 31 | margin-top: 40px; 32 | 33 | .action { 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | padding: 30px 40px; 38 | background-color: #111; 39 | border-radius: 10px; 40 | margin-top: 20px; 41 | 42 | .icon { 43 | color: $color-accent; 44 | } 45 | 46 | h3 { 47 | margin-top: 30px; 48 | text-align: center; 49 | } 50 | 51 | &:hover { 52 | background-color: #151515; 53 | cursor: pointer; 54 | } 55 | } 56 | } 57 | } 58 | 59 | .welcome-meta { 60 | padding: 30px 0; 61 | text-align: center; 62 | 63 | h4, p { 64 | cursor: pointer; 65 | } 66 | } 67 | } 68 | 69 | .recent-projects { 70 | display: flex; 71 | flex-direction: column; 72 | flex-shrink: 0; 73 | width: 300px; 74 | border-left: 1px solid $color-divider; 75 | 76 | .recent-project { 77 | display: flex; 78 | flex-direction: column; 79 | padding: 15px 20px; 80 | 81 | &:hover { 82 | background-color: rgba(255, 255, 255, .05); 83 | } 84 | 85 | h4 { 86 | margin-top: 0; 87 | } 88 | 89 | span { 90 | white-space: nowrap; 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | font-size: $font-size-xs; 94 | } 95 | } 96 | } 97 | } 98 | 99 | .new-project-popup { 100 | display: flex; 101 | flex-direction: column; 102 | flex-grow: 1; 103 | 104 | .new-project-path { 105 | display: flex; 106 | flex-direction: row; 107 | justify-content: center; 108 | margin-top: 20px; 109 | 110 | input { 111 | width: 200px; 112 | } 113 | 114 | button { 115 | margin-left: 10px; 116 | } 117 | } 118 | 119 | .new-project-type { 120 | display: flex; 121 | flex-direction: row; 122 | justify-content: center; 123 | padding: 30px 0 40px; 124 | user-select: none; 125 | 126 | div { 127 | display: flex; 128 | flex-direction: row; 129 | justify-content: baseline; 130 | 131 | span { 132 | margin-left: 10px; 133 | margin-top: 2px; 134 | } 135 | 136 | &:last-of-type { 137 | margin-left: 30px; 138 | } 139 | 140 | &.active { 141 | span { 142 | color: #fff; 143 | } 144 | } 145 | } 146 | } 147 | 148 | .new-project-actions { 149 | display: flex; 150 | flex-direction: row; 151 | justify-content: center; 152 | user-select: none; 153 | 154 | .secondary { 155 | margin-left: 20px; 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import {Provider, observer} from 'mobx-react'; 4 | import * as monaco from 'monaco-editor'; 5 | import {ipcRenderer, shell} from 'electron'; 6 | 7 | import * as stores from './stores'; 8 | import Icon from './components/Icon'; 9 | import Popup from './components/Popup'; 10 | import Sidebar from './components/Sidebar'; 11 | import Welcome from './components/Welcome'; 12 | import Editor from './components/Editor'; 13 | 14 | import {remote} from 'electron'; 15 | 16 | import './scss/main.scss'; 17 | 18 | 19 | 20 | const framerTheme = require('./misc/framer-theme.json'); 21 | monaco.editor.defineTheme('syntax', framerTheme); 22 | 23 | 24 | const AppAction = (props: any) => { 25 | return ( 26 |
27 | 28 | {props.text} 29 |
30 | ); 31 | } 32 | 33 | 34 | @observer 35 | class App extends React.Component { 36 | 37 | componentDidMount() { 38 | if (remote.app.isPackaged) { 39 | if (remote.process.argv.length >= 2) { 40 | const filePath = remote.process.argv[1]; 41 | if (filePath) { 42 | stores.ProjectStore.initializeProject(filePath); 43 | } 44 | } 45 | } 46 | } 47 | 48 | renderContent() { 49 | return ( 50 |
51 |
52 |
53 | 54 |

{stores.ProjectStore.projectName}

55 |
56 |
57 | { 61 | ipcRenderer.send('start-preview', stores.ProjectStore.projectTempPath); 62 | }} 63 | /> 64 | { 68 | shell.openItem(stores.ProjectStore.projectTempPath); 69 | }} 70 | /> 71 | { 75 | shell.openExternal('https://classic.framer.com/docs'); 76 | }} 77 | /> 78 |
79 |
80 |
81 | 82 | 95 |
96 |
97 | ); 98 | } 99 | 100 | render() { 101 | return ( 102 | 103 | 115 | 116 | ); 117 | } 118 | } 119 | 120 | ReactDOM.render( 121 | , 122 | document.getElementById('carousel') 123 | ); -------------------------------------------------------------------------------- /src/misc/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilwidlund/carousel/1ab0e12800cdbe84c160e471bb7b98bc8b37b005/src/misc/app-icon.png -------------------------------------------------------------------------------- /src/misc/framer-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "vs-dark", 3 | "inherit": true, 4 | "rules": [], 5 | "colors": { 6 | "activityBar.background": "#111", 7 | "activityBar.border": "#111", 8 | "activityBar.dropBackground": "#000", 9 | "activityBar.foreground": "#fff", 10 | "activityBarBadge.foreground": "#FFF", 11 | 12 | "activityBarBadge.background": "#00AEFF", 13 | "list.activeSelectionForeground": "#fff", 14 | "list.inactiveSelectionForeground": "#fff", 15 | "list.highlightForeground": "#00AEFF", 16 | "scrollbarSlider.activeBackground": "#777", 17 | "editorSuggestWidget.highlightForeground": "#fff", 18 | "textLink.foreground": "#00AEFF", 19 | "progressBar.background": "#00AEFF", 20 | "pickerGroup.foreground": "#00AEFF", 21 | "tab.activeBorder": "#00AEFF", 22 | "notificationLink.foreground": "#00AEFF", 23 | 24 | "badge.foreground": "#fff", 25 | 26 | "button.background": "#272727", 27 | "button.foreground": "#fff", 28 | "button.hoverBackground": "#333", 29 | 30 | "contrastBorder": "#000", 31 | 32 | "debugExceptionWidget.background": "#272727", 33 | "debugExceptionWidget.border": "#2D2D2D", 34 | "debugToolBar.background": "#141414", 35 | 36 | "descriptionForeground": "#808080", 37 | 38 | "diffEditor.insertedTextBackground": "#333", 39 | "diffEditor.insertedTextBorder": "#2D2D2D", 40 | "diffEditor.removedTextBackground": "#333", 41 | "diffEditor.removedTextBorder": "#2D2D2D", 42 | 43 | "dropdown.background": "#333", 44 | "dropdown.border": "#2D2D2D", 45 | "dropdown.foreground": "#fff", 46 | 47 | "editor.background": "#000", 48 | 49 | "editor.foreground": "#fff", 50 | 51 | "editor.findMatchBackground": "#555", 52 | 53 | "editor.findMatchHighlightBackground": "#333", 54 | 55 | "editor.findRangeHighlightBackground": "#333", 56 | 57 | "editor.hoverHighlightBackground": "#333", 58 | 59 | "editor.inactiveSelectionBackground": "#333", 60 | 61 | "editor.lineHighlightBackground": "#111", 62 | "editor.lineHighlightBorder": "#111", 63 | "editor.rangeHighlightBackground": "#222", 64 | 65 | "editor.selectionBackground": "#111", 66 | 67 | "editor.selectionHighlightBackground": "#111", 68 | 69 | "editor.wordHighlightStrongBackground": "#111", 70 | 71 | "editor.wordHighlightBackground": "#111", 72 | "editorBracketMatch.background": "#333", 73 | "editorBracketMatch.border": "#333", 74 | "editorCodeLens.foreground": "#333", 75 | "editorCursor.background": "#111", 76 | "editorCursor.foreground": "#fff", 77 | "editorError.border": "#2D2D2D", 78 | "editorError.foreground": "#e27e8d", 79 | 80 | "editorGutter.background": "#000", 81 | "editorGutter.deletedBackground": "#e27e8d", 82 | "editorGutter.modifiedBackground": "#111", 83 | 84 | "editorGroup.background": "#111", 85 | "editorGroup.border": "#2D2D2D", 86 | "editorGroup.dropBackground": "#111", 87 | 88 | "editorGroupHeader.tabsBackground": "#111", 89 | "editorGroupHeader.tabsBorder": "#2d2d2d", 90 | 91 | "editorHoverWidget.background": "#111", 92 | "editorHoverWidget.border": "#2d2d2d", 93 | "editorIndentGuide.background": "#111", 94 | "editorLineNumber.foreground": "#333", 95 | "editorLineNumber.activeForeground": "#777", 96 | "editorLink.activeForeground": "#888", 97 | 98 | "editorMarkerNavigation.background": "#111", 99 | "editorMarkerNavigationError.background": "#111", 100 | "editorMarkerNavigationWarning.background": "#2d2d2d", 101 | 102 | "editorOverviewRuler.border": "#2d2d2d", 103 | "editorOverviewRuler.commonContentForeground": "#000", 104 | "editorOverviewRuler.currentContentForeground": "#000", 105 | "editorOverviewRuler.incomingContentForeground": "#000", 106 | "editorRuler.foreground": "#000", 107 | 108 | "editorSuggestWidget.background": "#111", 109 | "editorSuggestWidget.border": "#2d2d2d", 110 | "editorSuggestWidget.foreground": "#888", 111 | "editorSuggestWidget.selectedBackground": "#333", 112 | 113 | "editorWarning.border": "#ffffff00", 114 | "editorWarning.foreground": "#e27e8d", 115 | "editorWhitespace.foreground": "#444", 116 | "editorWidget.background": "#111", 117 | "editorWidget.border": "#2d2d2d", 118 | "errorForeground": "#e27e8d", 119 | 120 | "extensionButton.prominentBackground": "#333", 121 | "extensionButton.prominentForeground": "#fff", 122 | "extensionButton.prominentHoverBackground": "#222", 123 | "focusBorder": "#2d2d2d", 124 | "foreground": "#888", 125 | 126 | "input.background": "#111", 127 | "input.border": "#111", 128 | "input.foreground": "#fff", 129 | "input.placeholderForeground": "#888", 130 | "inputOption.activeBorder": "#eee", 131 | "inputValidation.errorBackground": "#FF5459", 132 | "inputValidation.errorBorder": "#4C292A", 133 | "inputValidation.infoBackground": "#5ec4ff", 134 | "inputValidation.infoBorder": "#539afc", 135 | "inputValidation.warningBackground": "#fff", 136 | "inputValidation.warningBorder": "#d98e48", 137 | 138 | "list.activeSelectionBackground": "#333", 139 | "list.dropBackground": "#000", 140 | "list.focusBackground": "#333", 141 | "list.focusForeground": "#888", 142 | "list.hoverBackground": "#333", 143 | "list.hoverForeground": "#fff", 144 | "list.inactiveSelectionBackground": "#333", 145 | 146 | "merge.border": "#ffffff00", 147 | "merge.commonContentBackground": "#ffffff00", 148 | "merge.commonHeaderBackground": "#ffffff00", 149 | "merge.currentContentBackground": "#ffffff00", 150 | "merge.currentHeaderBackground": "#ffffff00", 151 | "merge.incomingContentBackground": "#ffffff00", 152 | "merge.incomingHeaderBackground": "#ffffff00", 153 | 154 | "panel.background": "#111", 155 | "panel.border": "#2D2D2D", 156 | "panelTitle.activeBorder": "#2D2D2D", 157 | "panelTitle.activeForeground": "#888", 158 | "panelTitle.inactiveForeground": "#333", 159 | 160 | "peekView.border": "#2D2D2D", 161 | "peekViewEditor.background": "#111", 162 | "peekViewEditor.matchHighlightBackground": "#333", 163 | "peekViewEditorGutter.background": "#111", 164 | "peekViewResult.background": "#111", 165 | "peekViewResult.fileForeground": "#fff", 166 | "peekViewResult.lineForeground": "#fff", 167 | "peekViewResult.matchHighlightBackground": "#333", 168 | "peekViewResult.selectionBackground": "#333", 169 | "peekViewResult.selectionForeground": "#fff", 170 | "peekViewTitle.background": "#111", 171 | "peekViewTitleDescription.foreground": "#888", 172 | "peekViewTitleLabel.foreground": "#00AEFF", 173 | 174 | "pickerGroup.border": "#333", 175 | 176 | "scrollbar.shadow": "#000", 177 | "scrollbarSlider.background": "#333", 178 | "scrollbarSlider.hoverBackground": "#333", 179 | 180 | "selection.background": "#fff", 181 | 182 | "sideBar.background": "#111", 183 | "sideBar.border": "#111", 184 | "sideBar.foreground": "#888", 185 | "sideBarSectionHeader.background": "#111", 186 | "sideBarSectionHeader.foreground": "#888", 187 | "sideBarTitle.foreground": "#888", 188 | 189 | "statusBar.background": "#111", 190 | "statusBar.border": "#111", 191 | "statusBar.debuggingBackground": "#111", 192 | "statusBar.debuggingForeground": "#666", 193 | "statusBar.foreground": "#666", 194 | "statusBar.noFolderBackground": "#111", 195 | "statusBar.noFolderForeground": "#666", 196 | "statusBarItem.activeBackground": "#09f", 197 | "statusBarItem.hoverBackground": "#333", 198 | 199 | "tab.activeBackground": "#111", 200 | "tab.activeForeground": "#fff", 201 | "tab.border": "#111", 202 | "tab.inactiveBackground": "#111", 203 | "tab.inactiveForeground": "#888", 204 | "tab.unfocusedActiveForeground": "#888", 205 | "tab.unfocusedInactiveForeground": "#888", 206 | 207 | "textBlockQuote.background": "#111", 208 | "textBlockQuote.border": "#111", 209 | "textCodeBlock.background": "#111", 210 | "textLink.activeForeground": "#00AEFF", 211 | "textPreformat.foreground": "#888", 212 | "textSeparator.foreground": "#888", 213 | "titleBar.activeBackground": "#111", 214 | "titleBar.activeForeground": "#fff", 215 | "titleBar.inactiveBackground": "#111", 216 | "titleBar.inactiveForeground": "#777", 217 | "walkThrough.embeddedEditorBackground": "#111", 218 | "welcomePage.buttonBackground": "#111", 219 | "welcomePage.buttonHoverBackground": "#333", 220 | "widget.shadow": "#000" 221 | }, 222 | "encodedTokenColors": [ 223 | { 224 | "name": "Comment", 225 | "scope": ["comment", "punctuation.definition.comment"], 226 | "settings": { 227 | "foreground": "#888" 228 | } 229 | }, 230 | { 231 | "name": "Constant", 232 | "scope": "constant", 233 | "settings": { 234 | "foreground": "#FFD14A" 235 | } 236 | }, 237 | { 238 | "name": "Entity", 239 | "scope": "entity", 240 | "settings": { 241 | "foreground": "#00AEFF" 242 | } 243 | }, 244 | { 245 | "name": "Keyword", 246 | "scope": "keyword", 247 | "settings": { 248 | "foreground": "#FFD14A" 249 | } 250 | }, 251 | { 252 | "name": "Keywords", 253 | "scope": ["keyword.control.conditional", "keyword.control.import"], 254 | "settings": { 255 | "foreground": "#FFD14A" 256 | } 257 | }, 258 | { 259 | "name": "Punctuation", 260 | "scope": "punctuation", 261 | "settings": { 262 | "foreground": "#888" 263 | } 264 | }, 265 | { 266 | "name": "Punctuation Parameters", 267 | "scope": "punctuation.definition.parameters", 268 | "settings": { 269 | "foreground": "#888" 270 | } 271 | }, 272 | { 273 | "name": "Function Parameters", 274 | "scope": [ 275 | "entity.name.variable.parameter", 276 | "meta.at-rule.function variable", 277 | "meta.at-rule.mixin variable", 278 | "meta.function.arguments", 279 | "meta.selectionset.graphql meta.arguments.graphql variable.arguments.graphql", 280 | "variable.parameter" 281 | ], 282 | "settings": { 283 | "foreground": "#fff" 284 | } 285 | }, 286 | { 287 | "name": "Punctuation Template Expression", 288 | "scope": "punctuation.definition.template-expression", 289 | "settings": { 290 | "foreground": "#FFD14A" 291 | } 292 | }, 293 | { 294 | "name": "Storage", 295 | "scope": "storage", 296 | "settings": { 297 | "foreground": "#8df" 298 | } 299 | }, 300 | { 301 | "name": "Storage Type Arrow Function", 302 | "scope": "storage.type.function.arrow", 303 | "settings": { 304 | "foreground": "#FFD14A" 305 | } 306 | }, 307 | { 308 | "name": "String", 309 | "scope": ["string", "punctuation.definition.string"], 310 | "settings": { 311 | "foreground": "#91DD64" 312 | } 313 | }, 314 | { 315 | "name": "String Template", 316 | "scope": ["string.template", "punctuation.definition.string.template"], 317 | "settings": { 318 | "foreground": "#91DD64" 319 | } 320 | }, 321 | { 322 | "name": "Support", 323 | "scope": "support", 324 | "settings": { 325 | "foreground": "#00AEFF" 326 | } 327 | }, 328 | { 329 | "name": "Support Function", 330 | "scope": ["support.function"], 331 | "settings": { 332 | "foreground": "#00AEFF" 333 | } 334 | }, 335 | { 336 | "name": "[JAVASCRIPT JSX] React Class", 337 | "scope": ["support.class.component.js"], 338 | "settings": { 339 | "foreground": "#00AEFF" 340 | } 341 | }, 342 | { 343 | "name": "Variable", 344 | "scope": "variable", 345 | "settings": { 346 | "foreground": "#fff" 347 | } 348 | }, 349 | { 350 | "name": "[CSS] - Entity", 351 | "scope": [ 352 | "source.css", 353 | "source.stylus", 354 | "source.scss", 355 | "entity.other.attribute-name.class.css" 356 | ], 357 | "settings": { 358 | "foreground": "#00AEFF" 359 | } 360 | }, 361 | { 362 | "name": "[CSS] - Hex hex", 363 | "scope": ["punctuation.definition.constant.css"], 364 | "settings": { 365 | "foreground": "#a8f" 366 | } 367 | }, 368 | { 369 | "name": "[CSS] - keys", 370 | "scope": ["support.type.property-name.css"], 371 | "settings": { 372 | "foreground": "#fff" 373 | } 374 | }, 375 | { 376 | "name": "[CSS] - Keyword", 377 | "scope": [ 378 | "source.css punctuation.definition.keyword", 379 | "source.css keyword.control" 380 | ], 381 | "settings": { 382 | "foreground": "#f6b" 383 | } 384 | }, 385 | { 386 | "name": "[CSS] - Important", 387 | "scope": "keyword.other.important.scss", 388 | "settings": { 389 | "foreground": "#f6b" 390 | } 391 | }, 392 | { 393 | "name": "[CSS] - Punctiation", 394 | "scope": ["punctuation.definition.entity.css"], 395 | "settings": { 396 | "foreground": "#00AEFF" 397 | } 398 | }, 399 | { 400 | "name": "[CSS] - ID hastags", 401 | "scope": ["entity.other.attribute-name.pseudo-element.css"], 402 | "settings": { 403 | "foreground": "#00AEFF" 404 | } 405 | }, 406 | { 407 | "name": "[CSS] - support misc function", 408 | "scope": ["support.function.misc.scss"], 409 | "settings": { 410 | "foreground": "#a8f" 411 | } 412 | }, 413 | { 414 | "name": "[CSS] - ID Selector", 415 | "scope": ["entity.other.attribute-name.id.css"], 416 | "settings": { 417 | "foreground": "#8df" 418 | } 419 | }, 420 | { 421 | "name": "[CSS] - Element Selector", 422 | "scope": "entity.name.tag.css", 423 | "settings": { 424 | "foreground": "#00AEFF" 425 | } 426 | }, 427 | { 428 | "name": "[CSS] - Support", 429 | "scope": [ 430 | "source.css support", 431 | "source.stylus support", 432 | "source.css support.constant" 433 | ], 434 | "settings": { 435 | "foreground": "#fff" 436 | } 437 | }, 438 | { 439 | "name": "[CSS] - Constant", 440 | "scope": ["source.stylus constant", "source.css constant"], 441 | "settings": { 442 | "foreground": "#a8f" 443 | } 444 | }, 445 | { 446 | "name": "[CSS] - support constant", 447 | "scope": ["support.constant.property-value.css"], 448 | "settings": { 449 | "foreground": "#FFD14A" 450 | } 451 | }, 452 | { 453 | "name": "[CSS] - units", 454 | "scope": [ 455 | "keyword.other.unit.css", 456 | "keyword.other.unit.px.css", 457 | "keyword.other.unit.percentage.css", 458 | "constant.other.color.rgb-value.hex.css", 459 | "keyword.other.unit.ms.css", 460 | "keyword.other.unit.s.css", 461 | "keyword.other.unit.vh.css", 462 | "keyword.other.unit.vw.css", 463 | "keyword.other.unit.deg.css", 464 | "keyword.other.unit.fr.css" 465 | ], 466 | "settings": { 467 | "foreground": "#a8f" 468 | } 469 | }, 470 | { 471 | "name": "[CSS] - String", 472 | "scope": [ 473 | "source.css string", 474 | "source.css punctuation.definition.string", 475 | "source.stylus string", 476 | "source.stylus punctuation.definition.string" 477 | ], 478 | "settings": { 479 | "foreground": "#98EC65" 480 | } 481 | }, 482 | { 483 | "name": "[CSS] - Variable", 484 | "scope": ["source.css variable", "source.stylus variable"], 485 | "settings": { 486 | "foreground": "#FFD14A" 487 | } 488 | }, 489 | { 490 | "name": "[HTML] - Entity", 491 | "scope": ["entity.other"], 492 | "settings": { 493 | "foreground": "#8df" 494 | } 495 | }, 496 | { 497 | "name": "[HTML] - Elements", 498 | "scope": [ 499 | "entity.name.tag.block.any.html", 500 | "entity.name.tag.inline.any.html", 501 | "entity.name.tag.structure.any.html", 502 | "entity.name.tag.html", 503 | "meta.jsx.children.tsx" 504 | ], 505 | "settings": { 506 | "foreground": "#00AAEF" 507 | } 508 | }, 509 | { 510 | "name": "[HTML] - Keywords", 511 | "scope": ["punctuation.definition.keyword", "keyword.control"], 512 | "settings": { 513 | "foreground": "#FFD14A" 514 | } 515 | }, 516 | { 517 | "name": "[HTML] - ID value", 518 | "scope": "meta.toc-list.id.html", 519 | "settings": { 520 | "foreground": "#91DD64" 521 | } 522 | }, 523 | { 524 | "name": "[HTML] - Meta tags", 525 | "scope": ["meta.tag.block.any.html", "meta.tag.inline.any.html"], 526 | "settings": { 527 | "foreground": "#00AEFF" 528 | } 529 | }, 530 | { 531 | "name": "[HTML] - Tags", 532 | "scope": [ 533 | "punctuation.definition.tag.end.html", 534 | "punctuation.definition.tag.begin.html" 535 | ], 536 | "settings": { 537 | "foreground": "#777" 538 | } 539 | }, 540 | { 541 | "name": "[HTML] - Text basic", 542 | "scope": "text.html.basic", 543 | "settings": { 544 | "foreground": "#c3c3c3" 545 | } 546 | }, 547 | { 548 | "name": "[HTML] - Quotes", 549 | "scope": 550 | "punctuation.definition.string.begin, punctuation.definition.string.end", 551 | "settings": { 552 | "foreground": "#91DD64" 553 | } 554 | }, 555 | { 556 | "name": "[JAVASCRIPT] - Numbers", 557 | "scope": "source.js constant", 558 | "settings": { 559 | "foreground": "#a8f" 560 | } 561 | }, 562 | { 563 | "name": "[JAVASCRIPT] - Keywords", 564 | "scope": "keyword.operator", 565 | "settings": { 566 | "foreground": "#f6b" 567 | } 568 | }, 569 | { 570 | "name": "[JAVASCRIPT] - Keywords", 571 | "scope": "source.js keyword", 572 | "settings": { 573 | "foreground": "#f6b" 574 | } 575 | }, 576 | 577 | { 578 | "name": "[JAVASCRIPT] - Storage Type Function", 579 | "scope": "source.js storage.type.function", 580 | "settings": { 581 | "foreground": "#fff" 582 | } 583 | }, 584 | { 585 | "name": "[JAVASCRIPT] - Keywords", 586 | "scope": [ 587 | "source.js punctuation.definition.keyword", 588 | "source.js keyword.control" 589 | ], 590 | "settings": { 591 | "foreground": "#FFD14A" 592 | } 593 | }, 594 | { 595 | "name": "[JAVASCRIPT] - Keywords", 596 | "scope": [ 597 | "source.js punctuation.definition.keyword", 598 | "source.js keyword.control" 599 | ], 600 | "settings": { 601 | "foreground": "#FFD14A" 602 | } 603 | }, 604 | { 605 | "name": "[JAVASCRIPT] - Variable Language", 606 | "scope": 607 | "variable.language, entity.name.type.class.js, meta.tag.js, entity.name.tag.js", 608 | "settings": { 609 | "foreground": "#FFD14A" 610 | } 611 | }, 612 | { 613 | "name": "[JAVASCRIPT] - Inherited Component", 614 | "scope": [ 615 | "entity.other.inherited-class.js", 616 | "variable.language.this.js", 617 | "variable.other.readwrite.alias.js", 618 | "meta.import.js" 619 | ], 620 | "settings": { 621 | "foreground": "#FFD14A" 622 | } 623 | }, 624 | { 625 | "name": "Meta", 626 | "scope": "meta", 627 | "settings": { 628 | "foreground": "#fff" 629 | } 630 | }, 631 | { 632 | "name": "Meta Brace", 633 | "scope": "meta.brace", 634 | "settings": { 635 | "foreground": "#888" 636 | } 637 | }, 638 | { 639 | "name": "Support Variable Property DOM", 640 | "scope": "support.variable.property.dom", 641 | "settings": { 642 | "foreground": "#00AEFF" 643 | } 644 | }, 645 | { 646 | "name": "[JSON] - Support", 647 | "scope": "source.json support", 648 | "settings": { 649 | "foreground": "#fff" 650 | } 651 | }, 652 | { 653 | "name": "[JSON] - String", 654 | "scope": [ 655 | "source.json string", 656 | "source.json punctuation.definition.string", 657 | "punctuation.definition.string.end.json", 658 | "punctuation.definition.string.begin.json" 659 | ], 660 | "settings": { 661 | "foreground": "#a8f" 662 | } 663 | } 664 | ] 665 | } -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | h1, h2, h3, h4 { 4 | font-weight: 400; 5 | margin: .8em 0; 6 | color: #fff; 7 | } 8 | 9 | h1 { 10 | font-size: $font-size-xxl; 11 | } 12 | 13 | h2 { 14 | font-size: $font-size-l; 15 | } 16 | 17 | h3 { 18 | font-size: $font-size-m; 19 | } 20 | 21 | h4 { 22 | font-size: $font-size-s; 23 | } 24 | 25 | .material-icon { 26 | position: relative; 27 | font-family: 'Material Icons'; 28 | font-weight: normal; 29 | font-style: normal; 30 | font-size: 24px; /* Preferred icon size */ 31 | display: inline-block; 32 | line-height: 1; 33 | text-transform: none; 34 | letter-spacing: normal; 35 | word-wrap: normal; 36 | white-space: nowrap; 37 | direction: ltr; 38 | 39 | /* Support for all WebKit browsers. */ 40 | -webkit-font-smoothing: antialiased; 41 | /* Support for Safari and Chrome. */ 42 | text-rendering: optimizeLegibility; 43 | 44 | /* Support for Firefox. */ 45 | -moz-osx-font-smoothing: grayscale; 46 | 47 | /* Support for IE. */ 48 | font-feature-settings: 'liga'; 49 | 50 | &.action { 51 | 52 | &:before { 53 | content: ''; 54 | position: absolute; 55 | top: 50%; 56 | left: 50%; 57 | width: 150%; 58 | height: 150%; 59 | border-radius: 75%; 60 | background-color: rgba(255, 255, 255, .1); 61 | transform: scale(.8) translate(-50%, -50%); 62 | opacity: 0; 63 | 64 | -webkit-transition: all .1s ease-in-out; 65 | } 66 | 67 | &:hover { 68 | color: #fff !important; 69 | 70 | &:before { 71 | transform: scale(1) translate(-50%, -50%); 72 | opacity: 1; 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/scss/_generic.scss: -------------------------------------------------------------------------------- 1 | button { 2 | background-color: $color-accent; 3 | color: #fff; 4 | border: none; 5 | font-family: inherit; 6 | font-size: $font-size-xs; 7 | padding: 10px 15px 8px; 8 | border-radius: 50px; 9 | 10 | &.secondary { 11 | background-color: #222; 12 | } 13 | 14 | &:hover { 15 | opacity: .8; 16 | } 17 | 18 | &:focus { 19 | outline: none !important; 20 | } 21 | } 22 | 23 | input { 24 | background-color: transparent; 25 | padding: 0 0 3px; 26 | border: 0; 27 | border-bottom: 1px solid rgba(255, 255, 255, .2); 28 | color: #fff; 29 | 30 | &:focus { 31 | outline: none !important; 32 | border-bottom: 1px solid #fff; 33 | } 34 | } 35 | 36 | input[type="radio"] { 37 | position: relative; 38 | width: 16px; 39 | height: 16px; 40 | border-radius: 8px; 41 | background-color: #333; 42 | border: none; 43 | margin: 0; 44 | -webkit-appearance: none; 45 | 46 | &:checked { 47 | background-color: $color-accent; 48 | } 49 | 50 | &:checked:after { 51 | content: ''; 52 | position: absolute; 53 | top: 5px; 54 | left: 5px; 55 | height: 6px; 56 | width: 6px; 57 | border-radius: 3px; 58 | background-color: #fff; 59 | } 60 | } -------------------------------------------------------------------------------- /src/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font-size: 100%; 11 | font: inherit; 12 | vertical-align: baseline; 13 | } 14 | 15 | /* HTML5 display-role reset for older browsers */ 16 | 17 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 18 | display: block; } 19 | 20 | body { 21 | line-height: 1; } 22 | 23 | ol, ul { 24 | list-style: none; } 25 | 26 | blockquote, q { 27 | quotes: none; } 28 | 29 | blockquote { 30 | &:before, &:after { 31 | content: ''; 32 | content: none; } } 33 | 34 | q { 35 | &:before, &:after { 36 | content: ''; 37 | content: none; } } 38 | 39 | table { 40 | border-collapse: collapse; 41 | border-spacing: 0; } 42 | 43 | 44 | div { 45 | box-sizing: border-box; 46 | } -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // ----- COLORS 3 | 4 | $color-accent: #4258ff; 5 | $color-text: #888; 6 | $color-divider: #111; 7 | $color-bg-sidebar: #000; 8 | $color-bg-welcome: #000; 9 | 10 | 11 | // ----- FONT SIZES 12 | 13 | $font-size-xxl: 48px; 14 | $font-size-xl: 32px; 15 | $font-size-l: 22px; 16 | $font-size-m: 18px; 17 | $font-size-s: 16px; 18 | $font-size-xs: 14px; 19 | $font-size-xxs: 12px; -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'reset'; 3 | @import 'fonts'; 4 | @import 'generic'; 5 | 6 | body { 7 | display: flex; 8 | margin: 0; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: $font-size-s; 11 | color: $color-text; 12 | height: 100%; 13 | width: 100%; 14 | 15 | #carousel, #carousel-content, #app { 16 | display: flex; 17 | flex-direction: column; 18 | width: 100%; 19 | height: 100%; 20 | 21 | .app-header { 22 | display: flex; 23 | align-items: center; 24 | height: 80px; 25 | padding: 0 20px; 26 | background-color: #000; 27 | user-select: none; 28 | border-bottom: 1px solid $color-divider; 29 | 30 | .app-project { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | width: 360px; 35 | 36 | img { 37 | height: 25px; 38 | } 39 | 40 | .project-name { 41 | margin: 0 0 0 20px; 42 | padding: 5px 0 0 20px; 43 | border-left: 1px solid rgba(255, 255, 255, .2); 44 | font-size: $font-size-m; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | } 49 | } 50 | 51 | .app-actions { 52 | display: flex; 53 | height: 100%; 54 | 55 | .action { 56 | display: flex; 57 | align-items: center; 58 | padding: 0 20px; 59 | color: #888; 60 | 61 | .icon { 62 | font-size: 18px; 63 | } 64 | 65 | .label { 66 | margin-left: 10px; 67 | padding-top: 5px; 68 | font-size: $font-size-xs; 69 | } 70 | 71 | &:hover { 72 | color: #fff; 73 | background-color: rgba(255, 255, 255, .05); 74 | } 75 | } 76 | } 77 | } 78 | 79 | .app-content { 80 | display: flex; 81 | width: 100%; 82 | height: 100%; 83 | } 84 | } 85 | } 86 | 87 | 88 | // Scrollbar Style 89 | ::-webkit-scrollbar { 90 | width: 6px; 91 | height: 6px; 92 | background-color: #131313; 93 | } 94 | 95 | ::-webkit-scrollbar-thumb { 96 | background-color: #333; 97 | } 98 | 99 | 100 | @import '../components/Sidebar'; 101 | @import '../components/Welcome'; 102 | @import '../components/Editor'; 103 | @import '../components/Popup'; 104 | @import '../components/Tabs'; -------------------------------------------------------------------------------- /src/snippets.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'Gamepad', 4 | icon: 'gamepad', 5 | jsSnippet: () => { 6 | return [ 7 | `// If you want throttled events, uncomment the line below`, 8 | `// Gamepad.throttle = true;`, 9 | ``, 10 | `Gamepad.on('gamepadevent', (e) => {`, 11 | `\t// Gamepad Right Stick Going Up`, 12 | `\tif (e.keyCode == 41) {`, 13 | `\t\tprint('Up');`, 14 | `\t}`, 15 | `\t`, 16 | `\t// Gamepad Right Stick Going Left`, 17 | `\tif (e.keyCode == 42) {`, 18 | `\t\tprint('Left');`, 19 | `\t}`, 20 | `\t`, 21 | `\t// Gamepad Right Stick Going Down`, 22 | `\tif (e.keyCode == 43) {`, 23 | `\t\tprint('Down');`, 24 | `\t}`, 25 | `\t`, 26 | `\t// Gamepad Right Stick Going Right`, 27 | `\tif (e.keyCode == 44) {`, 28 | `\t\tprint('Right');`, 29 | `\t}`, 30 | `});` 31 | ].join('\n'); 32 | }, 33 | csSnippet: () => { 34 | return [ 35 | `# If you want throttled events, uncomment the line below`, 36 | `# Gamepad.throttle = true`, 37 | ``, 38 | `Gamepad.on 'gamepadevent', (e) ->`, 39 | `\t# Gamepad Right Stick Going Up`, 40 | `\tif e.keyCode == 41`, 41 | `\t\tprint 'Up'`, 42 | ``, 43 | `\t# Gamepad Right Stick Going Left`, 44 | `\tif e.keyCode == 42`, 45 | `\t\tprint 'Left'`, 46 | ``, 47 | `\t# Gamepad Right Stick Going Down`, 48 | `\tif e.keyCode == 43`, 49 | `\t\tprint 'Down'`, 50 | ``, 51 | `\t# Gamepad Right Stick Going Right`, 52 | `\tif e.keyCode == 44`, 53 | `\t\tprint 'Right'` 54 | ].join('\n'); 55 | } 56 | }, 57 | 58 | { 59 | name: 'Focusable', 60 | icon: 'view_carousel', 61 | jsSnippet: () => { 62 | return [ 63 | `new Focusable({`, 64 | `\tparent: null, // Insert parent`, 65 | `\twidth: Grid.getWidth(5),`, 66 | `\theight: Grid.getHeight(35),`, 67 | `\tbackgroundColor: 'rgba(255, 255, 255, .2)',`, 68 | `\tfocusProperties: {`, 69 | `\t\tscale: 1.1,`, 70 | `\t\tbackgroundColor: '#fff',`, 71 | `\t\tanimationOptions: {`, 72 | `\t\t\ttime: .2`, 73 | `\t\t}`, 74 | `\t},`, 75 | `\tanimationOptions: {`, 76 | `\t\ttime: .2`, 77 | `\t}`, 78 | `});` 79 | ].join('\n'); 80 | }, 81 | csSnippet: () => { 82 | return [ 83 | `new Focusable`, 84 | `\tparent: null # Insert parent`, 85 | `\twidth: Grid.getWidth(5)`, 86 | `\theight: Grid.getHeight(35)`, 87 | `\tbackgroundColor: 'rgba(255, 255, 255, .2)'`, 88 | `\tfocusProperties:`, 89 | `\t\tscale: 1.1`, 90 | `\t\tbackgroundColor: '#fff'`, 91 | `\t\tanimationOptions:`, 92 | `\t\t\ttime: .2`, 93 | `\tanimationOptions:`, 94 | `\t\ttime: .2` 95 | ].join('\n'); 96 | } 97 | } 98 | ] 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/stores/EditorStore.ts: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx'; 2 | import * as monaco from 'monaco-editor'; 3 | 4 | import ProjectStore from './ProjectStore'; 5 | import {IProjectFile} from '../types'; 6 | 7 | export class EditorStore { 8 | @observable selectedFile: IProjectFile = null; 9 | @observable editor: monaco.editor.IStandaloneCodeEditor = null; 10 | 11 | selectFile(file: IProjectFile) { 12 | if (this.selectedFile && this.selectedFile.model) { 13 | ProjectStore.saveFile(this.selectedFile, () => { 14 | this.selectedFile = file; 15 | }); 16 | } else { 17 | this.selectedFile = file; 18 | } 19 | } 20 | 21 | pasteSnippet(snippet: string) { 22 | const line = this.editor.getPosition(); 23 | 24 | const range = new monaco.Range(line.lineNumber, line.column, line.lineNumber, line.column); 25 | const id = { major: 1, minor: 1 }; 26 | const text = snippet; 27 | const op = {identifier: id, range: range, text: text, forceMoveMarkers: true}; 28 | this.editor.executeEdits('', [op]); 29 | } 30 | } 31 | 32 | const editorStore = new EditorStore(); 33 | export default editorStore; -------------------------------------------------------------------------------- /src/stores/PopupStore.ts: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx'; 2 | 3 | export class PopupStore { 4 | @observable popupContent: JSX.Element = null; 5 | 6 | displayPopup(content: JSX.Element) { 7 | this.popupContent = content; 8 | } 9 | 10 | disposePopup() { 11 | this.popupContent = null; 12 | } 13 | } 14 | 15 | const popupStore = new PopupStore(); 16 | export default popupStore; -------------------------------------------------------------------------------- /src/stores/ProjectStore.ts: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx'; 2 | import * as monaco from 'monaco-editor'; 3 | 4 | import {remote, ipcRenderer} from 'electron'; 5 | const trackEvent = remote.getGlobal('trackEvent'); 6 | const fs = remote.require('fs'); 7 | const walk = remote.require('walk'); 8 | const path = remote.require('path'); 9 | const archiver = remote.require('archiver'); 10 | const unzip = remote.require('unzip-stream'); 11 | const temp = remote.require('temp').track(); 12 | const Store = remote.require('electron-store'); 13 | 14 | import EditorStore from './EditorStore'; 15 | 16 | import {IRecentProject, IProjectFile, ProjectFileType} from '../types'; 17 | 18 | const store = new Store(); 19 | 20 | export class ProjectStore { 21 | 22 | recentProjects: IRecentProject[] = []; 23 | 24 | @observable projectInitialized = false; 25 | @observable projectPath: string = null; 26 | @observable projectTempPath: string = null; 27 | @observable projectName: string = null; 28 | @observable projectFiles: IProjectFile[] = []; 29 | 30 | constructor() { 31 | const projects = store.get('recent-projects') || []; 32 | 33 | // Make sure that recentProjects exists on disc 34 | this.recentProjects = projects.filter((p: IRecentProject) => { 35 | return fs.existsSync(p.path); 36 | }); 37 | } 38 | 39 | createProject(projectType: string, projectPath: string, cb?: Function) { 40 | const projectTemplate: string = projectType === 'coffee' ? 41 | path.join(remote.app.getAppPath(), './dist/templates/project-coffeescript') : 42 | path.join(remote.app.getAppPath(), './dist/templates/project-javascript'); 43 | 44 | const modulesTemplate: string = path.join(remote.app.getAppPath(), './dist/templates/modules'); 45 | 46 | const output = fs.createWriteStream(projectPath); 47 | const archive = archiver('zip'); 48 | 49 | output.on('close', () => { 50 | console.log('Project Created!'); 51 | trackEvent('Project', 'Created', projectType); 52 | if (cb) cb(); 53 | this.initializeProject(projectPath); 54 | }); 55 | 56 | archive.pipe(output); 57 | archive.directory(projectTemplate, false); 58 | archive.directory(modulesTemplate, 'modules'); 59 | archive.finalize(); 60 | } 61 | 62 | initializeProject(projectPath: string) { 63 | projectPath = path.resolve(projectPath); 64 | 65 | if (!projectPath.endsWith('.crsl')) return; 66 | 67 | this.projectPath = projectPath; 68 | this.projectName = path.parse(projectPath).name; 69 | 70 | temp.mkdir(`carousel-${this.projectName}`, (err: Error, dirPath: string) => { 71 | fs.createReadStream(projectPath) 72 | .pipe(unzip.Extract({path: dirPath})) 73 | .on('close', () => { 74 | 75 | this.traverseFiles(dirPath, (files: IProjectFile[]) => { 76 | 77 | this.buildModels(files); 78 | 79 | this.projectFiles = files; 80 | this.projectTempPath = dirPath; 81 | 82 | ipcRenderer.send('start-preview', this.projectTempPath); 83 | 84 | this.projectInitialized = true; 85 | trackEvent('Project', 'Initialized', this.fileFormat); 86 | 87 | this.addToRecentProjects({ 88 | path: this.projectPath, 89 | name: this.projectName, 90 | lastOpen: Date.now() 91 | }); 92 | 93 | EditorStore.selectFile(this.mainFile); 94 | }); 95 | }); 96 | }); 97 | } 98 | 99 | traverseFiles(p: string, cb?: Function) { 100 | const walker = walk.walk(p); 101 | 102 | const files: IProjectFile[] = []; 103 | 104 | walker.on('file', (root: string, fileStats: any, next: Function) => { 105 | 106 | let type; 107 | 108 | if (fileStats.name.includes('Main.js') || fileStats.name.includes('Main.coffee')) { 109 | type = ProjectFileType.Main; 110 | } else if (fileStats.name.includes('.view.js') || fileStats.name.includes('.view.coffee')) { 111 | type = ProjectFileType.View; 112 | } else if (fileStats.name.includes('.component.js') || fileStats.name.includes('.component.coffee')) { 113 | type = ProjectFileType.Component; 114 | } else if (fileStats.name.includes('.generic.js') || fileStats.name.includes('.generic.coffee')) { 115 | type = ProjectFileType.Generic; 116 | } else if (fileStats.name.endsWith('.jpg') || fileStats.name.endsWith('.png') || fileStats.name.endsWith('.gif')) { 117 | type = ProjectFileType.Image; 118 | } else if (fileStats.name.endsWith('.JPG') || fileStats.name.endsWith('.PNG') || fileStats.name.endsWith('.GIF')) { 119 | type = ProjectFileType.Image; 120 | } 121 | 122 | if (type != null) { 123 | const projectFile: IProjectFile = { 124 | name: fileStats.name, 125 | shortName: fileStats.name.substring(0, fileStats.name.indexOf('.')), 126 | path: path.resolve(root, fileStats.name), 127 | model: null, 128 | selected: false, 129 | type: type 130 | } 131 | 132 | files.push(projectFile); 133 | } 134 | 135 | next(); 136 | }); 137 | 138 | walker.on('end', () => { 139 | cb(files); 140 | }); 141 | } 142 | 143 | buildModels(files: IProjectFile[]) { 144 | files.map((f, i) => { 145 | if (f.name.endsWith('.js')) { 146 | f.model = monaco.editor.createModel(fs.readFileSync(f.path, 'utf8'), 'javascript'); 147 | } else if (f.name.endsWith('.coffee')) { 148 | f.model = monaco.editor.createModel(fs.readFileSync(f.path, 'utf8'), 'coffeescript'); 149 | } 150 | }); 151 | } 152 | 153 | saveFile(file: IProjectFile, cb?: Function) { 154 | fs.writeFile(file.path, file.model.getValue(), (err: Error) => { 155 | if (err) return console.error(err); 156 | 157 | console.log('File Saved!'); 158 | trackEvent('Project File', 'Saved', file.path); 159 | 160 | if (cb) cb(); 161 | }); 162 | } 163 | 164 | saveProject(cb?: Function) { 165 | this.saveFile(EditorStore.selectedFile, () => { 166 | const output = fs.createWriteStream(this.projectPath); 167 | const archive = archiver('zip'); 168 | 169 | output.on('close', () => { 170 | console.log('Project Saved!'); 171 | trackEvent('Project', 'Saved', this.fileFormat); 172 | if (cb) cb(); 173 | }); 174 | 175 | archive.pipe(output); 176 | archive.directory(this.projectTempPath, false); 177 | archive.finalize(); 178 | }); 179 | } 180 | 181 | createNewFile(fileType: ProjectFileType, fileName: string, cb?: Function) { 182 | let filePath: string; 183 | let type: string; 184 | 185 | switch(fileType) { 186 | case ProjectFileType.View: 187 | filePath = `views/${fileName}.view.`; 188 | type = 'view'; 189 | break; 190 | case ProjectFileType.Component: 191 | filePath = `components/${fileName}.component.`; 192 | type = 'component'; 193 | break; 194 | case ProjectFileType.Generic: 195 | filePath = `generic/${fileName}.generic.`; 196 | type = 'generic'; 197 | break; 198 | default: 199 | filePath = `generic/${fileName}.generic.`; 200 | type = 'generic'; 201 | } 202 | 203 | filePath += this.fileFormat; 204 | 205 | fs.writeFile(path.resolve(this.projectTempPath, filePath), '', (err: Error) => { 206 | if (err) return console.error(err); 207 | 208 | const file: IProjectFile = { 209 | name: fileName + '.' + type + '.' + this.fileFormat, 210 | shortName: fileName, 211 | path: path.resolve(this.projectTempPath, filePath), 212 | model: null, 213 | selected: false, 214 | type: fileType 215 | }; 216 | 217 | if (this.fileFormat === 'js') { 218 | file.model = monaco.editor.createModel('', 'javascript'); 219 | } else if (this.fileFormat === 'coffee') { 220 | file.model = monaco.editor.createModel('', 'coffeescript'); 221 | } 222 | 223 | this.projectFiles.push(file); 224 | 225 | trackEvent('Project File', 'Created', file.path); 226 | 227 | if (cb) cb(path.resolve(this.projectTempPath, filePath)); 228 | }); 229 | } 230 | 231 | addToRecentProjects(project: IRecentProject) { 232 | let existingProject = this.recentProjects.find(p => p.path === project.path); 233 | 234 | if (existingProject) { 235 | const existingIndex = this.recentProjects.indexOf(existingProject); 236 | this.recentProjects[existingIndex] = project; 237 | 238 | this.recentProjects.sort((a: IRecentProject, b: IRecentProject) => { 239 | return b.lastOpen - a.lastOpen; 240 | }); 241 | } else { 242 | this.recentProjects.unshift(project); 243 | } 244 | 245 | store.set('recent-projects', this.recentProjects); 246 | } 247 | 248 | get fileFormat() { 249 | const mainFilePath = this.mainFile.path; 250 | return mainFilePath.slice(mainFilePath.lastIndexOf('.') + 1, mainFilePath.length); 251 | } 252 | 253 | get mainFile() { 254 | return this.projectFiles.filter(f => f.type === ProjectFileType.Main)[0]; 255 | } 256 | 257 | get viewFiles() { 258 | return this.projectFiles.filter(f => f.type === ProjectFileType.View); 259 | } 260 | 261 | get componentFiles() { 262 | return this.projectFiles.filter(f => f.type === ProjectFileType.Component); 263 | } 264 | 265 | get imageFiles() { 266 | return this.projectFiles.filter(f => f.type === ProjectFileType.Image); 267 | } 268 | 269 | get genericFiles() { 270 | return this.projectFiles.filter(f => f.type === ProjectFileType.Generic); 271 | } 272 | } 273 | 274 | const projectStore = new ProjectStore(); 275 | export default projectStore; -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import ProjectStore from './ProjectStore'; 2 | import EditorStore from './EditorStore'; 3 | import PopupStore from './PopupStore'; 4 | 5 | export { 6 | ProjectStore, 7 | EditorStore, 8 | PopupStore 9 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | 3 | 4 | 5 | // ----- PROJECT 6 | 7 | export interface IRecentProject { 8 | path: string; 9 | name: string; 10 | lastOpen: number; 11 | } 12 | 13 | export interface IRecentProjectsProps { 14 | ProjectStore?: any; 15 | } 16 | 17 | export enum ProjectFileType { 18 | Main, 19 | View, 20 | Component, 21 | Image, 22 | Generic 23 | } 24 | 25 | export interface IProjectFile { 26 | name: string; 27 | shortName: string; 28 | path: string; 29 | model: monaco.editor.IModel; 30 | selected: boolean; 31 | type: ProjectFileType; 32 | } 33 | 34 | 35 | // ----- EDITOR 36 | 37 | export interface IEditorProps { 38 | EditorStore?: any; 39 | ProjectStore?: any; 40 | value?: string; 41 | language?: string; 42 | uri?: monaco.Uri; 43 | options?: monaco.editor.IEditorConstructionOptions; 44 | onValueChange?(value: string): void; 45 | } 46 | 47 | 48 | // ----- SIDEBAR 49 | 50 | export interface IProjectFileCategoryHeaderProps { 51 | title: string; 52 | type: ProjectFileType; 53 | PopupStore?: any; 54 | } 55 | 56 | export interface IProjectFileItemProps { 57 | file: IProjectFile; 58 | selected: boolean; 59 | EditorStore?: any; 60 | } 61 | 62 | export interface ISidebarProps { 63 | ProjectStore?: any; 64 | EditorStore?: any; 65 | } 66 | 67 | 68 | // ----- BUTTON 69 | 70 | export interface IButtonProps { 71 | text: string; 72 | secondary?: boolean; 73 | onClick?(): void; 74 | } 75 | 76 | 77 | // ----- ICON 78 | 79 | export interface IIconProps { 80 | name: string; 81 | size?: number; 82 | color?: string; 83 | onClick?(): void; 84 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "lib": ["es2015", "dom"], 9 | "jsx": "react", 10 | "declaration": true, 11 | "declarationDir": "./dist/types", 12 | "experimentalDecorators": true 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "dist" 20 | ] 21 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: ['webpack/hot/dev-server', './src/index.tsx'] 7 | }, 8 | output: { 9 | filename: 'bundle.js', 10 | }, 11 | mode: 'development', 12 | devtool: 'source-map', 13 | target: 'electron-renderer', 14 | resolve: { 15 | extensions: ['.ts', '.tsx', '.js', '.json'] 16 | }, 17 | devServer: { 18 | contentBase: './dist', 19 | port: 8000 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | loader: 'awesome-typescript-loader' 26 | }, 27 | { 28 | test: /\.css?$/, 29 | use: ['style-loader', 'css-loader'] 30 | }, 31 | { 32 | test: /\.scss$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader' 39 | }, 40 | { 41 | loader: 'sass-loader' 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new webpack.optimize.LimitChunkCountPlugin({ 49 | maxChunks: 1 50 | }), 51 | new webpack.HotModuleReplacementPlugin, 52 | new MonacoWebpackPlugin() 53 | ] 54 | } --------------------------------------------------------------------------------