From c9ead5e93d623a08f9425c3d7234c7a781d7a71e Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sat, 25 Mar 2017 21:00:16 +0100 Subject: [PATCH] . --- app/defaultConfigValues.yaml | 1 + app/index.pug | 2 +- app/package.json | 1 + app/src/api/tab.ts | 5 +- app/src/api/tabRecovery.ts | 2 +- app/src/components/appRoot.less | 10 +- app/src/components/appRoot.pug | 73 +++++---- app/src/services/app.ts | 22 ++- app/src/services/config.ts | 1 + app/src/settings/components/settingsPane.pug | 163 +++++++++++-------- app/src/settings/recoveryProvider.ts | 2 +- app/src/terminal/api.ts | 15 ++ app/src/terminal/buttonProvider.ts | 4 +- app/src/terminal/index.ts | 3 + app/src/terminal/persistenceProviders.ts | 64 ++++++++ app/src/terminal/recoveryProvider.ts | 9 +- app/src/terminal/services/sessions.ts | 124 ++------------ app/src/terminal/tab.ts | 4 + app/src/theme.scss | 136 +++++++++++----- package.json | 1 + 20 files changed, 369 insertions(+), 273 deletions(-) create mode 100644 app/src/terminal/persistenceProviders.ts diff --git a/app/defaultConfigValues.yaml b/app/defaultConfigValues.yaml index d41777b5..e6c67aa5 100644 --- a/app/defaultConfigValues.yaml +++ b/app/defaultConfigValues.yaml @@ -4,6 +4,7 @@ appearance: dock: 'off' dockScreen: 'current' dockFill: 50 + tabsOnTop: true hotkeys: new-tab: - ['Ctrl-A', 'C'] diff --git a/app/index.pug b/app/index.pug index d927c3a8..7cad0401 100644 --- a/app/index.pug +++ b/app/index.pug @@ -8,5 +8,5 @@ html window.nodeRequire = require script(src='./preload.js') script(src='./bundle.js', defer) - body(style='background: ; min-height: 100vh') + body(style='background: ; min-height: 100vh; overflow: hidden') app-root diff --git a/app/package.json b/app/package.json index 6f6893fc..65c2696f 100644 --- a/app/package.json +++ b/app/package.json @@ -8,6 +8,7 @@ "electron-config": "0.2.1", "electron-debug": "1.0.1", "electron-is-dev": "0.1.2", + "fs-promise": "^2.0.1", "node-pty": "0.6.3", "path": "0.12.7" }, diff --git a/app/src/api/tab.ts b/app/src/api/tab.ts index 8f1781ba..7957d8ee 100644 --- a/app/src/api/tab.ts +++ b/app/src/api/tab.ts @@ -16,7 +16,7 @@ export class Tab { this.id = Tab.lastTabID++ } - displayActivity () { + displayActivity (): void { this.hasActivity = true } @@ -27,4 +27,7 @@ export class Tab { getRecoveryToken (): any { return null } + + destroy (): void { + } } diff --git a/app/src/api/tabRecovery.ts b/app/src/api/tabRecovery.ts index 4f7f87fb..8b2b612a 100644 --- a/app/src/api/tabRecovery.ts +++ b/app/src/api/tabRecovery.ts @@ -1,5 +1,5 @@ import { Tab } from './tab' export abstract class TabRecoveryProvider { - abstract recover (recoveryToken: any): Tab + abstract async recover (recoveryToken: any): Promise } diff --git a/app/src/components/appRoot.less b/app/src/components/appRoot.less index c1855161..5ba2e7e8 100644 --- a/app/src/components/appRoot.less +++ b/app/src/components/appRoot.less @@ -33,9 +33,15 @@ @tab-border-radius: 4px; -:host > .spacer { - flex: 0 0 5px; +.content { + flex: auto; + display: flex; + flex-direction: column-reverse; background: @title-bg; + + &.tabs-on-top { + flex-direction: column; + } } .tabs { diff --git a/app/src/components/appRoot.pug b/app/src/components/appRoot.pug index 24d2caa9..36e47872 100644 --- a/app/src/components/appRoot.pug +++ b/app/src/components/appRoot.pug @@ -1,41 +1,44 @@ -title-bar(*ngIf='!config.store.appearance.useNativeFrame && config.store.appearance.dock == "off"') +title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appearance.dock == "off"') -.spacer +.content( + [class.tabs-on-top]='config.full().appearance.tabsOnTop' +) + .tabs( + [class.active-tab-0]='app.tabs[0] == app.activeTab', + ) + button.btn.btn-secondary( + *ngFor='let button of getToolbarButtons(false)', + [title]='button.title', + (click)='button.click()', + ) + i.fa([class]='"fa fa-" + button.icon') + tab-header( + *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', + [index]='idx', + [model]='tab', + [active]='tab == app.activeTab', + [hasActivity]='tab.hasActivity', + @animateTab, + (click)='app.selectTab(tab)', + (closeClicked)='app.closeTab(tab)', + ) + button.btn.btn-secondary( + *ngFor='let button of getToolbarButtons(true)', + [title]='button.title', + (click)='button.click()', + ) + i.fa([class]='"fa fa-" + button.icon') -.tabs(class='active-tab-{{app.tabs.indexOf(app.activeTab)}}') - button.btn.btn-secondary( - *ngFor='let button of getToolbarButtons(false)', - [title]='button.title', - (click)='button.click()', - ) - i.fa([class]='"fa fa-" + button.icon') - tab-header( - *ngFor='let tab of app.tabs; let idx = index; trackBy: tab?.id', - [index]='idx', - [model]='tab', - [active]='tab == app.activeTab', - [hasActivity]='tab.hasActivity', - @animateTab, - (click)='app.selectTab(tab)', - (closeClicked)='app.closeTab(tab)', - ) - button.btn.btn-secondary( - *ngFor='let button of getToolbarButtons(true)', - [title]='button.title', - (click)='button.click()', - ) - i.fa([class]='"fa fa-" + button.icon') + .tabs-content + tab-body( + *ngFor='let tab of app.tabs; trackBy: tab?.id', + [active]='tab == app.activeTab', + [model]='tab', + [class.scrollable]='tab.scrollable', + ) -.tabs-content - tab-body( - *ngFor='let tab of app.tabs; trackBy: tab?.id', - [active]='tab == app.activeTab', - [model]='tab', - [class.scrollable]='tab.scrollable', - ) - -// TODO -//hotkey-hint + // TODO + //hotkey-hint toaster-container([toasterconfig]="toasterconfig") template(ngbModalContainer) diff --git a/app/src/services/app.ts b/app/src/services/app.ts index 9d301393..090a59bc 100644 --- a/app/src/services/app.ts +++ b/app/src/services/app.ts @@ -80,20 +80,26 @@ export class AppService { ) } - restoreTabs () { + async restoreTabs (): Promise { if (window.localStorage.tabsRecovery) { - JSON.parse(window.localStorage.tabsRecovery).forEach((token) => { + for (let token of JSON.parse(window.localStorage.tabsRecovery)) { + let tab: Tab for (let provider of this.tabRecoveryProviders) { try { - let tab = provider.recover(token) + tab = await provider.recover(token) if (tab) { - this.openTab(tab) - return + break } - } catch (_) { } + } catch (error) { + this.logger.warn('Tab recovery crashed:', token, provider, error) + } } - this.logger.warn('Cannot restore tab from the token:', token) - }) + if (tab) { + this.openTab(tab) + } else { + this.logger.warn('Cannot restore tab from the token:', token) + } + } this.saveTabs() } } diff --git a/app/src/services/config.ts b/app/src/services/config.ts index 040f48fb..94ca0855 100644 --- a/app/src/services/config.ts +++ b/app/src/services/config.ts @@ -15,6 +15,7 @@ export interface IAppearanceData { dock: string dockScreen: string dockFill: number + tabsOnTop: boolean } export interface ITerminalData { diff --git a/app/src/settings/components/settingsPane.pug b/app/src/settings/components/settingsPane.pug index 529074f5..5df3513f 100644 --- a/app/src/settings/components/settingsPane.pug +++ b/app/src/settings/components/settingsPane.pug @@ -5,101 +5,124 @@ ngb-tabset(type='tabs') template(ngbTabTitle) | Application template(ngbTabContent) - .form-group - label Window frame - br - div( - '[(ngModel)]'='config.store.appearance.useNativeFrame' - '(ngModelChange)'='config.save(); requestRestart()' - ngbRadioGroup - ) - label.btn.btn-secondary - input( - type='radio', - [value]='true' - ) - | Native - label.btn.btn-secondary - input( - type='radio', - [value]='false' - ) - | Custom - small.form-text.text-muted Whether a custom window or an OS native window should be used - .row - .col.col-auto + .col.col-sm-6 .form-group - label Dock the terminal + label Show tabs br div( - '[(ngModel)]'='config.store.appearance.dock' - '(ngModelChange)'='config.save(); docking.dock()' + '[(ngModel)]'='config.store.appearance.tabsOnTop' + '(ngModelChange)'='config.save()' ngbRadioGroup ) label.btn.btn-secondary input( type='radio', - [value]='"off"' + [value]='true' ) - | Off + | On top label.btn.btn-secondary input( type='radio', - [value]='"top"' + [value]='false' ) - | Top + | At the bottom + .col.col-sm-6 + .form-group + label Window frame + br + div( + '[(ngModel)]'='config.store.appearance.useNativeFrame' + '(ngModelChange)'='config.save(); requestRestart()' + ngbRadioGroup + ) label.btn.btn-secondary input( type='radio', - [value]='"left"' + [value]='true' ) - | Left + | Native label.btn.btn-secondary input( type='radio', - [value]='"right"' + [value]='false' ) - | Right - label.btn.btn-secondary - input( - type='radio', - [value]='"bottom"' - ) - | Bottom + | Custom + small.form-text.text-muted Whether a custom window or an OS native window should be used - .form-group(*ngIf='config.store.appearance.dock != "off"') - label Display on - br - div( - '[(ngModel)]'='config.store.appearance.dockScreen' - '(ngModelChange)'='config.save(); docking.dock()' - ngbRadioGroup - ) - label.btn.btn-secondary - input( - type='radio', - [value]='"current"' + .row + .col.col-auto + .form-group + label Dock the terminal + br + div( + '[(ngModel)]'='config.store.appearance.dock' + '(ngModelChange)'='config.save(); docking.dock()' + ngbRadioGroup ) - | Current - label.btn.btn-secondary(*ngFor='let screen of docking.getScreens()') - input( - type='radio', - [value]='screen.id' + label.btn.btn-secondary + input( + type='radio', + [value]='"off"' + ) + | Off + label.btn.btn-secondary + input( + type='radio', + [value]='"top"' + ) + | Top + label.btn.btn-secondary + input( + type='radio', + [value]='"left"' + ) + | Left + label.btn.btn-secondary + input( + type='radio', + [value]='"right"' + ) + | Right + label.btn.btn-secondary + input( + type='radio', + [value]='"bottom"' + ) + | Bottom + + .form-group(*ngIf='config.store.appearance.dock != "off"') + label Display on + br + div( + '[(ngModel)]'='config.store.appearance.dockScreen' + '(ngModelChange)'='config.save(); docking.dock()' + ngbRadioGroup + ) + label.btn.btn-secondary + input( + type='radio', + [value]='"current"' + ) + | Current + label.btn.btn-secondary(*ngFor='let screen of docking.getScreens()') + input( + type='radio', + [value]='screen.id' + ) + | {{screen.name}} + .col.col-auto + .form-group(*ngIf='config.store.appearance.dock != "off"') + label Docked terminal size + br + input( + type='range', + '[(ngModel)]'='config.store.appearance.dockFill', + '(mouseup)'='config.save(); docking.dock()', + min='0.05', + max='1', + step='0.01' ) - | {{screen.name}} - .col.col-auto - .form-group(*ngIf='config.store.appearance.dock != "off"') - label Docked terminal size - br - input( - type='range', - '[(ngModel)]'='config.store.appearance.dockFill', - '(mouseup)'='config.save(); docking.dock()', - min='0.05', - max='1', - step='0.01' - ) ngb-tab template(ngbTabTitle) diff --git a/app/src/settings/recoveryProvider.ts b/app/src/settings/recoveryProvider.ts index 074a5751..5e7271e7 100644 --- a/app/src/settings/recoveryProvider.ts +++ b/app/src/settings/recoveryProvider.ts @@ -5,7 +5,7 @@ import { SettingsTab } from './tab' @Injectable() export class RecoveryProvider extends TabRecoveryProvider { - recover (recoveryToken: any): Tab { + async recover (recoveryToken: any): Promise { if (recoveryToken.type == 'app:settings') { return new SettingsTab() } diff --git a/app/src/terminal/api.ts b/app/src/terminal/api.ts index 26d9278c..4b6ef1f3 100644 --- a/app/src/terminal/api.ts +++ b/app/src/terminal/api.ts @@ -1,3 +1,18 @@ export abstract class TerminalDecorator { abstract decorate (terminal): void } + +export interface SessionOptions { + name?: string, + command?: string, + args?: string[], + cwd?: string, + env?: any, + recoveryId?: string +} + +export abstract class SessionPersistenceProvider { + abstract async recoverSession (recoveryId: any): Promise + abstract async createSession (options: SessionOptions): Promise + abstract async terminateSession (recoveryId: string): Promise +} diff --git a/app/src/terminal/buttonProvider.ts b/app/src/terminal/buttonProvider.ts index 149d645c..f28dac34 100644 --- a/app/src/terminal/buttonProvider.ts +++ b/app/src/terminal/buttonProvider.ts @@ -17,8 +17,8 @@ export class ButtonProvider extends ToolbarButtonProvider { return [{ icon: 'plus', title: 'New terminal', - click: () => { - let session = this.sessions.createNewSession({ command: 'zsh' }) + click: async () => { + let session = await this.sessions.createNewSession({ command: 'zsh' }) this.app.openTab(new TerminalTab(session)) } }] diff --git a/app/src/terminal/index.ts b/app/src/terminal/index.ts index 81c50eb8..846c816d 100644 --- a/app/src/terminal/index.ts +++ b/app/src/terminal/index.ts @@ -6,8 +6,10 @@ import { ToolbarButtonProvider, TabRecoveryProvider } from 'api' import { TerminalTabComponent } from './components/terminalTab' import { SessionsService } from './services/sessions' +import { ScreenPersistenceProvider } from './persistenceProviders' import { ButtonProvider } from './buttonProvider' import { RecoveryProvider } from './recoveryProvider' +import { SessionPersistenceProvider } from './api' @NgModule({ @@ -19,6 +21,7 @@ import { RecoveryProvider } from './recoveryProvider' { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, SessionsService, + { provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider }, ], entryComponents: [ TerminalTabComponent, diff --git a/app/src/terminal/persistenceProviders.ts b/app/src/terminal/persistenceProviders.ts new file mode 100644 index 00000000..c1b64149 --- /dev/null +++ b/app/src/terminal/persistenceProviders.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs-promise' +const exec = require('child-process-promise').exec + +import { SessionOptions, SessionPersistenceProvider } from './api' + + +export class NullPersistenceProvider extends SessionPersistenceProvider { + async recoverSession (_recoveryId: any): Promise { + return null + } + + async createSession (_options: SessionOptions): Promise { + return null + } + + async terminateSession (_recoveryId: string): Promise { + return + } +} + + +export class ScreenPersistenceProvider extends SessionPersistenceProvider { + list(): Promise { + return exec('screen -list').then((result) => { + return result.stdout.split('\n') + .filter((line) => /\bterm-tab-/.exec(line)) + .map((line) => line.trim().split('.')[0]) + }).catch(() => { + return [] + }) + } + + async recoverSession (recoveryId: any): Promise { + // TODO check + return { + recoveryId, + command: 'screen', + args: ['-r', recoveryId], + } + } + + async createSession (options: SessionOptions): Promise { + let configPath = '/tmp/.termScreenConfig' + await fs.writeFile(configPath, ` + escape ^^^ + vbell off + term xterm-color + bindkey "^[OH" beginning-of-line + bindkey "^[OF" end-of-line + termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007' + defhstatus "^Et" + hardstatus off + `, 'utf-8') + let recoveryId = `term-tab-${Date.now()}` + options.args = ['-c', configPath, '-U', '-S', recoveryId, '--', options.command].concat(options.args || []) + options.command = 'screen' + options.recoveryId = recoveryId + return options + } + + async terminateSession (recoveryId: string): Promise { + await exec(`screen -S ${recoveryId} -X quit`) + } +} diff --git a/app/src/terminal/recoveryProvider.ts b/app/src/terminal/recoveryProvider.ts index 8149fbbd..c569eeba 100644 --- a/app/src/terminal/recoveryProvider.ts +++ b/app/src/terminal/recoveryProvider.ts @@ -10,11 +10,12 @@ export class RecoveryProvider extends TabRecoveryProvider { super() } - recover (recoveryToken: any): Tab { + async recover (recoveryToken: any): Promise { if (recoveryToken.type == 'app:terminal') { - const options = this.sessions.recoveryProvider.getRecoverySession(recoveryToken.recoveryId) - let session = this.sessions.createSession(options) - session.recoveryId = recoveryToken.recoveryId + let session = await this.sessions.recover(recoveryToken.recoveryId) + if (!session) { + return null + } return new TerminalTab(session) } return null diff --git a/app/src/terminal/services/sessions.ts b/app/src/terminal/services/sessions.ts index e4b25bd4..a41ac731 100644 --- a/app/src/terminal/services/sessions.ts +++ b/app/src/terminal/services/sessions.ts @@ -1,87 +1,9 @@ -import { Injectable, NgZone, EventEmitter } from '@angular/core' -import { Logger, LogService } from 'services/log' -const exec = require('child-process-promise').exec import * as nodePTY from 'node-pty' -import * as fs from 'fs' +import { Injectable, EventEmitter } from '@angular/core' +import { Logger, LogService } from 'services/log' +import { SessionOptions, SessionPersistenceProvider } from '../api' -export interface ISessionRecoveryProvider { - list (): Promise - getRecoverySession (recoveryId: any): SessionOptions - wrapNewSession (options: SessionOptions): SessionOptions - terminateSession (recoveryId: string): Promise -} - -export class NullSessionRecoveryProvider implements ISessionRecoveryProvider { - async list (): Promise { - return [] - } - - getRecoverySession (_recoveryId: any): SessionOptions { - return null - } - - wrapNewSession (options: SessionOptions): SessionOptions { - return options - } - - async terminateSession (_recoveryId: string): Promise { - return null - } -} - -export class ScreenSessionRecoveryProvider implements ISessionRecoveryProvider { - list(): Promise { - return exec('screen -list').then((result) => { - return result.stdout.split('\n') - .filter((line) => /\bterm-tab-/.exec(line)) - .map((line) => line.trim().split('.')[0]) - }).catch(() => { - return [] - }) - } - - getRecoverySession (recoveryId: any): SessionOptions { - return { - command: 'screen', - args: ['-r', recoveryId], - } - } - - wrapNewSession (options: SessionOptions): SessionOptions { - // TODO - let configPath = '/tmp/.termScreenConfig' - fs.writeFileSync(configPath, ` - escape ^^^ - vbell off - term xterm-color - bindkey "^[OH" beginning-of-line - bindkey "^[OF" end-of-line - termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007' - defhstatus "^Et" - hardstatus off - `, 'utf-8') - let recoveryId = `term-tab-${Date.now()}` - options.args = ['-c', configPath, '-U', '-S', recoveryId, '--', options.command].concat(options.args || []) - options.command = 'screen' - options.recoveryId = recoveryId - return options - } - - async terminateSession (recoveryId: string): Promise { - return exec(`screen -S ${recoveryId} -X quit`) - } -} - - -export interface SessionOptions { - name?: string, - command?: string, - args?: string[], - cwd?: string, - env?: any, - recoveryId?: string -} export class Session { open: boolean @@ -96,6 +18,7 @@ export class Session { constructor (options: SessionOptions) { this.name = options.name + this.recoveryId = options.recoveryId console.log('Spawning', options.command) let env = { @@ -184,57 +107,44 @@ export class Session { } } + @Injectable() export class SessionsService { sessions: {[id: string]: Session} = {} logger: Logger private lastID = 0 - recoveryProvider: ISessionRecoveryProvider constructor( - private zone: NgZone, + private persistence: SessionPersistenceProvider, log: LogService, ) { this.logger = log.create('sessions') - this.recoveryProvider = new ScreenSessionRecoveryProvider() - //this.recoveryProvider = new NullSessionRecoveryProvider() } - createNewSession (options: SessionOptions) : Session { - options = this.recoveryProvider.wrapNewSession(options) - let session = this.createSession(options) - session.recoveryId = options.recoveryId + async createNewSession (options: SessionOptions) : Promise { + options = await this.persistence.createSession(options) + let session = this.addSession(options) return session } - createSession (options: SessionOptions) : Session { + addSession (options: SessionOptions) : Session { this.lastID++ options.name = `session-${this.lastID}` let session = new Session(options) const destroySubscription = session.destroyed.subscribe(() => { delete this.sessions[session.name] + this.persistence.terminateSession(session.recoveryId) destroySubscription.unsubscribe() }) this.sessions[session.name] = session return session } - async destroySession (session: Session): Promise { - await session.gracefullyDestroy() - await this.recoveryProvider.terminateSession(session.recoveryId) - return null - } - - recoverAll () : Promise { - return >(this.recoveryProvider.list().then((items) => { - return this.zone.run(() => { - return items.map((recoveryId) => { - const options = this.recoveryProvider.getRecoverySession(recoveryId) - let session = this.createSession(options) - session.recoveryId = recoveryId - return session - }) - }) - })) + async recover (recoveryId: string) : Promise { + const options = await this.persistence.recoverSession(recoveryId) + if (!options) { + return null + } + return this.addSession(options) } } diff --git a/app/src/terminal/tab.ts b/app/src/terminal/tab.ts index 6eb4840b..c34c3a3c 100644 --- a/app/src/terminal/tab.ts +++ b/app/src/terminal/tab.ts @@ -20,4 +20,8 @@ export class TerminalTab extends Tab { recoveryId: this.session.recoveryId, } } + + destroy (): void { + this.session.gracefullyDestroy() + } } diff --git a/app/src/theme.scss b/app/src/theme.scss index 830b4c29..dc2aee25 100644 --- a/app/src/theme.scss +++ b/app/src/theme.scss @@ -78,53 +78,107 @@ title-bar { } -.tabs tab-header { - background: $body-bg; - .content-wrapper { - background: $body-bg2; +app-root .content { + background: $body-bg2; - .index { - color: #444; - } + .tabs { + background: $body-bg; - button { - color: $body-color; - border: none; - transition: 0.25s all; - - &:hover { background: $button-hover-bg !important; } - &:active { background: $button-active-bg !important; } - } - } - - &.pre-selected, &:nth-last-child(1) { - .content-wrapper { - border-bottom-right-radius: $tab-border-radius; - } - } - - &.post-selected { - .content-wrapper { - border-bottom-left-radius: $tab-border-radius; - } - } - - &.active { - background: $body-bg2; - - .content-wrapper { - border-top: 1px solid $blue; + tab-header { background: $body-bg; - border-top-left-radius: $tab-border-radius; - border-top-right-radius: $tab-border-radius; + + .content-wrapper { + background: $body-bg2; + + .index { + color: #555; + } + + button { + color: $body-color; + border: none; + transition: 0.25s all; + + &:hover { background: $button-hover-bg !important; } + &:active { background: $button-active-bg !important; } + } + } + + &.active { + background: $body-bg2; + + .content-wrapper { + background: $body-bg; + } + } + + &.has-activity:not(.active) { + .content-wrapper .index { + background: $blue; + color: white; + text-shadow: 0 1px 1px rgba(0,0,0,.95); + } + } } } - &.has-activity:not(.active) { - .content-wrapper .index { - background: $blue; - color: white; - text-shadow: 0 1px 1px rgba(0,0,0,.95); + &.tabs-on-top .tabs { + margin-top: 3px; + + tab-header { + &.pre-selected, &:nth-last-child(1) { + .content-wrapper { + border-bottom-right-radius: $tab-border-radius; + } + } + + &.post-selected { + .content-wrapper { + border-bottom-left-radius: $tab-border-radius; + } + } + + .content-wrapper { + border-top: 1px solid transparent; + } + + &.active .content-wrapper { + border-top: 1px solid $blue; + border-top-left-radius: $tab-border-radius; + border-top-right-radius: $tab-border-radius; + } + } + } + + &:not(.tabs-on-top) .tabs { + margin-bottom: 3px; + + tab-header { + &.pre-selected, &:nth-last-child(1) { + .content-wrapper { + border-top-right-radius: $tab-border-radius; + } + } + + &.post-selected { + .content-wrapper { + border-top-left-radius: $tab-border-radius; + } + } + + .content-wrapper { + border-bottom: 1px solid transparent; + } + + &.active .content-wrapper { + border-bottom: 1px solid $blue; + border-bottom-left-radius: $tab-border-radius; + border-bottom-right-radius: $tab-border-radius; + } } } } + +tab-body { + background: $body-bg; +} diff --git a/package.json b/package.json index 080d1577..b9e8d69d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "term", "devDependencies": { + "@types/fs-promise": "^1.0.1", "apply-loader": "^0.1.0", "autoprefixer": "^6.7.7", "awesome-typescript-loader": "3.0.8",