├── .editorconfig ├── .gitignore ├── README.md ├── firebase.json ├── gulpfile.js ├── package.json ├── src ├── app-shell.js ├── app │ └── app.ts ├── components │ ├── About.ts │ ├── Home.ts │ ├── header.ts │ ├── sidenav.ts │ └── toast.ts ├── css │ ├── core.scss │ └── core │ │ ├── _card.scss │ │ ├── _colors.scss │ │ ├── _core.scss │ │ ├── _dialog.scss │ │ ├── _header.scss │ │ ├── _loader.scss │ │ ├── _main.scss │ │ ├── _side-nav.scss │ │ ├── _toast.scss │ │ └── _z-index.scss ├── images │ ├── apple-touch-icon.png │ ├── chrome-splashscreen-icon-384x384.png │ ├── chrome-touch-icon-192x192.png │ ├── ic_add_24px.svg │ ├── ic_info_outline_24px.svg │ ├── ic_menu_24px.svg │ ├── icon-128x128.png │ ├── ms-touch-icon-144x144-precomposed.png │ └── side-nav-bg@2x.jpg ├── index.html ├── main.ts ├── ng2-service-worker.ts ├── services │ ├── Auth.ts │ ├── Backend.ts │ └── Nav.ts └── util │ └── styleloader.js ├── system.config.js ├── tasks ├── build.ts ├── compile.ts ├── copy.ts └── styles.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng2-application-shell 2 | 3 | Port of the Chrome Team's [Application Shell](https://github.com/GoogleChrome/application-shell) architecture to Angular2. 4 | 5 | Work in Progress! 6 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "ng2-forum-demo", 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [{ 10 | "source": "**", 11 | "destination": "/index.html" 12 | }] 13 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var tsregister = require('ts-node/register'); 3 | var tsconfig = require('./tsconfig.json'); 4 | 5 | var config = { 6 | index: 'src/index.html', 7 | css:['src/**/*.scss'], 8 | staticFiles: [ 9 | 'src/**/*', 10 | '!src/**/*.ts', 11 | 'node_modules/systemjs/dist/system.js', 12 | 'node_modules/angular2/bundles/angular2-polyfills.js' 13 | ], 14 | dist: './dist', 15 | system: { 16 | configFile: 'system.config.js' 17 | } 18 | }; 19 | 20 | require('./tasks/build').build(gulp, config); 21 | require('./tasks/copy').copy(gulp, config); 22 | require('./tasks/styles').compile(gulp, config); 23 | require('./tasks/compile').compile(gulp, config); 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-application-shell", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "angular2": "^2.0.0-alpha.52", 13 | "es6-promise": "^3.0.2", 14 | "es6-shim": "^0.33.13", 15 | "firebase": "^2.3.2", 16 | "reflect-metadata": "^0.1.2", 17 | "rxjs": "^5.0.0-alpha.14", 18 | "zone.js": "^0.5.8" 19 | }, 20 | "devDependencies": { 21 | "gulp": "^3.9.0", 22 | "gulp-inline": "^0.1.0", 23 | "gulp-minify-css": "^1.2.2", 24 | "gulp-sass": "^2.1.1", 25 | "gulp-util": "^3.0.7", 26 | "systemjs": "^0.19.6", 27 | "systemjs-builder": "^0.14.11", 28 | "ts-node": "^0.5.4", 29 | "typescript": "^1.7.3", 30 | "typescript-node": "^0.1.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app-shell.js: -------------------------------------------------------------------------------- 1 | //TODO: service worker stuffs 2 | 3 | System.config({ 4 | map: { 5 | app: 'src/main.ts' 6 | } 7 | }); 8 | 9 | System.import('app').catch(function(err){ 10 | console.log(err); 11 | }); -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | //ng2 deps 2 | import {Component} from 'angular2/core'; 3 | import {Router, Route, RouteConfig, AsyncRoute, ROUTER_DIRECTIVES} from 'angular2/router'; 4 | 5 | //app services 6 | import {AuthService} from '../services/Auth'; 7 | import {Nav} from '../services/Nav'; 8 | import {Backend, BackendConfig} from '../services/Backend'; 9 | 10 | //generic components 11 | import {AppHeader} from '../components/header'; 12 | import {SideNav} from '../components/sidenav'; 13 | import {Toast} from '../components/toast'; 14 | 15 | //routable components 16 | import {Home} from '../components/Home'; 17 | import {About} from '../components/About'; 18 | 19 | @Component({ 20 | selector: 'app', 21 | providers: [Nav], 22 | directives: [AppHeader, SideNav, Toast, ROUTER_DIRECTIVES], 23 | template: ` 24 | 25 |
26 | 27 |
28 | 29 | `, 30 | }) 31 | @RouteConfig([ 32 | new Route({ name: 'Home', component: Home, path: '/home', useAsDefault: true }), 33 | new Route({ name: 'About', component: About, path: '/about' }) 34 | ]) 35 | export class App { 36 | constructor(public backend: Backend, public nav: Nav) { } 37 | 38 | authenticate() { 39 | this.backend.authenticate() 40 | } 41 | showToast(message) { } 42 | } -------------------------------------------------------------------------------- /src/components/About.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {Nav} from '../services/Nav'; 3 | 4 | @Component({ 5 | selector: 'about', 6 | template: ` 7 |
8 |

