From 531068179c7b3ab18fc02464234735ed7c441414 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 10 Feb 2016 16:00:32 -0800 Subject: [PATCH] High contrast mode switch * added button to the bottom right to switch to higher contrast, * suitable for projectors * increased contrast between background and text colors * thicker strokes around chart elements * ensure yellow details panel backgrounds are darker --- client/app/scripts/components/app.js | 15 ++- client/app/scripts/contrast-main.js | 9 ++ client/app/scripts/main.js | 2 +- client/app/scripts/utils/color-utils.js | 4 +- client/app/scripts/utils/router-utils.js | 7 +- client/app/scripts/utils/web-api-utils.js | 5 +- client/app/styles/contrast.less | 31 ++++++ client/app/styles/main.less | 127 +++++++++++----------- client/build/contrast.html | 20 ++++ client/server.js | 2 +- client/webpack.local.config.js | 5 + client/webpack.production.config.js | 1 + 12 files changed, 153 insertions(+), 75 deletions(-) create mode 100644 client/app/scripts/contrast-main.js create mode 100644 client/app/styles/contrast.less create mode 100644 client/build/contrast.html diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 7ac137463d..ea85045b59 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -51,7 +51,7 @@ export default class App extends React.Component { AppStore.addListener(this.onChange); window.addEventListener('keyup', this.onKeyPress); - getRouter().start({hashbang: true}); + getRouter(this.props.base).start({hashbang: true}); if (!AppStore.isRouteSet()) { // dont request topologies when already done via router getTopologies(AppStore.getActiveTopologyOptions()); @@ -75,6 +75,10 @@ export default class App extends React.Component { // width of details panel blocking a view const detailsWidth = showingDetails ? 450 : 0; const topMargin = 100; + const contrastMode = this.props.base.indexOf('contrast') > -1; + // link url to switch contrast with current UI state + const otherContrastModeUrl = contrastMode ? '/' : '/contrast.html'; + const otherContrastModeTitle = contrastMode ? 'Switch to normal contrast' : 'Switch to high contrast'; return (
@@ -112,9 +116,12 @@ export default class App extends React.Component { {this.state.version} on {this.state.hostname} -    - - Report an issue +   + + + + +
diff --git a/client/app/scripts/contrast-main.js b/client/app/scripts/contrast-main.js new file mode 100644 index 0000000000..29b3d735dc --- /dev/null +++ b/client/app/scripts/contrast-main.js @@ -0,0 +1,9 @@ +require('font-awesome-webpack'); +require('../styles/contrast.less'); + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './components/app.js'; + +ReactDOM.render(, document.getElementById('app')); diff --git a/client/app/scripts/main.js b/client/app/scripts/main.js index cac95b50fa..10cbcc26ad 100644 --- a/client/app/scripts/main.js +++ b/client/app/scripts/main.js @@ -6,4 +6,4 @@ import ReactDOM from 'react-dom'; import App from './components/app.js'; -ReactDOM.render(, document.getElementById('app')); +ReactDOM.render(, document.getElementById('app')); diff --git a/client/app/scripts/utils/color-utils.js b/client/app/scripts/utils/color-utils.js index 55e26359e2..f6c9d86dca 100644 --- a/client/app/scripts/utils/color-utils.js +++ b/client/app/scripts/utils/color-utils.js @@ -59,7 +59,9 @@ export function getNodeColorDark(text = '', secondText = '', isPseudo = false) { let hsl = color.hsl(); // ensure darkness - if (hsl.l > 0.7) { + if (hsl.h > 20 && hsl.h < 120) { + hsl = hsl.darker(2); + } else if (hsl.l > 0.7) { hsl = hsl.darker(1.5); } else { hsl = hsl.darker(1); diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index 12bd691316..f55a9af2db 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -16,7 +16,9 @@ export function updateRoute() { const state = AppStore.getAppState(); const stateUrl = JSON.stringify(state); const dispatch = false; - const urlStateString = window.location.hash.replace('#!/state/', '') || '{}'; + const urlStateString = window.location.hash + .replace('#!/state/', '') + .replace('#!/', '') || '{}'; const prevState = JSON.parse(urlStateString); if (shouldReplaceState(prevState, state)) { @@ -36,6 +38,7 @@ page('/state/:state', function(ctx) { route(state); }); -export function getRouter() { +export function getRouter(base) { + page.base(base); return page; } diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 7272ab6546..aab8056b46 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -8,8 +8,6 @@ import { clearControlError, closeWebsocket, openWebsocket, receiveError, import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer'; -const wsProto = location.protocol === 'https:' ? 'wss' : 'ws'; -const wsUrl = wsProto + '://' + location.host + location.pathname.replace(/\/$/, ''); const log = debug('scope:web-api-utils'); const reconnectTimerInterval = 5000; @@ -47,6 +45,9 @@ export function basePath(urlPath) { return parts.join('/').replace(/\/$/, ''); } +const wsProto = location.protocol === 'https:' ? 'wss' : 'ws'; +const wsUrl = wsProto + '://' + location.host + basePath(location.pathname); + function createWebsocket(topologyUrl, optionsQuery) { if (socket) { socket.onclose = null; diff --git a/client/app/styles/contrast.less b/client/app/styles/contrast.less new file mode 100644 index 0000000000..1931662bba --- /dev/null +++ b/client/app/styles/contrast.less @@ -0,0 +1,31 @@ +@import "main"; + +@background-color: lighten(@primary-color, 75%); +@background-lighter-color: lighten(@background-color, 10%); +@background-darker-color: darken(@background-color, 20%); +@background-darker-secondary-color: darken(@background-color, 15%); +@background-dark-color: @primary-color; +@text-color: darken(@primary-color, 20%); +@text-secondary-color: lighten(@text-color, 10%); +@text-tertiary-color: lighten(@text-color, 20%); +@border-light-color: lighten(@text-color, 50%); +@text-darker-color: darken(@text-color, 20%); +@white: @background-lighter-color; + +@node-opacity-blurred: 0.6; +@node-highlight-fill-opacity: 0.3; +@node-highlight-stroke-opacity: 0.5; +@node-highlight-stroke-width: 3px; +@node-border-stroke-width: 6px; +@node-pseudo-border-stroke-width: 2px; +@node-pseudo-opacity: 1; +@edge-highlight-opacity: 0.3; +@edge-opacity-blurred: 0; +@edge-opacity: 0.5; +@edge-link-stroke-width: 3px; + +@btn-opacity-default: 1; +@btn-opacity-hover: 1; +@btn-opacity-selected: 1; + +@link-opacity-default: 1; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 80202f17e9..ca1817f588 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -27,7 +27,6 @@ @background-darker-color: darken(@background-color, 8%); @background-darker-secondary-color: darken(@background-color, 4%); @background-dark-color: @primary-color; -@background-medium-color: lighten(@background-dark-color, 55%); @text-color: lighten(@primary-color, 10%); @text-secondary-color: lighten(@primary-color, 27%); @text-tertiary-color: lighten(@primary-color, 50%); @@ -40,6 +39,24 @@ @terminal-header-height: 34px; +@node-opacity-blurred: 0.25; +@node-highlight-fill-opacity: 0.1; +@node-highlight-stroke-opacity: 0.4; +@node-highlight-stroke-width: 1px; +@node-border-stroke-width: 3px; +@node-pseudo-border-stroke-width: 1px; +@node-pseudo-opacity: 0.8; +@edge-highlight-opacity: 0.1; +@edge-opacity-blurred: 0.2; +@edge-opacity: 0.5; +@edge-link-stroke-width: 1px; + +@btn-opacity-default: 0.7; +@btn-opacity-hover: 1; +@btn-opacity-selected: 0.9; + +@link-opacity-default: 0.8; + /* add this class to truncate text with ellipsis, container needs width */ .truncate { white-space: nowrap; @@ -77,12 +94,12 @@ .btn-opacity { .palable; - opacity: 0.8; + opacity: @btn-opacity-default; &-selected { - opacity: 0.9; + opacity: @btn-opacity-selected; } &:hover { - opacity: 1; + opacity: @btn-opacity-hover; } } @@ -170,6 +187,23 @@ h2 { &-label { text-transform: uppercase; margin: 0 0.25em; + + &-icon { + margin-left: 0.5em; + padding: 4px 3px; + color: @text-color; + position: relative; + border: 1px solid transparent; + border-radius: 10%; + &:hover { + border: 1px solid @text-secondary-color; + } + span { + font-size: 150%; + position: relative; + top: 2px; + } + } } } @@ -206,10 +240,9 @@ h2 { .topologies-sub-item { // border: 1px solid @background-darker-secondary-color; color: @text-secondary-color; - .palable; + .btn-opacity; cursor: pointer; padding: 4px 8px; - opacity: 0.7; border-radius: 4px; opacity: 0.8; margin-bottom: 3px; @@ -222,10 +255,6 @@ h2 { &-active { opacity: 0.85; } - - &:hover { - opacity: 1; - } } .topologies-sub-item { @@ -280,7 +309,7 @@ h2 { transition: opacity .5s ease-in-out; &.pseudo { - opacity: 0.8; + opacity: @node-pseudo-opacity; cursor: default; .node-label { @@ -293,12 +322,12 @@ h2 { .border { stroke: @text-tertiary-color; - stroke-width: 1px; + stroke-width: @node-pseudo-border-stroke-width; } } &.blurred { - opacity: 0.25; + opacity: @node-opacity-blurred; } } @@ -306,14 +335,14 @@ h2 { transition: opacity .5s ease-in-out; &.blurred { - opacity: 0.2; + opacity: @edge-opacity-blurred; } .link { stroke: @text-secondary-color; - stroke-width: 1px; + stroke-width: @edge-link-stroke-width; fill: none; - stroke-opacity: 0.5; + stroke-opacity: @edge-opacity; } .shadow { stroke: @weave-blue; @@ -323,14 +352,14 @@ h2 { } &.highlighted { .shadow { - stroke-opacity: 0.1; + stroke-opacity: @edge-highlight-opacity; } } } circle.border { - stroke-width: 3px; + stroke-width: @node-border-stroke-width; fill: none; } @@ -345,10 +374,10 @@ h2 { circle.highlighted { fill: @weave-blue; - fill-opacity: 0.1; + fill-opacity: @node-highlight-fill-opacity; stroke: @weave-blue; - stroke-width: 1px; - stroke-opacity: 0.4; + stroke-width: @node-highlight-stroke-width; + stroke-opacity: @node-highlight-stroke-opacity; } } @@ -390,18 +419,16 @@ h2 { right: 8px; span { - .palable; + .btn-opacity; padding: 4px 5px; margin-left: 2px; font-size: 110%; color: @white; cursor: pointer; - opacity: 0.7; border: 1px solid rgba(255, 255, 255, 0); border-radius: 10%; &:hover { - opacity: 1; border-color: rgba(255, 255, 255, 0.6); } } @@ -440,34 +467,25 @@ h2 { &-link { .truncate; - .palable; + .btn-opacity; display: inline-block; margin-right: 0.5em; cursor: pointer; text-decoration: underline; - opacity: 0.8; + opacity: @link-opacity-default; max-width: 12em; - - &:hover { - opacity: 1; - } } &-more { - .palable; + .btn-opacity; padding: 0 2px; text-transform: uppercase; cursor: pointer; - opacity: 0.7; font-size: 60%; font-weight: bold; display: inline-block; position: relative; top: -5px; - - &:hover { - opacity: 1; - } } } @@ -480,17 +498,15 @@ h2 { } .node-control-button { - .palable; + .btn-opacity; padding: 6px; margin-left: 2px; font-size: 110%; color: @white; cursor: pointer; - opacity: 0.7; border: 1px solid rgba(255, 255, 255, 0); border-radius: 10%; &:hover { - opacity: 1; border-color: rgba(255, 255, 255, 0.6); } &-pending, &-pending:hover { @@ -534,8 +550,7 @@ h2 { margin-top: 48px; text-align: center; font-size: 48px; - color: @background-medium-color; - opacity: 0.7; + color: @text-tertiary-color; } &-section { @@ -557,7 +572,7 @@ h2 { text-align: center; &-expand { - .palable; + .btn-opacity; margin: 4px 16px 0; border-top: 1px solid @border-light-color; text-transform: uppercase; @@ -565,15 +580,11 @@ h2 { color: @text-secondary-color; width: 100%; cursor: pointer; - opacity: 0.8; - - &:hover { - opacity: 1.0; - } + opacity: @link-opacity-default; } &-overflow { - .palable; + .btn-opacity; display: flex; flex-direction: row; flex-wrap: wrap; @@ -584,10 +595,6 @@ h2 { position: relative; padding-bottom: 16px; - &:hover { - opacity: 1; - } - &-expand { text-transform: uppercase; font-size: 70%; @@ -698,18 +705,13 @@ h2 { } &-more { - .palable; + .btn-opacity; padding: 2px 0; text-transform: uppercase; cursor: pointer; color: @text-secondary-color; - opacity: 0.7; font-size: 80%; font-weight: bold; - - &:hover { - opacity: 1; - } } &-node { @@ -721,14 +723,10 @@ h2 { } &-link { - .palable; + .btn-opacity; text-decoration: underline; cursor: pointer; - opacity: 0.8; - - &:hover { - opacity: 1; - } + opacity: @link-opacity-default; } &-value, &-metric { @@ -910,6 +908,7 @@ h2 { cursor: pointer; font-size: 90%; margin-left: 0.5em; + opacity: @link-opacity-default; } } } diff --git a/client/build/contrast.html b/client/build/contrast.html new file mode 100644 index 0000000000..11c0941ef2 --- /dev/null +++ b/client/build/contrast.html @@ -0,0 +1,20 @@ + + + + + Weave Scope + + + + + +
+
+
+ + + + + diff --git a/client/server.js b/client/server.js index ac8223c105..6efba3ec9d 100644 --- a/client/server.js +++ b/client/server.js @@ -19,7 +19,7 @@ var app = express(); // Serve application file depending on environment -app.get(/(app|terminal-app|vendors).js/, function(req, res) { +app.get(/(app|contrast-app|terminal-app|vendors).js/, function(req, res) { var filename = req.originalUrl; if (process.env.NODE_ENV === 'production') { res.sendFile(__dirname + '/build' + filename); diff --git a/client/webpack.local.config.js b/client/webpack.local.config.js index 19416e1f58..51604fcc39 100644 --- a/client/webpack.local.config.js +++ b/client/webpack.local.config.js @@ -28,6 +28,11 @@ module.exports = { 'webpack-dev-server/client?http://localhost:4041', 'webpack/hot/only-dev-server' ], + 'contrast-app': [ + './app/scripts/contrast-main', + 'webpack-dev-server/client?http://localhost:4041', + 'webpack/hot/only-dev-server' + ], 'terminal-app': [ './app/scripts/terminal-main', 'webpack-dev-server/client?http://localhost:4041', diff --git a/client/webpack.production.config.js b/client/webpack.production.config.js index a8a4d65769..1d56404b23 100644 --- a/client/webpack.production.config.js +++ b/client/webpack.production.config.js @@ -18,6 +18,7 @@ module.exports = { entry: { app: './app/scripts/main', + 'contrast-app': './app/scripts/contrast-main', 'terminal-app': './app/scripts/terminal-main', vendors: ['classnames', 'd3', 'dagre', 'flux', 'immutable', 'lodash', 'page', 'react', 'react-dom', 'react-motion']