diff --git a/internal_packages/preferences/lib/tabs/workspace-section.cjsx b/internal_packages/preferences/lib/tabs/workspace-section.cjsx
index c8fceb1d7..ca562797b 100644
--- a/internal_packages/preferences/lib/tabs/workspace-section.cjsx
+++ b/internal_packages/preferences/lib/tabs/workspace-section.cjsx
@@ -132,42 +132,6 @@ class AppearanceModeOption extends React.Component
{label}
-class ThemeSelector extends React.Component
- constructor: (@props) ->
- @_themeManager = NylasEnv.themes
- @state = @_getState()
-
- componentDidMount: =>
- @disposable = @_themeManager.onDidChangeActiveThemes =>
- @setState @_getState()
-
- componentWillUnmount: ->
- @disposable.dispose()
-
- _getState: =>
- themes: @_themeManager.getLoadedThemes()
- activeTheme: @_themeManager.getActiveTheme().name
-
- _setActiveTheme: (theme) =>
- @setState activeTheme: theme
- @_themeManager.setActiveTheme theme
-
- _onChangeTheme: (event) =>
- value = event.target.value
- if value is 'install'
- NylasEnv.commands.dispatch document.body, 'application:install-package'
- else
- @_setActiveTheme(value)
-
- render: =>
-
- Select theme:
-
-
class WorkspaceSection extends React.Component
@displayName: 'WorkspaceSection'
@@ -202,8 +166,6 @@ class WorkspaceSection extends React.Component
keyPath="core.workspace.interfaceZoom"
config={@props.config} />
-
-
Layout
diff --git a/internal_packages/theme-picker/lib/main.js b/internal_packages/theme-picker/lib/main.js
new file mode 100644
index 000000000..949bc9d27
--- /dev/null
+++ b/internal_packages/theme-picker/lib/main.js
@@ -0,0 +1,18 @@
+/** @babel */
+import React from 'react';
+import Actions from '../../../src/flux/actions'
+
+import ThemePicker from './theme-picker'
+
+
+export function activate() {
+ this.disposable = NylasEnv.commands.add("body",
+ "window:launch-theme-picker",
+ () => Actions.openModal(children=,
+ height=400,
+ width=250));
+}
+
+export function deactivate() {
+ this.disposable.dispose();
+}
diff --git a/internal_packages/theme-picker/lib/theme-option.jsx b/internal_packages/theme-picker/lib/theme-option.jsx
new file mode 100644
index 000000000..96e8554fb
--- /dev/null
+++ b/internal_packages/theme-picker/lib/theme-option.jsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import fs from 'fs-plus';
+import path from 'path';
+
+import {EventedIFrame} from 'nylas-component-kit';
+import LessCompileCache from '../../../src/less-compile-cache'
+
+
+class ThemeOption extends React.Component {
+ static propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ active: React.PropTypes.bool.isRequired,
+ }
+
+ constructor(props) {
+ super(props);
+ this.lessCache = null;
+ }
+
+ componentDidMount() {
+ this._writeContent();
+ }
+
+ _getImportPaths() {
+ const themes = [this.props.theme];
+ // Pulls the theme package for Light as the base theme
+ for (const theme of NylasEnv.themes.getActiveThemes()) {
+ if (theme.name === NylasEnv.themes.baseThemeName()) {
+ themes.push(theme);
+ }
+ }
+ const themePaths = [];
+ for (const theme of themes) {
+ themePaths.push(theme.getStylesheetsPath());
+ }
+ return themePaths.filter((themePath) => fs.isDirectorySync(themePath));
+ }
+
+ _loadStylesheet(stylesheetPath) {
+ if (path.extname(stylesheetPath) === '.less') {
+ return this._loadLessStylesheet(stylesheetPath);
+ }
+ return fs.readFileSync(stylesheetPath, 'utf8');
+ }
+
+ _loadLessStylesheet(lessStylesheetPath) {
+ const {configDirPath, resourcePath} = NylasEnv.getLoadSettings();
+ if (this.lessCache) {
+ this.lessCache.setImportPaths(this._getImportPaths());
+ } else {
+ const importPaths = this._getImportPaths();
+ this.lessCache = new LessCompileCache({configDirPath, resourcePath, importPaths});
+ }
+ const themeVarPath = path.relative(`${resourcePath}/internal_packages/theme-picker/preview-styles`,
+ this.props.theme.getStylesheetsPath());
+ let varImports = `@import "../../../static/variables/ui-variables";`
+ if (fs.existsSync(`${this.props.theme.getStylesheetsPath()}/ui-variables.less`)) {
+ varImports += `@import "${themeVarPath}/ui-variables";`
+ }
+ if (fs.existsSync(`${this.props.theme.getStylesheetsPath()}/theme-colors.less`)) {
+ varImports += `@import "${themeVarPath}/theme-colors";`
+ }
+ const less = fs.readFileSync(lessStylesheetPath, 'utf8');
+ return this.lessCache.cssForFile(lessStylesheetPath, [varImports, less].join('\n'));
+ }
+
+ _writeContent() {
+ const domNode = React.findDOMNode(this);
+ const doc = domNode.contentDocument;
+ if (!doc) return;
+
+ const {resourcePath} = NylasEnv.getLoadSettings();
+
+ const html = `
+
+
+
+
${this.props.theme.displayName}
+
+
+
+
+
+ `
+
+ doc.open();
+ doc.write(html);
+ doc.close();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default ThemeOption;
diff --git a/internal_packages/theme-picker/lib/theme-picker.jsx b/internal_packages/theme-picker/lib/theme-picker.jsx
new file mode 100644
index 000000000..e8037d6a9
--- /dev/null
+++ b/internal_packages/theme-picker/lib/theme-picker.jsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import Actions from '../../../src/flux/actions'
+
+import {Flexbox, RetinaImg} from 'nylas-component-kit';
+import ThemeOption from './theme-option';
+
+
+class ThemePicker extends React.Component {
+ static displayName = 'ThemePicker';
+
+ constructor(props) {
+ super(props);
+ this._themeManager = NylasEnv.themes;
+ this.state = this._getState();
+ }
+
+ componentDidMount() {
+ this.disposable = this._themeManager.onDidChangeActiveThemes(() => {
+ this.setState(this._getState());
+ });
+ }
+
+ componentWillUnmount() {
+ this.disposable.dispose();
+ }
+
+ _getState() {
+ return {
+ themes: this._themeManager.getLoadedThemes(),
+ activeTheme: this._themeManager.getActiveTheme().name,
+ }
+ }
+
+ _setActiveTheme(theme) {
+ const prevActiveTheme = this.state.activeTheme;
+ this.setState({activeTheme: theme});
+ this._themeManager.setActiveTheme(theme);
+ this._rewriteIFrame(prevActiveTheme, theme);
+ }
+
+ _rewriteIFrame(prevActiveTheme, activeTheme) {
+ const prevActiveThemeDoc = document.querySelector(`.theme-preview-${prevActiveTheme}`).contentDocument;
+ const prevActiveElement = prevActiveThemeDoc.querySelector(".theme-option.active-true");
+ prevActiveElement.className = "theme-option active-false";
+ const activeThemeDoc = document.querySelector(`.theme-preview-${activeTheme}`).contentDocument;
+ const activeElement = activeThemeDoc.querySelector(".theme-option.active-false");
+ activeElement.className = "theme-option active-true";
+ }
+
+ _renderThemeOptions() {
+ const themeOptions = this.state.themes.map((theme) =>
+ this._setActiveTheme(theme.name)}
+ style={{cursor: "pointer", width: "115px", margin: "2px"}}>
+
+
+ )
+ return themeOptions;
+ }
+
+ render() {
+ return (
+
+
+ Actions.closeModal()} />
+ Themes
+ Click any theme to preview.
+
+
+ {this._renderThemeOptions()}
+
+
+
+
+ );
+ }
+}
+
+export default ThemePicker;
diff --git a/internal_packages/theme-picker/package.json b/internal_packages/theme-picker/package.json
new file mode 100644
index 000000000..6e9e18e95
--- /dev/null
+++ b/internal_packages/theme-picker/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "theme-picker",
+ "version": "0.1.0",
+ "main": "./lib/main",
+ "description": "View different themes and choose them easily",
+ "license": "GPL-3.0",
+ "private": true,
+ "engines": {
+ "nylas": "*"
+ },
+ "dependencies": {
+ }
+}
diff --git a/internal_packages/theme-picker/preview-styles/theme-option.less b/internal_packages/theme-picker/preview-styles/theme-option.less
new file mode 100644
index 000000000..b8bd74c8e
--- /dev/null
+++ b/internal_packages/theme-picker/preview-styles/theme-option.less
@@ -0,0 +1,93 @@
+html,
+body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+}
+
+.theme-option {
+ position: absolute;
+ top: 0;
+ width: 100px;
+ height: 60px;
+ background-color: @background-secondary;
+ color: @text-color;
+ border-radius: 5px;
+ text-align: center;
+ overflow: hidden;
+
+ &.active-true {
+ border: 1px solid #3187e1;
+ }
+
+ &.active-false {
+ border: 1px solid darken(#f6f6f6, 10%);
+ }
+
+ .theme-name {
+ font-family: @font-family;
+ font-size: 14px;
+ margin-top: 5px;
+ height: 18px;
+ overflow: hidden;
+ }
+
+ .swatches {
+ padding-left: 27px;
+ padding-right: 27px;
+ display: flex;
+ flex-direction: row;
+
+ .swatch {
+ flex: 1;
+ height: 10px;
+ width: 10px;
+ margin: 4px 2px 4px 2px;
+ border-radius: 2px;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ background-clip: border-box;
+ background-origin: border-box;
+
+ &.font-color {
+ background-color: @text-color;
+ }
+
+ &.active-color {
+ background-color: @component-active-color;
+ }
+
+ &.toolbar-color {
+ background-color: @toolbar-background-color;
+ }
+ }
+ }
+
+ .divider-black {
+ position: absolute;
+ bottom: 12px;
+ height: 1px;
+ width: 100%;
+ background-color: black;
+ opacity: 0.15;
+ }
+
+ .divider-white {
+ position: absolute;
+ z-index: 10;
+ bottom: 11px;
+ height: 1px;
+ width: 100%;
+ background-color: white;
+ opacity: 0.15;
+ }
+
+ .strip {
+ position: absolute;
+ bottom: 0;
+ height: 12px;
+ width: 100%;
+ background-color: @panel-background-color;
+ }
+}
diff --git a/internal_packages/theme-picker/spec/theme-picker-spec.jsx b/internal_packages/theme-picker/spec/theme-picker-spec.jsx
new file mode 100644
index 000000000..094aa29ae
--- /dev/null
+++ b/internal_packages/theme-picker/spec/theme-picker-spec.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+const ReactTestUtils = React.addons.TestUtils;
+
+import ThemePackage from '../../../src/theme-package';
+import ThemePicker from '../lib/theme-picker';
+
+const {resourcePath} = NylasEnv.getLoadSettings();
+const light = new ThemePackage(resourcePath + '/internal_packages/ui-light');
+const dark = new ThemePackage(resourcePath + '/internal_packages/ui-dark');
+
+describe('ThemePicker', ()=> {
+ beforeEach(()=> {
+ spyOn(ThemePicker.prototype, '_setActiveTheme').andCallThrough();
+ spyOn(NylasEnv.themes, 'getLoadedThemes').andReturn([light, dark]);
+ spyOn(NylasEnv.themes, 'getActiveTheme').andReturn(light);
+ this.component = ReactTestUtils.renderIntoDocument();
+ });
+
+ it('changes the active theme when a theme is clicked', ()=> {
+ const themeOption = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'clickable-theme-option')[1]);
+ ReactTestUtils.Simulate.mouseDown(themeOption);
+ expect(ThemePicker.prototype._setActiveTheme).toHaveBeenCalled();
+ });
+});
diff --git a/menus/darwin.cson b/menus/darwin.cson
index 548d53624..884a7fd23 100644
--- a/menus/darwin.cson
+++ b/menus/darwin.cson
@@ -5,6 +5,8 @@
{ label: 'About Nylas', command: 'application:about' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
+ { label: 'Change Theme...', command: 'window:launch-theme-picker' }
+ { label: 'Install New Theme...', command: 'application:install-package' }
{ type: 'separator' }
{ label: 'Add Account...', command: 'application:add-account' }
{ label: 'VERSION', enabled: false }
diff --git a/menus/linux.cson b/menus/linux.cson
index 6525e9667..53f4232a2 100644
--- a/menus/linux.cson
+++ b/menus/linux.cson
@@ -32,6 +32,8 @@
] }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
+ { label: 'Change Theme...', command: 'window:launch-theme-picker' }
+ { label: 'Install New Theme...', command: 'application:install-package' }
]
}
diff --git a/menus/win32.cson b/menus/win32.cson
index 2f04f16c9..89c4344b7 100644
--- a/menus/win32.cson
+++ b/menus/win32.cson
@@ -61,6 +61,8 @@
}
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
+ { label: 'Change Theme...', command: 'window:launch-theme-picker' }
+ { label: 'Install New Theme...', command: 'application:install-package' }
{ type: 'separator' }
{ label: 'Print Current Thread', command: 'application:print-thread' }
{ type: 'separator' }
diff --git a/src/components/flexbox.cjsx b/src/components/flexbox.cjsx
index aa160d70b..5cd20dee3 100644
--- a/src/components/flexbox.cjsx
+++ b/src/components/flexbox.cjsx
@@ -21,13 +21,17 @@ class Flexbox extends React.Component
direction: React.PropTypes.string
inline: React.PropTypes.bool
style: React.PropTypes.object
+ height: React.PropTypes.string
+
+ @defaultProps:
+ height: '100%'
render: ->
style = _.extend {}, (@props.style || {}),
'flexDirection': @props.direction,
'position':'relative'
'display': 'flex'
- 'height':'100%'
+ 'height': @props.height
if @props.inline is true
style.display = 'inline-flex'
diff --git a/src/components/modal.jsx b/src/components/modal.jsx
new file mode 100644
index 000000000..fcc086bd3
--- /dev/null
+++ b/src/components/modal.jsx
@@ -0,0 +1,98 @@
+import _ from 'underscore';
+import React from 'react';
+import Actions from '../flux/actions';
+
+
+class Modal extends React.Component {
+
+ static propTypes = {
+ className: React.PropTypes.string,
+ children: React.PropTypes.element,
+ height: React.PropTypes.number,
+ width: React.PropTypes.number,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ offset: 0,
+ dimensions: {},
+ };
+ }
+
+ componentDidMount() {
+ this._focusImportantElement();
+ }
+
+ _focusImportantElement = ()=> {
+ const modalNode = React.findDOMNode(this);
+
+ const focusable = modalNode.querySelectorAll("[tabIndex], input");
+ const matches = _.sortBy(focusable, (node)=> {
+ if (node.tabIndex > 0) {
+ return node.tabIndex;
+ } else if (node.nodeName === "INPUT") {
+ return 1000000
+ }
+ return 1000001
+ })
+ if (matches[0]) {
+ matches[0].focus();
+ }
+ };
+
+ _computeModalStyles = (height, width)=> {
+ const modalStyle = {
+ top: "50%",
+ left: "50%",
+ margin: "-200px 0 0 -125px",
+ height: height,
+ width: width,
+ position: "absolute",
+ backgroundColor: "white",
+ boxShadow: "0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5)",
+ borderRadius: "5px",
+ };
+ const containerStyle = {
+ height: "100%",
+ width: "100%",
+ zIndex: 1000,
+ position: "absolute",
+ backgroundColor: "transparent",
+ };
+ return {containerStyle, modalStyle};
+ };
+
+ _onBlur = (event)=> {
+ const target = event.nativeEvent.relatedTarget;
+ if (!target || (!React.findDOMNode(this).contains(target))) {
+ Actions.closeModal();
+ }
+ };
+
+ _onKeyDown = (event)=> {
+ if (event.key === "Escape") {
+ Actions.closeModal();
+ }
+ };
+
+ render() {
+ const {children, height, width} = this.props;
+ const {containerStyle, modalStyle} = this._computeModalStyles(height, width);
+
+ return (
+
+ );
+ }
+
+}
+
+export default Modal;
diff --git a/src/components/newsletter-signup.cjsx b/src/components/newsletter-signup.cjsx
index eb373c927..a22e46a50 100644
--- a/src/components/newsletter-signup.cjsx
+++ b/src/components/newsletter-signup.cjsx
@@ -56,7 +56,7 @@ class NewsletterSignup extends React.Component
"/newsletter-subscription/#{encodeURIComponent(props.emailAddress)}?name=#{encodeURIComponent(props.name)}"
render: =>
-
+
{@_renderControl()}
diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee
index efcb04d27..fa667efb4 100644
--- a/src/flux/actions.coffee
+++ b/src/flux/actions.coffee
@@ -518,6 +518,9 @@ class Actions
@openPopover: ActionScopeWindow
@closePopover: ActionScopeWindow
+ @openModal: ActionScopeWindow
+ @closeModal: ActionScopeWindow
+
###
Public: Set metadata for a specified model and pluginId.
diff --git a/src/flux/stores/modal-store.jsx b/src/flux/stores/modal-store.jsx
new file mode 100644
index 000000000..ee8b7ec99
--- /dev/null
+++ b/src/flux/stores/modal-store.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import NylasStore from 'nylas-store'
+import Actions from '../actions'
+import {Modal} from 'nylas-component-kit';
+
+
+const CONTAINER_ID = "nylas-modal-container";
+
+function createContainer(id) {
+ const element = document.createElement(id);
+ document.body.appendChild(element);
+ return element;
+}
+
+class ModalStore extends NylasStore {
+
+ constructor(containerId = CONTAINER_ID) {
+ super()
+ this.isOpen = false;
+ this.container = createContainer(containerId);
+ React.render(, this.container);
+
+ this.listenTo(Actions.openModal, this.openModal);
+ this.listenTo(Actions.closeModal, this.closeModal);
+ }
+
+ isModalOpen = ()=> {
+ return this.isOpen;
+ };
+
+ renderModal = (child, props, callback)=> {
+ const modal = (
+ {child}
+ );
+
+ React.render(modal, this.container, ()=> {
+ this.isOpen = true;
+ this.trigger();
+ callback();
+ });
+ };
+
+ openModal = (component, height, width, callback = ()=> {})=> {
+ const props = {
+ height: height,
+ width: width,
+ };
+
+ if (this.isOpen) {
+ this.closeModal(()=> {
+ this.renderModal(component, props, callback);
+ })
+ } else {
+ this.renderModal(component, props, callback);
+ }
+ };
+
+ closeModal = (callback = ()=>{})=> {
+ React.render(, this.container, ()=> {
+ this.isOpen = false;
+ this.trigger();
+ callback();
+ });
+ };
+
+}
+
+export default new ModalStore();
diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee
index a0610c6a6..c0e4efb28 100644
--- a/src/global/nylas-component-kit.coffee
+++ b/src/global/nylas-component-kit.coffee
@@ -16,6 +16,7 @@ class NylasComponentKit
@load "Switch", 'switch'
@load "Popover", 'popover'
@load "FixedPopover", 'fixed-popover'
+ @load "Modal", 'modal'
@load "Flexbox", 'flexbox'
@load "RetinaImg", 'retina-img'
@load "SwipeContainer", 'swipe-container'
diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee
index 8731e28fb..4adf9fcdc 100644
--- a/src/global/nylas-exports.coffee
+++ b/src/global/nylas-exports.coffee
@@ -130,8 +130,8 @@ class NylasExports
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
@require "PreferencesUIStore", 'flux/stores/preferences-ui-store'
@require "PopoverStore", 'flux/stores/popover-store'
+ @require "ModalStore", 'flux/stores/modal-store'
@require "SearchableComponentStore", 'flux/stores/searchable-component-store'
-
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
@require "MailRulesTemplates", 'mail-rules-templates'
@require "MailRulesProcessor", 'mail-rules-processor'
diff --git a/static/components/modal.less b/static/components/modal.less
new file mode 100644
index 000000000..f318bd0a1
--- /dev/null
+++ b/static/components/modal.less
@@ -0,0 +1,19 @@
+@import "ui-variables";
+
+.nylas-modal-container {
+ position: absolute;
+ z-index: 40;
+
+ .modal {
+ position: absolute;
+ background-color: @background-primary;
+ border-radius: @border-radius-base;
+ box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15);
+ }
+}
+
+body.platform-win32 {
+ .modal {
+ border-radius: 0;
+ }
+}
\ No newline at end of file
diff --git a/static/images/theme-picker/picker-close@1x.png b/static/images/theme-picker/picker-close@1x.png
new file mode 100644
index 000000000..a4252ca5d
Binary files /dev/null and b/static/images/theme-picker/picker-close@1x.png differ
diff --git a/static/images/theme-picker/picker-close@2x.png b/static/images/theme-picker/picker-close@2x.png
new file mode 100644
index 000000000..56ad7581a
Binary files /dev/null and b/static/images/theme-picker/picker-close@2x.png differ
diff --git a/static/index.less b/static/index.less
index 65e4a1d94..5833e4cfe 100644
--- a/static/index.less
+++ b/static/index.less
@@ -31,4 +31,5 @@
@import "components/editable-list";
@import "components/outline-view";
@import "components/fixed-popover";
+@import "components/modal";
@import "components/date-input";
diff --git a/static/variables/ui-variables.less b/static/variables/ui-variables.less
index b427b4431..7bc0d785e 100644
--- a/static/variables/ui-variables.less
+++ b/static/variables/ui-variables.less
@@ -90,7 +90,6 @@
@text-color-search-match: #fff000;
@text-color-search-current-match: #ff8b1a;
-@font-family-sans-serif: "Nylas-Pro", "Helvetica", sans-serif;
@font-family-sans-serif: "Nylas-Pro", "Helvetica", sans-serif;
@font-family-serif: Georgia, "Times New Roman", Times, serif;
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@@ -448,8 +447,6 @@ rgba(253,253,253,0.75) 100%);
@component-border-radius: 2px;
-
-@body-bg: @white;
//== Panels and Sidebars
@panel-background-color: @gray-lighter;
@toolbar-background-color: darken(@white, 17.5%);