About

9 | 10 |
11 | ` 12 | }) 13 | export class About { 14 | constructor(){ 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/Home.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {Nav} from '../services/Nav'; 3 | 4 | @Component({ 5 | selector: 'home', 6 | template: ` 7 |
8 |

Hello World

9 | 10 | 11 |
12 |
13 |

Hello World

14 | 15 | 16 |
17 |
18 |

Hello World

19 | 20 | 21 |
22 |
23 |

Hello World

24 | 25 | 26 |
27 |
28 |

Hello World

29 | 30 | 31 |
32 |
33 |

Hello World

34 | 35 | 36 |
37 |
38 |

Hello World

39 | 40 | 41 |
42 |
43 |

Hello World

44 | 45 | 46 |
47 |
48 |

Hello World

49 | 50 | 51 |
52 |
53 |

Hello World

54 | 55 | 56 |
57 |
58 |

Hello World

59 | 60 | 61 |
62 |
63 |

Hello World

64 | 65 | 66 |
67 | 68 | ` 69 | }) 70 | export class Home { 71 | constructor(private nav: Nav){ 72 | 73 | } 74 | openSideNav(){ 75 | this.nav.open() 76 | } 77 | } -------------------------------------------------------------------------------- /src/components/header.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {Nav} from '../services/Nav'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | template: ` 7 | toggle_nav 8 | 9 |

NG2 App Shell

10 | ` 11 | }) 12 | export class AppHeader { 13 | constructor(private nav: Nav){ 14 | 15 | } 16 | openSideNav(){ 17 | this.nav.open() 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/sidenav.ts: -------------------------------------------------------------------------------- 1 | import {Component, Directive, Output, Input, EventEmitter, Renderer, ElementRef} from 'angular2/core'; 2 | import {App} from '../app/app'; 3 | import {Nav} from '../services/Nav'; 4 | import {ReplaySubject} from 'rxjs/subject/ReplaySubject'; 5 | import {ROUTER_DIRECTIVES} from 'angular2/router'; 6 | 7 | @Directive({ 8 | selector: '[side-nav-content]', 9 | host: { 10 | '(transitionend)': 'cleanup()' 11 | } 12 | }) 13 | class SideNavContent { 14 | @Input() open: boolean; 15 | 16 | constructor(private el: ElementRef, private renderer: Renderer){ 17 | 18 | } 19 | ngOnChanges(changes:any){ 20 | if(changes.open.currentValue && changes.open.previousValue !== undefined){ 21 | this.setOpen() 22 | } 23 | else{ 24 | this.setClosed() 25 | } 26 | } 27 | setOpen(){ 28 | this.renderer.setElementClass(this.el, 'side-nav__content--animatable', true); 29 | this.renderer.setElementStyle(this.el, 'transform', 'translateX(0px)'); 30 | } 31 | setClosed(){ 32 | this.renderer.setElementClass(this.el, 'side-nav__content--animatable', true); 33 | this.renderer.setElementStyle(this.el, 'transform', 'translateX(-102%)'); 34 | } 35 | cleanup(){ 36 | this.renderer.setElementClass(this.el, 'side-nav__content--animatable', false); 37 | } 38 | 39 | } 40 | 41 | @Component({ 42 | selector: 'side-nav', 43 | directives: [SideNavContent, ROUTER_DIRECTIVES], 44 | template: ` 45 |
46 |
47 |
48 |

App shell

