Color input.

This commit is contained in:
Matt Swensen 2018-10-23 20:35:55 -06:00
parent 3316df73e5
commit 77872ffa77
No known key found for this signature in database
GPG Key ID: 3F9E482BFC526F35
10 changed files with 218 additions and 35 deletions

@ -10,7 +10,7 @@ export default class App extends PureComponent {
return (
<UrlStateProvider history={ this.props.history }>
<Color>
{ getColor => (
{ ({ getColor }) => (
<div className="app" style={{ backgroundColor: getColor('shade0') }}>
<div className="container">
<h1 style={{ color: getColor('shade7') }}>themer</h1>

@ -1,20 +1,31 @@
import React, { PureComponent } from 'react';
import { UrlStateConsumer } from './UrlState';
import { get } from 'lodash';
export default class Color extends PureComponent {
render() {
return (
<UrlStateConsumer>
{ ({ getValueOrFallback }) => this.props.children(
(...args) => getValueOrFallback(
[...args].map(colorKey => [
'colors',
getValueOrFallback([['activeColorSet']]),
colorKey
]),
true,
)
) }
{ ({ getValueOrFallback, mergeState, rawState }) => this.props.children({
getColor: (...args) => getValueOrFallback(
[...args].map(colorKey => [
'colors',
getValueOrFallback([['activeColorSet']]),
colorKey
]),
true,
),
setColor: (key, value) => {
mergeState({
colors: {
[getValueOrFallback([['activeColorSet']])]: {
[key]: value,
}
}
});
},
getRawColor: key => get(rawState, ['colors', getValueOrFallback([['activeColorSet']]), key], ''),
}) }
</UrlStateConsumer>
);
}

52
web/src/ColorInput.css Normal file

@ -0,0 +1,52 @@
.color-input {
--input-size: 1.5rem;
}
.inputs-wrapper {
display: flex;
align-items: center;
}
.label {
display: inline-block;
width: 8ch;
}
input[type="text"] {
font-family: "Fira Code", monospace;
font-size: 1rem;
line-height: 1rem;
border: none;
border-bottom-width: 0.125rem;
border-bottom-style: solid;
padding: 0.125rem 0.25rem;
background: none;
height: var(--input-size);
box-sizing: border-box;
}
.swatch {
height: var(--input-size);
width: var(--input-size);
border-top-left-radius: 0.125rem;
border-top-right-radius: 0.125rem;
border-bottom-right-radius: 0.125rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
input[type="color"] {
opacity: 0;
width: 0;
height: 0;
padding: 0;
margin: 0;
border: none;
outline: none;
}
.help {
font-size: 0.75rem;
}

53
web/src/ColorInput.js Normal file

@ -0,0 +1,53 @@
import React, { PureComponent } from 'react';
import Color from './Color';
import { Droplet } from './Icons';
import './ColorInput.css';
import getBestForeground from './getBestForeground';
export default class ColorInput extends PureComponent {
render() {
return (
<Color>
{ ({ getColor, getRawColor, setColor }) => (
<div className="color-input">
<div className="inputs-wrapper">
<label style={{ color: getColor('shade7') }}>
<span className="label">{ this.props.colorKey }</span>
<input
type="text"
style={{
color: getColor('shade7'),
borderBottomColor: getColor(this.props.colorKey, 'shade7'),
}}
value={ getRawColor(this.props.colorKey) }
onChange={ evt => setColor(this.props.colorKey, evt.target.value) }
/>
</label>
<label
className="swatch"
style={{
color: getBestForeground(
getColor('shade7'),
getColor('shade0'),
getColor(this.props.colorKey, 'shade7'),
),
backgroundColor: getColor(this.props.colorKey, 'shade7'),
}}
tabIndex="0"
>
<Droplet />
<input
type="color"
value={ getRawColor(this.props.colorKey) }
onChange={ evt => setColor(this.props.colorKey, evt.target.value) }
tabIndex="-1"
/>
</label>
</div>
<div className="help" style={{ color: getColor('shade4', 'shade7') }}>{ this.props.help }</div>
</div>
) }
</Color>
)
}
}

@ -2,7 +2,7 @@
font-size: 1rem;
font-family: 'Fira Code', monospace;
padding: 0.25em 0.5em;
border-width: .0625rem;
border-width: 0.0625rem;
border-style: solid;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
@ -11,3 +11,13 @@
.tab-container button + button {
margin-left: -0.0625rem;
}
.input-container {
padding: 1rem;
border-width: 0.0625rem;
border-style: solid;
margin-top: -0.0625rem;
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}

@ -2,26 +2,27 @@ import React, { PureComponent } from 'react';
import { UrlStateConsumer } from './UrlState';
import './ColorSetInputs.css';
import Color from './Color';
import ColorInput from './ColorInput';
export default class ColorSetInputs extends PureComponent {
render() {
return (
<section>
<UrlStateConsumer>
{ ({ getValueOrFallback, mergeState }) => {
const isDark = getValueOrFallback(['activeColorSet']) === 'dark';
return (
<Color>
{ getColor => {
const getTabStyle = active => ({
backgroundColor: active ? getColor('shade0') : getColor('shade2', 'shade0'),
color: getColor('shade7'),
borderTopColor: getColor('shade7'),
borderRightColor: getColor('shade7'),
borderBottomColor: active ? getColor('shade0') : getColor('shade7'),
borderLeftColor: getColor('shade7'),
});
return (
<UrlStateConsumer>
{ ({ getValueOrFallback, mergeState }) => {
const isDark = getValueOrFallback(['activeColorSet']) === 'dark';
return (
<Color>
{ ({ getColor }) => {
const getTabStyle = active => ({
backgroundColor: active ? getColor('shade0') : getColor('shade2', 'shade0'),
color: getColor('shade7'),
borderTopColor: getColor('shade7'),
borderRightColor: getColor('shade7'),
borderBottomColor: active ? getColor('shade0') : getColor('shade7'),
borderLeftColor: getColor('shade7'),
});
return (
<>
<div className="tab-container">
<button
style={ getTabStyle(isDark) }
@ -32,13 +33,25 @@ export default class ColorSetInputs extends PureComponent {
onClick={ () => mergeState({ activeColorSet: 'light' }) }
>Light Variant</button>
</div>
);
} }
</Color>
);
} }
</UrlStateConsumer>
</section>
<div className="input-container" style={{ borderColor: getColor('shade7') }}>
<ColorInput colorKey="shade0" help="background color" />
<ColorInput colorKey="shade7" help="foreground text" />
<ColorInput colorKey="accent0" help="error, vcs deletion" />
<ColorInput colorKey="accent1" help="syntax" />
<ColorInput colorKey="accent2" help="warning, vcs modification" />
<ColorInput colorKey="accent3" help="success, vcs addition" />
<ColorInput colorKey="accent4" help="syntax" />
<ColorInput colorKey="accent5" help="syntax" />
<ColorInput colorKey="accent6" help="syntax, caret/cursor" />
<ColorInput colorKey="accent7" help="syntax, special" />
</div>
</>
);
} }
</Color>
);
} }
</UrlStateConsumer>
);
}
}

22
web/src/Icons.js Normal file

@ -0,0 +1,22 @@
import React from 'react';
export const Droplet = () => (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3.004c.25.386.525.772.827 1.172.297.395 1.308 1.664 1.495 1.91C11.522 7.663 12 8.802 12 10.5c0 2.002-1.613 3.5-4 3.5s-4-1.498-4-3.5c0-1.698.48-2.837 1.678-4.414.187-.246 1.198-1.515 1.495-1.91.302-.4.576-.786.827-1.172z" stroke="currentColor" strokeWidth="2" fill="none" fillRule="evenodd"/>
</svg>
);
export const Check = ({ backgroundColor, outlineColor, checkColor }) => (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" style={{ backgroundColor, boxShadow: `inset 0 0 0 1px ${outlineColor}`, borderRadius: '2px' }}>
<path d="M4 9l2 2M6 11l6-6" stroke={ checkColor } strokeWidth="2" fill="none" fillRule="evenodd" strokeLinecap="square"/>
</svg>
);
export const Radio = ({ selected }) => (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<circle stroke="currentColor" cx="8" cy="8" r="7.5"/>
<circle fill={ selected ? 'currentColor' : 'transparent' } cx="8" cy="8" r="4"/>
</g>
</svg>
);

@ -67,6 +67,7 @@ export class UrlStateProvider extends Component {
render() {
return (
<UrlStateContext.Provider value={{
rawState: this.state,
getValueOrFallback: this.getValueOrFallback,
mergeState: this.mergeState,
}}>

@ -0,0 +1,13 @@
import Color from 'color';
export default (option1, option2, background) => {
const op1 = Color(option1);
const op2 = Color(option2);
const bg = Color(background);
if (op1.contrast(bg) > op2.contrast(bg)) {
return op1.hex();
}
else {
return op2.hex();
}
};

@ -0,0 +1,8 @@
import getBestForeground from './getBestForeground';
describe('getBestForeground', () => {
test('getBestForeground', () => {
expect(getBestForeground('#ccc', '#333', '#000')).toBe('#CCCCCC');
expect(getBestForeground('#ccc', '#333', '#fff')).toBe('#333333');
});
});