49 |
50 | 51 |
52 | Index 53 | About 54 | URL 2 55 |
56 | 57 |
Version @VERSION@
58 |
59 |
60 | ` 61 | }) 62 | export class SideNav { 63 | @Output() toggle: EventEmitter = new EventEmitter(); 64 | isVisible: ReplaySubject; 65 | constructor(public nav: Nav){ 66 | this.isVisible = nav.isOpen; 67 | } 68 | } -------------------------------------------------------------------------------- /src/components/toast.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | 3 | @Component({ 4 | selector: 'toast-view', 5 | template: ` 6 | 7 | ` 8 | }) 9 | export class Toast { 10 | visible = false; 11 | show(message){ 12 | this.message = message; 13 | this.visible = true; 14 | setTimeout(() => { 15 | this.visible = false; 16 | },3000); 17 | } 18 | } -------------------------------------------------------------------------------- /src/css/core.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2015 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | @import 'core/_colors'; 19 | @import 'core/_card'; 20 | @import 'core/_core'; 21 | @import 'core/_dialog'; 22 | @import 'core/_side-nav'; 23 | @import 'core/_main'; 24 | @import 'core/_header'; 25 | @import 'core/_loader'; 26 | @import 'core/_z-index'; 27 | @import 'core/_toast'; 28 | -------------------------------------------------------------------------------- /src/css/core/_card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 16px; 3 | position: relative; 4 | box-sizing: border-box; 5 | background: #fff; 6 | border-radius: 2px; 7 | margin: 16px; 8 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 9 | 0 3px 1px -2px rgba(0,0,0,.2), 10 | 0 1px 5px 0 rgba(0,0,0,.12); 11 | } 12 | -------------------------------------------------------------------------------- /src/css/core/_colors.scss: -------------------------------------------------------------------------------- 1 | $primary: #F44336; 2 | $primaryLight: #FFCDD2; 3 | $primaryDark: #D32F2F; 4 | $secondary: #448AFF; 5 | $secondaryDark: #303F9F; 6 | $toast: #404040; 7 | $background: #FAFAFA; -------------------------------------------------------------------------------- /src/css/core/_core.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | padding: 0; 7 | margin: 0; 8 | height: 100%; 9 | width: 100%; 10 | font-family: 'Helvetica', 'Verdana', sans-serif; 11 | font-weight: 400; 12 | // Not implemented yet but is a nice solution for async loading fonts 13 | font-display: optional; 14 | color: #444; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | html { 20 | overflow: hidden; 21 | } 22 | 23 | body { 24 | display: flex; 25 | flex-direction: column; 26 | flex-wrap: nowrap; 27 | justify-content: flex-start; 28 | align-items: stretch; 29 | align-content: stretch; 30 | background: #ececec; 31 | } 32 | 33 | body:after { 34 | content: ''; 35 | position: fixed; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 100%; 40 | pointer-events: none; 41 | background: $background; 42 | opacity: 0; 43 | will-change: opacity; 44 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1) 0.4s; 45 | } 46 | 47 | h1, h2, h3, h4, h5, h6 { 48 | font-family: 'Roboto', 'Helvetica', 'Verdana', sans-serif; 49 | } 50 | 51 | .app-deeplink:after { 52 | opacity: 1; 53 | pointer-events: auto; 54 | } 55 | 56 | a { 57 | color: $secondary; 58 | } 59 | 60 | .is-hidden { 61 | display: none; 62 | } 63 | 64 | button::-moz-focus-inner { 65 | border: 0; 66 | } 67 | 68 | @media(min-width: 600px) { 69 | .view-underpanel { 70 | top: 0; 71 | right: 0; 72 | position: fixed; 73 | width: 400px; 74 | height: 100%; 75 | overflow: hidden; 76 | pointer-events: none; 77 | } 78 | 79 | .view-underpanel__block { 80 | position: absolute; 81 | top: 0; 82 | right: 0; 83 | width: 360px; 84 | height: 100%; 85 | background: $background; 86 | box-shadow: 0 0 14px rgba(0,0,0,.24), 87 | 0 14px 28px rgba(0,0,0,.48); 88 | transform: translateX(105%); 89 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.04s, 90 | opacity 0.213s cubic-bezier(0,0,0.21,1) 0.04s; 91 | will-change: transform; 92 | opacity: 0; 93 | } 94 | 95 | .view-underpanel__block:after { 96 | content: ''; 97 | height: 144px; 98 | width: 100%; 99 | display: block; 100 | background: $primaryDark; 101 | position: absolute; 102 | top: 0; 103 | left: 0; 104 | } 105 | 106 | .view-underpanel--visible .view-underpanel__block, 107 | .view-underpanel--locked .view-underpanel__block { 108 | opacity: 1; 109 | transform: translateX(0); 110 | } 111 | } 112 | 113 | @media(min-width: 960px) { 114 | .view-underpanel { 115 | margin-top: 56px; 116 | left: 46%; 117 | right: auto; 118 | position: fixed; 119 | width: 520px; 120 | height: 100%; 121 | overflow: hidden; 122 | pointer-events: none; 123 | } 124 | 125 | .view-underpanel__block { 126 | opacity: 0.0001; 127 | width: 504px; 128 | left: 8px; 129 | transform: translateY(50px); 130 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.04s, 131 | opacity 0.213s cubic-bezier(0,0,0.21,1) 0.04s; 132 | 133 | box-shadow: 0 0 6px rgba(0,0,0,.16), 134 | 0 6px 12px rgba(0,0,0,.32); 135 | } 136 | 137 | .view-underpanel__block:after { 138 | height: 288px; 139 | } 140 | 141 | .view-underpanel--visible .view-underpanel__block, 142 | .view-underpanel--locked .view-underpanel__block { 143 | opacity: 1; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/css/core/_dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog-view { 2 | background: rgba(0,0,0,0.57); 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100%; 7 | height: 100%; 8 | opacity: 0; 9 | pointer-events: none; 10 | will-change: opacity; 11 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 12 | } 13 | 14 | .dialog-view--visible { 15 | opacity: 1; 16 | pointer-events: auto; 17 | } 18 | 19 | .dialog-view__panel { 20 | background: #FFF; 21 | border-radius: 2px; 22 | box-shadow: 0 0 14px rgba(0,0,0,.24), 23 | 0 14px 28px rgba(0,0,0,.48); 24 | min-width: 280px; 25 | position: absolute; 26 | left: 50%; 27 | top: 50%; 28 | transform: translate(-50%, -50%) translateY(30px); 29 | transition: transform 0.333s cubic-bezier(0,0,0.21,1) 0.05s; 30 | } 31 | 32 | .dialog-view--visible .dialog-view__panel { 33 | transform: translate(-50%, -50%); 34 | } 35 | 36 | .dialog-view__panel-header { 37 | padding: 24px; 38 | } 39 | 40 | .dialog-view__panel-footer { 41 | padding: 8px; 42 | text-align: right; 43 | } 44 | 45 | .dialog-view__panel-button { 46 | height: 36px; 47 | line-height: 1; 48 | text-transform: uppercase; 49 | color: $secondary; 50 | font-size: 15px; 51 | font-weight: 500; 52 | background: none; 53 | border: none; 54 | padding: 0 8px; 55 | } 56 | 57 | .dialog-view__panel-title { 58 | line-height: 32px; 59 | font-size: 24px; 60 | color: #000; 61 | opacity: 0.87; 62 | font-weight: 500; 63 | margin: 0; 64 | } 65 | 66 | .dialog-view__panel-message { 67 | font-size: 16px; 68 | line-height: 24px; 69 | margin: 20px 0 0 0; 70 | opacity: 0.54; 71 | } 72 | -------------------------------------------------------------------------------- /src/css/core/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | height: 56px; 4 | color: #FFF; 5 | background: $primary; 6 | position: fixed; 7 | font-size: 20px; 8 | box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 9 | 0 2px 9px 1px rgba(0, 0, 0, 0.12), 10 | 0 4px 2px -2px rgba(0, 0, 0, 0.2); 11 | padding: 16px 16px 0 16px; 12 | will-change: transform; 13 | 14 | display: flex; 15 | flex-direction: row; 16 | flex-wrap: nowrap; 17 | justify-content: flex-start; 18 | align-items: stretch; 19 | align-content: center; 20 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.1s; 21 | } 22 | 23 | .header--collapsed { 24 | transition: transform 0.233s cubic-bezier(0,0,0.21,1) 0.13s; 25 | transform: translateY(-56px); 26 | } 27 | 28 | .header__menu { 29 | // background: url(/images/ic_menu_24px.svg) center center no-repeat; 30 | width: 24px; 31 | height: 24px; 32 | margin-right: 16px; 33 | text-indent: -30000px; 34 | overflow: hidden; 35 | opacity: 0.54; 36 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 37 | border: none; 38 | outline: none; 39 | } 40 | 41 | .header__menu:focus, 42 | .header__menu:hover { 43 | opacity: 1; 44 | border: 1px solid white; 45 | } 46 | 47 | .header__title { 48 | font-weight: 400; 49 | font-size: 20px; 50 | margin: 0; 51 | flex: 1; 52 | } -------------------------------------------------------------------------------- /src/css/core/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | left: 50%; 3 | top: 50%; 4 | position: fixed; 5 | transform: translate(-50%, -50%); 6 | 7 | #spinner { 8 | box-sizing: border-box; 9 | stroke: #673AB7; 10 | stroke-width: 3px; 11 | transform-origin: 50%; 12 | 13 | animation: line 1.6s cubic-bezier(0.4, 0.0, 0.2, 1) infinite, 14 | rotate 1.6s linear infinite; 15 | } 16 | 17 | @keyframes rotate { 18 | 19 | from { 20 | transform: rotate(0) 21 | } 22 | 23 | to { 24 | transform: rotate(450deg); 25 | } 26 | } 27 | 28 | @keyframes line { 29 | 0% { 30 | stroke-dasharray: 2, 85.964; 31 | transform: rotate(0); 32 | } 33 | 34 | 50% { 35 | stroke-dasharray: 65.973, 21.9911; 36 | stroke-dashoffset: 0; 37 | } 38 | 39 | 100% { 40 | stroke-dasharray: 2, 85.964; 41 | stroke-dashoffset: -65.973; 42 | transform: rotate(90deg); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/css/core/_main.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | padding-top: 72px; 3 | flex: 1; 4 | overflow-x: hidden; 5 | overflow-y: auto; 6 | -webkit-overflow-scrolling: touch; 7 | } 8 | 9 | app { 10 | overflow-y: auto; 11 | } 12 | 13 | .superfail .main { 14 | background: url(/images/superfail.svg) center center no-repeat; 15 | } 16 | 17 | .empty-set-cta { 18 | color: $primary; 19 | font-size: 20px; 20 | position: fixed; 21 | left: 0; 22 | top: 0; 23 | width: 100%; 24 | height: 100%; 25 | background: $background; 26 | opacity: 0; 27 | will-change: opacity; 28 | pointer-events: none; 29 | transition: opacity 0.333s cubic-bezier(0,0,0.21,1); 30 | 31 | display: flex; 32 | flex-direction: column; 33 | flex-wrap: nowrap; 34 | justify-content: center; 35 | align-content: center; 36 | align-items: center; 37 | } 38 | 39 | .empty-set-cta--visible { 40 | opacity: 1; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/core/_side-nav.scss: -------------------------------------------------------------------------------- 1 | .side-nav { 2 | width: 100%; 3 | height: 100%; 4 | position: fixed; 5 | pointer-events: none; 6 | top: 0; 7 | left: 0; 8 | overflow: hidden; 9 | } 10 | 11 | .side-nav:before { 12 | content: ''; 13 | width: 100%; 14 | height: 100%; 15 | background: #000; 16 | opacity: 0; 17 | display: block; 18 | position: absolute; 19 | will-change: opacity; 20 | transition: opacity 0.233s cubic-bezier(0,0,0.21,1); 21 | } 22 | 23 | .side-nav--visible { 24 | pointer-events: auto; 25 | } 26 | 27 | .side-nav--visible:before { 28 | opacity: 0.7; 29 | } 30 | 31 | .side-nav__content { 32 | background: #FAFAFA; 33 | width: 80%; 34 | max-width: 304px; 35 | height: 100%; 36 | overflow: hidden; 37 | position: relative; 38 | 39 | box-shadow: 0 0 4px rgba(0, 0, 0, .14), 40 | 0 4px 8px rgba(0, 0, 0, .28); 41 | 42 | will-change: transform; 43 | transform: translateX(-102%); 44 | 45 | &.side-nav--visible { 46 | transform: translateX(0px); 47 | } 48 | } 49 | 50 | 51 | .side-nav__content--animatable { 52 | transition: transform 0.233s cubic-bezier(0,0,0.21,1); 53 | } 54 | 55 | .side-nav__header { 56 | background: $primary url(/images/side-nav-bg@2x.jpg); 57 | background-size: cover; 58 | width: 100%; 59 | height: 171px; 60 | position: relative; 61 | } 62 | 63 | .side-nav__title { 64 | font-size: 16px; 65 | line-height: 1; 66 | color: #FFF; 67 | position: absolute; 68 | bottom: 8px; 69 | left: 16px; 70 | height: 16px; 71 | font-weight: 500; 72 | } 73 | 74 | .side-nav__body { 75 | padding-top: 8px; 76 | } 77 | 78 | .side-nav__version { 79 | position: absolute; 80 | bottom: 16px; 81 | left: 16px; 82 | font-size: 14px; 83 | opacity: 0.54; 84 | } 85 | 86 | .side-nav__delete-memos, 87 | .side-nav__delete-all, 88 | .side-nav__blog-post, 89 | .side-nav__file-bug-report { 90 | font-family: 'Roboto'; 91 | font-size: 14px; 92 | outline: none; 93 | height: 48px; 94 | padding-left: 72px; 95 | width: 100%; 96 | text-align: left; 97 | display: block; 98 | border: none; 99 | background: url(/images/ic_delete_24px.svg) 16px 12px no-repeat; 100 | color: rgba(0,0,0,0.87); 101 | cursor: pointer; 102 | } 103 | 104 | .side-nav__delete-all { 105 | background-image: url(/images/ic_restore_24px.svg); 106 | } 107 | 108 | .side-nav__blog-post { 109 | background-image: url(/images/ic_info_outline_24px.svg); 110 | line-height: 48px; 111 | text-decoration: none; 112 | } 113 | 114 | .side-nav__blog-post:focus { 115 | background-color: #eee; 116 | outline: 0; 117 | } 118 | 119 | .side-nav__file-bug-report { 120 | background-image: url(/images/ic_feedback_24px.svg); 121 | line-height: 48px; 122 | text-decoration: none; 123 | } 124 | 125 | .side-nav__blog-post.active { 126 | font-weight: bold; 127 | background-color: rgba(0,0,0,.05); 128 | } 129 | -------------------------------------------------------------------------------- /src/css/core/_toast.scss: -------------------------------------------------------------------------------- 1 | .toast-view { 2 | background-color: $toast; 3 | border-radius: 3px; 4 | box-shadow: 0 0 2px rgba(0,0,0,.12), 5 | 0 2px 4px rgba(0,0,0,.24); 6 | color: #fff; 7 | line-height: 20px; 8 | margin-top: 8px; 9 | padding: 16px; 10 | transition: opacity 200ms, 11 | transform 300ms cubic-bezier(0.165,0.840,0.440,1.000); 12 | white-space: nowrap; 13 | opacity: 0; 14 | transform: translateY(20px); 15 | will-change: transform; 16 | position: fixed; 17 | left: 16px; 18 | bottom: 16px; 19 | } 20 | 21 | .toast-view--visible { 22 | opacity: 1; 23 | transform: translateY(0); 24 | } 25 | -------------------------------------------------------------------------------- /src/css/core/_z-index.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | z-index: 2; 3 | } 4 | 5 | .main { 6 | z-index: 0; 7 | } 8 | 9 | .new-appshelling-btn { 10 | z-index: 3; 11 | } 12 | 13 | .view-underpanel { 14 | z-index: 4; 15 | } 16 | 17 | .details-view { 18 | z-index: 6; 19 | } 20 | 21 | .edit-view__circular-reveal-container { 22 | z-index: 10; 23 | } 24 | 25 | .appshell-view { 26 | z-index: 5; 27 | } 28 | 29 | .details-view__box-reveal { 30 | z-index: 5; 31 | } 32 | 33 | .details-view__panel { 34 | z-index: 6; 35 | } 36 | 37 | .details-view__playback { 38 | z-index: 1; 39 | } 40 | 41 | .edit-view__panel { 42 | z-index: 7; 43 | } 44 | 45 | body:after { 46 | z-index: 100; 47 | } 48 | 49 | .side-nav { 50 | z-index: 200; 51 | } 52 | 53 | .dialog-view { 54 | z-index: 250; 55 | } 56 | 57 | .toast-view { 58 | z-index: 300; 59 | } 60 | 61 | .loader { 62 | z-index: 400; 63 | } 64 | -------------------------------------------------------------------------------- /src/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/apple-touch-icon.png -------------------------------------------------------------------------------- /src/images/chrome-splashscreen-icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/chrome-splashscreen-icon-384x384.png -------------------------------------------------------------------------------- /src/images/chrome-touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/chrome-touch-icon-192x192.png -------------------------------------------------------------------------------- /src/images/ic_add_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/ic_info_outline_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/ic_menu_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/icon-128x128.png -------------------------------------------------------------------------------- /src/images/ms-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/ms-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /src/images/side-nav-bg@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ng2-application-shell/f1e051c5bccb5a58a1f10f07d2cc67a0eff028d3/src/images/side-nav-bg@2x.jpg -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NG2 Application Shell 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | toggle_nav 22 | 23 |

NG2 App Shell

24 |
25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from 'angular2/platform/browser'; 2 | import {platform, provide} from 'angular2/core'; 3 | import {ROUTER_PROVIDERS} from 'angular2/router'; 4 | 5 | import {AuthService} from './services/Auth'; 6 | import {Backend, BackendConfig} from './services/Backend'; 7 | import {Nav} from './services/Nav'; 8 | 9 | const FIREBASE_URL = 'https://ng2-forum-demo.firebaseio.com'; 10 | 11 | import {App} from './app/app'; 12 | 13 | bootstrap(App,[ 14 | ROUTER_PROVIDERS, 15 | AuthService, 16 | Backend, 17 | provide(BackendConfig, {useValue: {url: FIREBASE_URL }}) 18 | ]); 19 | 20 | -------------------------------------------------------------------------------- /src/ng2-service-worker.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Injector, Injectable, provide} from 'angular2/src/core/di'; 3 | 4 | class SWLogger { 5 | log(...args: any[]) { 6 | console.log.apply(console, ['[service-worker]'].concat(args)); 7 | } 8 | } 9 | 10 | 11 | const SW_EVENTS = { 12 | INSTALL: 'install', 13 | FETCH: 'fetch', 14 | ACTIVATE: 'activate' 15 | } 16 | 17 | class SWContext { 18 | addEventListener(event, handler) { } 19 | } 20 | 21 | 22 | @Injectable() 23 | class NgServiceWorker { 24 | constructor(serviceWorkerContext: SWContext, private _logger: SWLogger) { 25 | serviceWorkerContext.addEventListener(SW_EVENTS.ACTIVATE, (ev) => this.onActivate(ev)); 26 | serviceWorkerContext.addEventListener(SW_EVENTS.INSTALL, (ev) => this.onInstall(ev)); 27 | serviceWorkerContext.addEventListener(SW_EVENTS.FETCH, (ev) => this.onFetch(ev)); 28 | } 29 | 30 | bootstrap() { 31 | this._logger.log('bootstrap') 32 | } 33 | 34 | onInstall(installEvent) { 35 | installEvent.waitUntil(this._onInstall()); 36 | } 37 | 38 | private _onInstall() { 39 | return Promise.resolve('ready'); 40 | } 41 | 42 | onActivate(activateEvent) { 43 | this._logger.log('activate', activateEvent); 44 | } 45 | onFetch(fetchEvent) { 46 | this._logger.log('fetch', fetchEvent.request.url); 47 | } 48 | } 49 | 50 | 51 | 52 | Injector 53 | .resolveAndCreate([ 54 | SWLogger, 55 | provide(SWContext, { useValue: self }), 56 | NgServiceWorker, ]) 57 | .get(NgServiceWorker) 58 | .bootstrap(); -------------------------------------------------------------------------------- /src/services/Auth.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, Inject} from 'angular2/core'; 2 | import {Backend} from './Backend'; 3 | 4 | @Injectable() 5 | export class AuthService { 6 | constructor(private backend: Backend){ 7 | 8 | } 9 | authenticate(){ 10 | this.backend.authenticate() 11 | } 12 | } -------------------------------------------------------------------------------- /src/services/Backend.ts: -------------------------------------------------------------------------------- 1 | import Firebase from 'firebase'; 2 | import {Injectable} from 'angular2/core'; 3 | import {Observable} from 'rxjs/Observable'; 4 | import {ReplaySubject} from 'rxjs/subject/ReplaySubject'; 5 | 6 | export class BackendConfig { 7 | url: string; 8 | } 9 | 10 | 11 | @Injectable() 12 | export class Backend { 13 | authState: ReplaySubject = new ReplaySubject(1); 14 | ref: Firebase; 15 | constructor(config: BackendConfig){ 16 | this.ref = new Firebase(config.url); 17 | } 18 | authenticate(){ 19 | let authRequest = new Observable(obs => { 20 | 21 | this.ref.authWithOAuthPopup('github', (err, res) => { 22 | if(err){ 23 | obs.error(err); 24 | } 25 | else { 26 | obs.next(res); 27 | } 28 | }) 29 | 30 | }); 31 | 32 | authRequest.subscribe(this.authState); 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/services/Nav.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'angular2/core'; 2 | import {ReplaySubject} from 'rxjs/subject/ReplaySubject'; 3 | 4 | @Injectable() 5 | export class Nav { 6 | isOpen: ReplaySubject = new ReplaySubject(1); 7 | 8 | open(){ 9 | this.isOpen.next(true); 10 | } 11 | close(){ 12 | this.isOpen.next(false); 13 | } 14 | toggle(){ 15 | this.isOpen.map(open => !open).take(1).subscribe(this.isOpen); 16 | } 17 | } -------------------------------------------------------------------------------- /src/util/styleloader.js: -------------------------------------------------------------------------------- 1 | var remoteStyles = ['https://fonts.googleapis.com/css?family=Roboto:400,300,700,500,400italic'] 2 | 3 | window.requestAnimationFrame(function() { 4 | var elementToInsertLinkBefore = 5 | document.getElementsByTagName('script')[0]; 6 | for (var i = 0; i < remoteStyles.length; i++) { 7 | var linkElement = document.createElement('link'); 8 | linkElement.rel = 'stylesheet'; 9 | linkElement.media = 'all'; 10 | linkElement.href = remoteStyles[i]; 11 | 12 | elementToInsertLinkBefore.parentNode.insertBefore(linkElement, 13 | elementToInsertLinkBefore); 14 | } 15 | }); -------------------------------------------------------------------------------- /system.config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | transpiler: 'typescript', 3 | typescriptOptions: { 4 | emitDecoratorMetadata: true 5 | }, 6 | map: { 7 | typescript: 'node_modules/typescript/lib/typescript.js', 8 | app: './src', 9 | angular2: 'node_modules/angular2', 10 | rxjs: 'node_modules/rxjs', 11 | 'reflect-metadata': 'node_modules/reflect-metadata/temp/Reflect.js', 12 | 'zone.js': 'node_modules/zone.js/dist/zone.js', 13 | firebase: 'node_modules/firebase/lib/firebase-web.js' 14 | }, 15 | packages: { 16 | app: { 17 | defaultExtension: 'ts', 18 | main: 'main.ts' 19 | }, 20 | angular2: { 21 | defaultExtension: 'js', 22 | }, 23 | rxjs: { 24 | defaultExtension: 'js' 25 | }, 26 | 'reflect-metadata': { 27 | format: 'global' 28 | } 29 | } 30 | }); -------------------------------------------------------------------------------- /tasks/build.ts: -------------------------------------------------------------------------------- 1 | declare var require; 2 | 3 | const sass = require('gulp-sass'); 4 | const inline = require('gulp-inline'); 5 | const minifyCSS = require('gulp-minify-css'); 6 | 7 | export const build = (gulp, config) => { 8 | 9 | gulp.task('default', ['compile:sass','compile:app','copy:dev'], () => { 10 | 11 | gulp.src(config.index) 12 | .pipe(inline({ 13 | base: 'dist', 14 | css: minifyCSS, 15 | disabledTypes: ['img', 'js'], // Only inline css files 16 | })) 17 | .pipe(gulp.dest('dist/')); 18 | 19 | }); 20 | 21 | 22 | } -------------------------------------------------------------------------------- /tasks/compile.ts: -------------------------------------------------------------------------------- 1 | declare var require; 2 | 3 | var Builder = require('systemjs-builder'); 4 | var util = require('gulp-util'); 5 | 6 | export const compile = (gulp, config) => { 7 | 8 | gulp.task('compile:app', ['compile:vendor'], () => { 9 | 10 | let builder = new Builder(); 11 | 12 | return builder.loadConfig(config.system.configFile) 13 | .then(() => { 14 | return builder.bundle('app - dist/vendor.js', 'dist/app.js', {minify: util.env.production}) 15 | }) 16 | }); 17 | 18 | gulp.task('compile:vendor', [], () => { 19 | 20 | let builder = new Builder(); 21 | 22 | return builder.loadConfig(config.system.configFile) 23 | .then(() => { 24 | return builder.bundle('app - [app/**/*]', 'dist/vendor.js', {minify: util.env.production}) 25 | }) 26 | }); 27 | 28 | gulp.task('compile:serviceworker', [], () => { 29 | 30 | let builder = new Builder(); 31 | 32 | return builder.loadConfig(config.system.configFile) 33 | .then(() => { 34 | return builder.buildStatic('app/ng2-service-worker', 'dist/worker.js', {minify: util.env.production}) 35 | }) 36 | }); 37 | } -------------------------------------------------------------------------------- /tasks/copy.ts: -------------------------------------------------------------------------------- 1 | const copy = (gulp, config) => { 2 | 3 | gulp.task('copy:dev', () => { 4 | return gulp.src(config.staticFiles) 5 | .pipe(gulp.dest(config.dist)); 6 | }); 7 | 8 | } 9 | 10 | export {copy}; -------------------------------------------------------------------------------- /tasks/styles.ts: -------------------------------------------------------------------------------- 1 | declare var require; 2 | 3 | const sass = require('gulp-sass'); 4 | 5 | export const compile = (gulp, config) => { 6 | gulp.task('compile:sass', () => { 7 | return gulp.src(config.css) 8 | .pipe(sass().on('error', sass.logError)) 9 | .pipe(gulp.dest(config.dist)) 10 | }) 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "outDir": "build", 7 | "rootDir": ".", 8 | "sourceMap": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node" 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } --------------------------------------------------------------------------------