Clean up state derivation abstractions.

This commit is contained in:
Matt Swensen 2019-03-07 21:32:08 -07:00
parent 4745429549
commit 8f95f562b1
No known key found for this signature in database
GPG Key ID: 3F9E482BFC526F35
20 changed files with 1086 additions and 1029 deletions

@ -1,109 +1,9 @@
import React, { useState, useEffect } from 'react';
import styles from './App.module.css';
import { UrlStateProvider } from './UrlState';
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Main from './Main';
import ColorState from './ColorState';
import ColorSetInputs from './ColorSetInputs';
import TextPreviews from './TextPreviews';
import WallpaperPreview from './WallpaperPreview';
import PreBuiltList from './PreBuiltList';
import Download from './Download';
import Link from './Link';
import CopyUrl from './CopyUrl';
import StarCount from './StarCount';
export default ({ history }) => {
const [keyboarding, setKeyboarding] = useState(false);
const onKeyDown = (evt) => {
if (evt.key === 'Tab') {
setKeyboarding(true);
}
};
const onMouseDown = () => {
setKeyboarding(false);
}
useEffect(() => {
window.document.addEventListener('keydown', onKeyDown);
return () => {
window.document.removeEventListener('keydown', onKeyDown);
};
});
useEffect(() => {
window.document.addEventListener('mousedown', onMouseDown);
return () => {
window.document.removeEventListener('mousedown', onMouseDown);
}
});
return (
<UrlStateProvider history={ history }>
<ColorState>
{ ({ getColor }) => (
<div
className={ `${styles.app} ${keyboarding ? styles.keyboarding : ''}` }
style={{
backgroundColor: getColor('shade0'),
'--selection-foreground-color': getColor('shade0'),
'--selection-background-color': getColor('accent5'),
'--focus-outline-color': getColor('accent6'),
}}
>
<div className={ styles.container }>
<header className={ styles.header }>
<h1 className={ styles.h1 } style={{ color: getColor('shade7') }}>themer</h1>
<StarCount />
</header>
<hr
className={ styles.hr }
style={{
backgroundImage: `
linear-gradient(
to right,
${getColor('accent0', 'shade2', 'shade7')},
${getColor('accent1', 'shade2', 'shade7')},
${getColor('accent2', 'shade2', 'shade7')},
${getColor('accent3', 'shade2', 'shade7')},
${getColor('accent4', 'shade2', 'shade7')},
${getColor('accent4', 'shade2', 'shade7')},
${getColor('accent5', 'shade2', 'shade7')},
${getColor('accent6', 'shade2', 'shade7')},
${getColor('accent7', 'shade2', 'shade7')}
)
`,
}}
/>
<p style={{ color: getColor('shade6', 'shade7')}}>themer takes a set of colors and generates themes for your apps (editors, terminals, wallpapers, and more).</p>
<h2 className={ styles.h2 } style={{ color: getColor('shade7') }}>1. Define colors</h2>
<p className={ styles.help } style={{ color: getColor('shade6', 'shade7') }}>Input your colors using any CSS format (keyword, hsl, rgb, etc.).</p>
<ColorSetInputs />
<p className={ styles.preBuilt } style={{ color: getColor('shade6', 'shade7') }}>
Or start with a pre-built color set:
</p>
<PreBuiltList />
<h2 className={ styles.h2 } style={{ color: getColor('shade7')}}>2. Preview</h2>
<div className={ styles.previewsContainer }>
<TextPreviews />
<WallpaperPreview />
</div>
<h2 className={ styles.h2 } style={{ color: getColor('shade7')}}>3. Download</h2>
<p className={ styles.help } style={{ color: getColor('shade6', 'shade7') }}>Select which themes you'd like to generate from your color set.</p>
<Download />
</div>
<div className={ styles.shape } style={{ '--shape-color': getColor('shade1', 'shade0') }}>
<div className={ styles.container }>
<p style={{ color: getColor('shade7') }}>
<span style={{ color: getColor('accent1', 'shade7') }}>Pro tip:</span>
{' '}
The current URL uniquely identifies your current theme. Bookmark it, email it, or share it however you like.
<CopyUrl className={ styles.copyUrl }/>
</p>
</div>
</div>
<footer className={ styles.footer } style={{ color: getColor('shade3', 'shade7') }}>
themer is free and open source software, made by <Link href="https://mjswensen.com">mjswensen</Link> with <Link href="https://github.com/mjswensen/themer/graphs/contributors">contributors</Link>, and is released under the MIT license
</footer>
</div>
) }
</ColorState>
</UrlStateProvider>
);
}
export default ({ history }) => (
<ThemeProvider history={ history }>
<Main />
</ThemeProvider>
);

@ -1,6 +1,6 @@
import React, { forwardRef } from 'react';
import ColorState from './ColorState';
import React, { forwardRef, useContext } from 'react';
import styles from './Button.module.css';
import ThemeContext from './ThemeContext';
export default forwardRef(({
small,
@ -10,39 +10,36 @@ export default forwardRef(({
disabled,
children,
}, buttonRef) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
const getColorKeys = () => {
if (disabled) {
return ['shade3', 'shade0'];
return ['shade3'];
} else if (small || special) {
return ['shade7'];
} else {
return ['accent4', 'shade6', 'shade7'];
return ['accent4', 'shade6'];
}
}
return (
<ColorState>
{ ({ getColor }) => (
<button
className={ [
styles.button,
small && styles.small,
special && styles.special,
className,
].filter(Boolean).join(' ') }
data-text="Download"
style={{
color: getColor(...getColorKeys()),
'--button-resting-background-color': getColor('shade1', 'shade0'),
'--button-hover-background-color': getColor('shade2', 'shade0'),
'--button-active-background-color': getColor('shade0'),
'--button-special-color-1': getColor('accent1', 'shade7'),
'--button-special-color-2': getColor('accent7', 'shade7'),
}}
onClick={ onClick }
disabled={ disabled }
ref={ buttonRef }
>{ children }</button>
) }
</ColorState>
<button
className={ [
styles.button,
small && styles.small,
special && styles.special,
className,
].filter(Boolean).join(' ') }
data-text="Download"
style={{
color: getActiveColorOrFallback(getColorKeys()),
'--button-resting-background-color': getActiveColorOrFallback(['shade1'], true),
'--button-hover-background-color': getActiveColorOrFallback(['shade2'], true),
'--button-active-background-color': getActiveColorOrFallback(['shade0'], true),
'--button-special-color-1': getActiveColorOrFallback(['accent1']),
'--button-special-color-2': getActiveColorOrFallback(['accent7']),
}}
onClick={ onClick }
disabled={ disabled }
ref={ buttonRef }
>{ children }</button>
);
});

@ -1,29 +0,0 @@
import React from "react";
import { UrlStateConsumer } from "./UrlState";
export default ({ children }) => {
return (
<UrlStateConsumer>
{({ getValueOrFallback, mergeState }) =>
children({
getValue: () =>
getValueOrFallback(
[
[
"calculateIntermediaryShades",
getValueOrFallback([["activeColorSet"]])
]
],
v => v === "true"
),
setValue: v =>
mergeState({
calculateIntermediaryShades: {
[getValueOrFallback([["activeColorSet"]])]: v
}
})
})
}
</UrlStateConsumer>
);
};

@ -1,43 +1,44 @@
import React from 'react';
import React, { useContext } from 'react';
import { CheckIcon } from './Icons';
import styles from './Checkbox.module.css';
import ColorState from './ColorState';
import ThemeContext from './ThemeContext';
export default ({ className, value, accentSelected, onChange, label }) => (
<ColorState>
{ ({ getColor }) => (
<label
className={ [styles.wrapper, className].join(' ') }
style={{
color: value && accentSelected
? getColor('accent3', 'shade7')
: getColor('shade7'),
}}
tabIndex="0"
onKeyDown={ evt => {
if (evt.key === ' ' || evt.key === 'Enter') {
evt.preventDefault();
onChange(!value);
}
} }
>
<input
type="checkbox"
className={ styles.input }
checked={ value }
onChange={ evt => onChange(evt.target.checked) }
/>
<CheckIcon
backgroundColor={
value
? ( accentSelected ? getColor('accent3', 'shade7')
: getColor('shade7')) : 'transparent'
}
outlineColor={ value ? 'transparent' : getColor('shade7') }
checkColor={ value ? getColor('shade0') : 'transparent' }
/>
<span className={ styles.label }>{ label }</span>
</label>
) }
</ColorState>
);
export default ({ className, value, accentSelected, onChange, label }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<label
className={ [styles.wrapper, className].join(' ') }
style={{
color: value && accentSelected
? getActiveColorOrFallback(['accent3'])
: getActiveColorOrFallback(['shade7']),
}}
tabIndex="0"
onKeyDown={ evt => {
if (evt.key === ' ' || evt.key === 'Enter') {
evt.preventDefault();
onChange(!value);
}
} }
>
<input
type="checkbox"
className={ styles.input }
checked={ value }
onChange={ evt => onChange(evt.target.checked) }
/>
<CheckIcon
backgroundColor={
value
? ( accentSelected
? getActiveColorOrFallback(['accent3'])
: getActiveColorOrFallback('shade7')
) : 'transparent'
}
outlineColor={ value ? 'transparent' : getActiveColorOrFallback(['shade7']) }
checkColor={ value ? getActiveColorOrFallback(['shade0'], true) : 'transparent' }
/>
<span className={ styles.label }>{ label }</span>
</label>
);
};

@ -1,88 +1,85 @@
import React from 'react';
import ColorState from './ColorState';
import React, { useContext } from 'react';
import styles from './CodePreview.module.css';
import { cursor } from './cursor.module.css';
import ThemeContext from './ThemeContext';
export default () => {
const { activePreparedColorSet } = useContext(ThemeContext);
return (
<ColorState>
{ ({ getColor }) => (
<pre className={ styles.pre }>
<code style={{ color: getColor('shade7') }}>
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 1 </span>
<span style={{ color: getColor('accent6', 'shade7') }}>import</span>
{' map '}
<span style={{ color: getColor('accent6', 'shade7') }}>from </span>
<span style={{ color: getColor('accent3', 'shade7') }}>'map.js'</span>
;
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 2 </span>
<span style={{ color: getColor('accent6', 'shade7') }}>import</span>
{' basePickBy '}
<span style={{ color: getColor('accent6', 'shade7') }}>from </span>
<span style={{ color: getColor('accent3', 'shade7') }}>'./.internal/basePickBy.js'</span>
;
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 3 </span>
<span style={{ color: getColor('accent6', 'shade7') }}>import</span>
{' getAllKeysIn '}
<span style={{ color: getColor('accent6', 'shade7') }}>from </span>
<span style={{ color: getColor('accent3', 'shade7') }}>'./.internal/getAllKeysIn.js'</span>
;
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 4 </span>
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 5 </span>
<span style={{ color: getColor('shade2', 'shade7') }}>{'/**\n'}</span>
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 6 </span>
<span style={{ color: getColor('shade2', 'shade7') }}> * Creates an object composed of the `object` properties `predicate` returns{'\n'}</span>
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 7 </span>
<span style={{ color: getColor('shade2', 'shade7') }}> * truthy for. The predicate is invoked with two arguments: (value, key).{'\n'}</span>
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 8 </span>
<span style={{ color: getColor('shade2', 'shade7') }}> */{'\n'}</span>
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }> 9 </span>
<span style={{ color: getColor('accent7', 'shade7') }}>function </span>
<span style={{ color: getColor('accent2', 'shade7') }}>pickBy</span>
(object, predicate)
{' {\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>10 </span>
<span style={{ color: getColor('accent5', 'shade7') }}> if </span>
(object
<span style={{ color: getColor('accent5', 'shade7') }}> == </span>
<span style={{ color: getColor('accent7', 'shade7') }}>null</span>
{') {\n'}
<span style={{ color: getColor('shade5', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>11 </span>
<span style={{ color: getColor('accent5', 'shade7') }}> return </span>
{'{};'}
<span style={{ borderLeftColor: getColor('accent6', 'shade7') }} className={ [styles.cursor, cursor].join(' ') }></span>
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>12 </span>
{' }\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>13 </span>
<span style={{ color: getColor('accent7', 'shade7') }}> const </span>
props
<span style={{ color: getColor('accent5', 'shade7') }}> = </span>
map(getAllKeysIn(object), prop
<span style={{ color: getColor('accent7', 'shade7') }}> => </span>
{'[prop]);\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>14 </span>
<span style={{ color: getColor('accent5', 'shade7') }}> return </span>
basePickBy(object, props, (value, path)
<span style={{ color: getColor('accent7', 'shade7') }}> => </span>
predicate(value, path[
<span style={{ color: getColor('accent0', 'shade7') }}>0</span>
{']));\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>15 </span>
{'}\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>16 </span>
{'\n'}
<span style={{ color: getColor('shade1', 'shade7'), backgroundColor: getColor('shade0') }} className={ styles.lineNumber }>17 </span>
<span style={{ color: getColor('accent6', 'shade7') }}>export </span>
<span style={{ color: getColor('accent7', 'shade7') }}>default </span>
pickBy;
</code>
</pre>
) }
</ColorState>
<pre className={ styles.pre }>
<code style={{ color: activePreparedColorSet['shade7'] }}>
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 1 </span>
<span style={{ color: activePreparedColorSet['accent6'] }}>import</span>
{' map '}
<span style={{ color: activePreparedColorSet['accent6'] }}>from </span>
<span style={{ color: activePreparedColorSet['accent3'] }}>'map.js'</span>
;
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 2 </span>
<span style={{ color: activePreparedColorSet['accent6'] }}>import</span>
{' basePickBy '}
<span style={{ color: activePreparedColorSet['accent6'] }}>from </span>
<span style={{ color: activePreparedColorSet['accent3'] }}>'./.internal/basePickBy.js'</span>
;
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 3 </span>
<span style={{ color: activePreparedColorSet['accent6'] }}>import</span>
{' getAllKeysIn '}
<span style={{ color: activePreparedColorSet['accent6'] }}>from </span>
<span style={{ color: activePreparedColorSet['accent3'] }}>'./.internal/getAllKeysIn.js'</span>
;
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 4 </span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 5 </span>
<span style={{ color: activePreparedColorSet['shade2'] }}>{'/**\n'}</span>
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 6 </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> * Creates an object composed of the `object` properties `predicate` returns{'\n'}</span>
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 7 </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> * truthy for. The predicate is invoked with two arguments: (value, key).{'\n'}</span>
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 8 </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> */{'\n'}</span>
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }> 9 </span>
<span style={{ color: activePreparedColorSet['accent7'] }}>function </span>
<span style={{ color: activePreparedColorSet['accent2'] }}>pickBy</span>
(object, predicate)
{' {\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>10 </span>
<span style={{ color: activePreparedColorSet['accent5'] }}> if </span>
(object
<span style={{ color: activePreparedColorSet['accent5'] }}> == </span>
<span style={{ color: activePreparedColorSet['accent7'] }}>null</span>
{') {\n'}
<span style={{ color: activePreparedColorSet['shade5'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>11 </span>
<span style={{ color: activePreparedColorSet['accent5'] }}> return </span>
{'{};'}
<span style={{ borderLeftColor: activePreparedColorSet['accent6'] }} className={ [styles.cursor, cursor].join(' ') }></span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>12 </span>
{' }\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>13 </span>
<span style={{ color: activePreparedColorSet['accent7'] }}> const </span>
props
<span style={{ color: activePreparedColorSet['accent5'] }}> = </span>
map(getAllKeysIn(object), prop
<span style={{ color: activePreparedColorSet['accent7'] }}> => </span>
{'[prop]);\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>14 </span>
<span style={{ color: activePreparedColorSet['accent5'] }}> return </span>
basePickBy(object, props, (value, path)
<span style={{ color: activePreparedColorSet['accent7'] }}> => </span>
predicate(value, path[
<span style={{ color: activePreparedColorSet['accent0'] }}>0</span>
{']));\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>15 </span>
{'}\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>16 </span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade1'], backgroundColor: activePreparedColorSet['shade0'] }} className={ styles.lineNumber }>17 </span>
<span style={{ color: activePreparedColorSet['accent6'] }}>export </span>
<span style={{ color: activePreparedColorSet['accent7'] }}>default </span>
pickBy;
</code>
</pre>
);
};

@ -1,62 +1,63 @@
import React, { useRef } from 'react';
import ColorState from './ColorState';
import React, { useRef, useContext } from 'react';
import { DropletIcon } from './Icons';
import styles from './ColorInput.module.css';
import getBestForeground from './getBestForeground';
import colorSupport from './colorInputSupport';
import ThemeContext from './ThemeContext';
export default ({ className, style, colorKey, help }) => {
const textInput = useRef(null);
const {
getActiveColorOrFallback,
getActiveRawColor,
setActiveRawColor,
} = useContext(ThemeContext);
return (
<ColorState>
{ ({ getColor, getRawColor, setColor }) => (
<div className={ [styles.outerWrapper, className].join(' ') } style={ style }>
<div className={ styles.inputsWrapper }>
<label className={ styles.textInputWrapper } style={{ color: getColor('shade7') }}>
<span className={ styles.label }>{ colorKey }:</span>
<input
type="text"
className={ styles.textInput }
style={{
color: getColor('shade7'),
borderBottomColor: getColor(colorKey, 'shade7'),
}}
value={ getRawColor(colorKey) }
onChange={ evt => setColor(colorKey, evt.target.value) }
ref={ textInput }
/>
</label>
<label
className={ styles.swatch }
style={{
color: getBestForeground(
getColor('shade7'),
getColor('shade0'),
getColor(colorKey, 'shade7'),
),
backgroundColor: getColor(colorKey, 'shade7'),
}}
tabIndex="-1"
onClick={ evt => {
if(!colorSupport) {
evt.preventDefault();
textInput.current.focus()
}
} }
>
<DropletIcon />
<input
type="color"
className={ styles.colorInput }
value={ getColor(colorKey) }
onChange={ evt => setColor(colorKey, evt.target.value) }
tabIndex="-1"
/>
</label>
</div>
<div className={ styles.help } style={{ color: getColor('shade4', 'shade7') }}>{ help }</div>
</div>
) }
</ColorState>
<div className={ [styles.outerWrapper, className].join(' ') } style={ style }>
<div className={ styles.inputsWrapper }>
<label className={ styles.textInputWrapper } style={{ color: getActiveColorOrFallback(['shade7']) }}>
<span className={ styles.label }>{ colorKey }:</span>
<input
type="text"
className={ styles.textInput }
style={{
color: getActiveColorOrFallback(['shade7']),
borderBottomColor: getActiveColorOrFallback([colorKey]),
}}
value={ getActiveRawColor(colorKey) }
onChange={ evt => setActiveRawColor(colorKey, evt.target.value) }
ref={ textInput }
/>
</label>
<label
className={ styles.swatch }
style={{
color: getBestForeground(
getActiveColorOrFallback(['shade7']),
getActiveColorOrFallback(['shade0'], true),
getActiveColorOrFallback([colorKey]),
),
backgroundColor: getActiveColorOrFallback([colorKey]),
}}
tabIndex="-1"
onClick={ evt => {
if(!colorSupport) {
evt.preventDefault();
textInput.current.focus()
}
} }
>
<DropletIcon />
<input
type="color"
className={ styles.colorInput }
value={ getActiveColorOrFallback([colorKey], colorKey === 'shade0') }
onChange={ evt => setActiveRawColor(colorKey, evt.target.value) }
tabIndex="-1"
/>
</label>
</div>
<div className={ styles.help } style={{ color: getActiveColorOrFallback(['shade4']) }}>{ help }</div>
</div>
)
};

@ -1,78 +1,68 @@
import React from 'react';
import { UrlStateConsumer } from './UrlState';
import React, { useContext } from 'react';
import styles from './ColorSetInputs.module.css';
import ColorState from './ColorState';
import ColorInput from './ColorInput';
import Checkbox from './Checkbox';
import CalculateIntermediaryShadesState from './CalculateIntermediaryShadesState';
import Tabs from './Tabs';
import ThemeContext from './ThemeContext';
export default () => (
<UrlStateConsumer>
{ ({ getValueOrFallback, mergeState }) => {
const isDark = getValueOrFallback([['activeColorSet']]) === 'dark';
return (
<Tabs>
{ ({ tabClassName, getTabStyle, contentClassName, contentStyle }) => (
<ColorState>
{ () => (
<>
<div>
<button
className={ tabClassName }
style={ getTabStyle(isDark) }
onClick={ () => mergeState({ activeColorSet: 'dark' }) }
>Dark Variant</button>
<button
className={ tabClassName }
style={ getTabStyle(!isDark) }
onClick={ () => mergeState({ activeColorSet: 'light' }) }
>Light Variant</button>
</div>
<div className={ `${styles.inputContainer} ${contentClassName}` } style={ contentStyle }>
<ColorInput className={ styles.shade0 } colorKey="shade0" help="background color" />
<CalculateIntermediaryShadesState>
{ ({ getValue, setValue }) => (
<>
{ !getValue() && (
<>
<ColorInput className={ styles.shade1 } colorKey="shade1" help="UI" />
<ColorInput className={ styles.shade2 } colorKey="shade2" help="UI, text selection" />
<ColorInput className={ styles.shade3 } colorKey="shade3" help="UI, code comments" />
<ColorInput className={ styles.shade4 } colorKey="shade4" help="UI" />
<ColorInput className={ styles.shade5 } colorKey="shade5" help="UI" />
<ColorInput className={ styles.shade6 } colorKey="shade6" help="foreground text" />
</>
) }
<ColorInput
className={ [styles.shade7, getValue() ? styles.collapsed : ''].join(' ') }
colorKey="shade7"
help="foreground text"
/>
<Checkbox
className={ styles.checkbox }
label="calculate intermediary shades"
value={ getValue() }
onChange={ setValue }
/>
</>
) }
</CalculateIntermediaryShadesState>
<ColorInput className={ styles.accent0 } colorKey="accent0" help="error, vcs deletion" />
<ColorInput className={ styles.accent1 } colorKey="accent1" help="syntax" />
<ColorInput className={ styles.accent2 } colorKey="accent2" help="warning, vcs modification" />
<ColorInput className={ styles.accent3 } colorKey="accent3" help="success, vcs addition" />
<ColorInput className={ styles.accent4 } colorKey="accent4" help="syntax" />
<ColorInput className={ styles.accent5 } colorKey="accent5" help="syntax" />
<ColorInput className={ styles.accent6 } colorKey="accent6" help="syntax, caret/cursor" />
<ColorInput className={ styles.accent7 } colorKey="accent7" help="syntax, special" />
</div>
</>
) }
</ColorState>
) }
</Tabs>
);
} }
</UrlStateConsumer>
);
export default () => {
const {
activeCalculateIntermediaryShades,
setActiveCalculateIntermediaryShades,
activeColorSet,
setActiveColorSet,
} = useContext(ThemeContext);
const isDark = activeColorSet === 'dark';
return (
<Tabs>
{ ({ tabClassName, getTabStyle, contentClassName, contentStyle }) => (
<>
<div>
<button
className={ tabClassName }
style={ getTabStyle(isDark) }
onClick={ () => setActiveColorSet('dark') }
>Dark Variant</button>
<button
className={ tabClassName }
style={ getTabStyle(!isDark) }
onClick={ () => setActiveColorSet('light') }
>Light Variant</button>
</div>
<div className={ `${styles.inputContainer} ${contentClassName}` } style={ contentStyle }>
<ColorInput className={ styles.shade0 } colorKey="shade0" help="background color" />
{ !activeCalculateIntermediaryShades && (
<>
<ColorInput className={ styles.shade1 } colorKey="shade1" help="UI" />
<ColorInput className={ styles.shade2 } colorKey="shade2" help="UI, text selection" />
<ColorInput className={ styles.shade3 } colorKey="shade3" help="UI, code comments" />
<ColorInput className={ styles.shade4 } colorKey="shade4" help="UI" />
<ColorInput className={ styles.shade5 } colorKey="shade5" help="UI" />
<ColorInput className={ styles.shade6 } colorKey="shade6" help="foreground text" />
</>
) }
<ColorInput
className={ [styles.shade7, activeCalculateIntermediaryShades ? styles.collapsed : ''].join(' ') }
colorKey="shade7"
help="foreground text"
/>
<Checkbox
className={ styles.checkbox }
label="calculate intermediary shades"
value={ activeCalculateIntermediaryShades }
onChange={ setActiveCalculateIntermediaryShades }
/>
<ColorInput className={ styles.accent0 } colorKey="accent0" help="error, vcs deletion" />
<ColorInput className={ styles.accent1 } colorKey="accent1" help="syntax" />
<ColorInput className={ styles.accent2 } colorKey="accent2" help="warning, vcs modification" />
<ColorInput className={ styles.accent3 } colorKey="accent3" help="success, vcs addition" />
<ColorInput className={ styles.accent4 } colorKey="accent4" help="syntax" />
<ColorInput className={ styles.accent5 } colorKey="accent5" help="syntax" />
<ColorInput className={ styles.accent6 } colorKey="accent6" help="syntax, caret/cursor" />
<ColorInput className={ styles.accent7 } colorKey="accent7" help="syntax, special" />
</div>
</>
) }
</Tabs>
);
};

@ -1,61 +0,0 @@
import React from 'react';
import { UrlStateConsumer } from './UrlState';
import { get } from 'lodash';
import colorSteps from 'color-steps';
import Color from 'color';
import CalculateIntermediaryShadesState from './CalculateIntermediaryShadesState';
export default ({ children }) => (
<CalculateIntermediaryShadesState>
{ ({ getValue: shouldCalculateIntermediaryShades }) => (
<UrlStateConsumer>
{ ({ getValueOrFallback, mergeState, rawState }) => children({
getColor: (...args) => {
const parse = v => Color(v).hex();
const calculatedState = (() => {
if (shouldCalculateIntermediaryShades()) {
try {
const calculatedColors = colorSteps(
parse(get(rawState, ['colors', getValueOrFallback([['activeColorSet']]), 'shade0'], '')),
parse(get(rawState, ['colors', getValueOrFallback([['activeColorSet']]), 'shade7'], '')),
);
return {
colors: {
[getValueOrFallback([['activeColorSet']])]: calculatedColors.reduce(
(shades, color, idx) => ({
...shades,
[`shade${idx+1}`]: color,
}),
{},
)
},
};
} catch {}
}
return {};
})();
return getValueOrFallback(
args.map(colorKey => [
'colors',
getValueOrFallback([['activeColorSet']]),
colorKey
]),
parse,
calculatedState,
);
},
setColor: (key, value) => {
mergeState({
colors: {
[getValueOrFallback([['activeColorSet']])]: {
[key]: value,
}
}
});
},
getRawColor: key => get(rawState, ['colors', getValueOrFallback([['activeColorSet']]), key], ''),
}) }
</UrlStateConsumer>
) }
</CalculateIntermediaryShadesState>
);

@ -1,11 +1,10 @@
import React, { useState } from 'react';
import React, { useState, useContext } from 'react';
import Checkbox from './Checkbox';
import styles from './Download.module.css';
import ColorState from './ColorState';
import Button from './Button';
import generateZip from './generateZip';
import saveAs from 'file-saver';
import { UrlStateConsumer } from './UrlState';
import ThemeContext from './ThemeContext';
export default () => {
const [hyper, setHyper] = useState(false);
@ -35,243 +34,238 @@ export default () => {
const [sketchPalettes, setSketchPalettes] = useState(false);
const [tmux, setTmux] = useState(false);
const { getActiveColorOrFallback, preparedColorSet } = useContext(ThemeContext);
return (
<ColorState>
{ ({ getColor }) => (
<>
<div className={ styles.fieldsetWrapper }>
<fieldset style={{ borderColor: getColor('shade2', 'shade7') }}>
<legend style={{ color: getColor('shade5', 'shade7') }}>Terminals</legend>
<Checkbox
value={ hyper }
onChange={ () => setHyper(!hyper) }
label="Hyper"
accentSelected
/>
<Checkbox
value={ iterm }
onChange={ () => setIterm(!iterm) }
label="iTerm"
accentSelected
/>
<Checkbox
value={ gnomeTerminal }
onChange={ () => setGnomeTerminal(!gnomeTerminal) }
label="GNOME Terminal"
accentSelected
/>
<Checkbox
value={ conemu }
onChange={ () => setConemu(!conemu) }
label="ConEmu"
accentSelected
/>
<Checkbox
value={ cmd }
onChange={ () => setCmd(!cmd) }
label="CMD.exe"
accentSelected
/>
<Checkbox
value={ termite }
onChange={ () => setTermite(!termite) }
label="Termite"
accentSelected
/>
<Checkbox
value={ kitty }
onChange={ () => setKitty(!kitty) }
label="kitty"
accentSelected
/>
</fieldset>
<fieldset style={{ borderColor: getColor('shade2', 'shade7') }}>
<legend style={{ color: getColor('shade5', 'shade7') }}>Editors / IDEs</legend>
<Checkbox
value={ atomSyntax }
onChange={ () => setAtomSyntax(!atomSyntax) }
label="Atom (syntax)"
accentSelected
/>
<Checkbox
value={ atomUi }
onChange={ () => setAtomUi(!atomUi) }
label="Atom (UI)"
accentSelected
/>
<Checkbox
value={ sublimeText }
onChange={ () => setSublimeText(!sublimeText) }
label="Sublime Text"
accentSelected
/>
<Checkbox
value={ vim }
onChange={ () => setVim(!vim) }
label="Vim"
accentSelected
/>
<Checkbox
value={ vimLightline }
onChange={ () => setVimLightline(!vimLightline) }
label="lightline.vim"
accentSelected
/>
<Checkbox
value={ vscode }
onChange={ () => setVscode(!vscode) }
label="VS Code"
accentSelected
/>
<Checkbox
value={ xcode }
onChange={ () => setXcode(!xcode) }
label="Xcode"
accentSelected
/>
<Checkbox
value={ bbedit }
onChange={ () => setBbedit(!bbedit) }
label="BBEdit"
accentSelected
/>
<Checkbox
value={ jetbrains }
onChange={ () => setJetbrains(!jetbrains) }
label="JetBrains"
accentSelected
/>
</fieldset>
<fieldset style={{ borderColor: getColor('shade2', 'shade7') }}>
<legend style={{ color: getColor('shade5', 'shade7') }}>Wallpapers</legend>
<Checkbox
value={ wallpaperBlockWave }
onChange={ () => setWallpaperBlockWave(!wallpaperBlockWave) }
label="“Block Wave”"
accentSelected
/>
<Checkbox
value={ wallpaperOctagon }
onChange={ () => setWallpaperOctagon(!wallpaperOctagon) }
label="“Octagon”"
accentSelected
/>
<Checkbox
value={ wallpaperTriangles }
onChange={ () => setWallpaperTriangles(!wallpaperTriangles) }
label="“Triangles”"
accentSelected
/>
<Checkbox
value={ wallpaperTrianglify }
onChange={ () => setWallpaperTrianglify(!wallpaperTrianglify) }
label="“Trianglify”"
accentSelected
/>
<Checkbox
value={ wallpaperShirts }
onChange={ () => setWallpaperShirts(!wallpaperShirts) }
label="“Shirts”"
accentSelected
/>
<div
className={ styles.wallpaperHint }
style={{ color: getColor('shade3', 'shade7') }}
>
Wallpapers will be rendered at the browser viewport's resolution.
</div>
{ window.document.fullscreenEnabled ? (
<Button
className={ styles.fullscreen }
small
onClick={
() => window.document.documentElement.requestFullscreen()
}
>Go fullscreen</Button>
) : null }
</fieldset>
<fieldset style={{ borderColor: getColor('shade2', 'shade7') }}>
<legend style={{ color: getColor('shade5', 'shade7') }}>Other</legend>
<Checkbox
value={ slack }
onChange={ () => setSlack(!slack) }
label="Slack sidebar"
accentSelected
/>
<Checkbox
value={ alfred }
onChange={ () => setAlfred(!alfred) }
label="Alfred.app"
accentSelected
/>
<Checkbox
value={ chrome }
onChange={ () => setChrome(!chrome) }
label="Chrome"
accentSelected
/>
<Checkbox
value={ sketchPalettes }
onChange={ () => setSketchPalettes(!sketchPalettes) }
label="Sketch palettes"
accentSelected
/>
<Checkbox
value={ tmux }
onChange={ () => setTmux(!tmux) }
label="tmux"
accentSelected
/>
</fieldset>
<>
<div className={ styles.fieldsetWrapper }>
<fieldset style={{ borderColor: getActiveColorOrFallback(['shade2']) }}>
<legend style={{ color: getActiveColorOrFallback(['shade5']) }}>Terminals</legend>
<Checkbox
value={ hyper }
onChange={ () => setHyper(!hyper) }
label="Hyper"
accentSelected
/>
<Checkbox
value={ iterm }
onChange={ () => setIterm(!iterm) }
label="iTerm"
accentSelected
/>
<Checkbox
value={ gnomeTerminal }
onChange={ () => setGnomeTerminal(!gnomeTerminal) }
label="GNOME Terminal"
accentSelected
/>
<Checkbox
value={ conemu }
onChange={ () => setConemu(!conemu) }
label="ConEmu"
accentSelected
/>
<Checkbox
value={ cmd }
onChange={ () => setCmd(!cmd) }
label="CMD.exe"
accentSelected
/>
<Checkbox
value={ termite }
onChange={ () => setTermite(!termite) }
label="Termite"
accentSelected
/>
<Checkbox
value={ kitty }
onChange={ () => setKitty(!kitty) }
label="kitty"
accentSelected
/>
</fieldset>
<fieldset style={{ borderColor: getActiveColorOrFallback(['shade2']) }}>
<legend style={{ color: getActiveColorOrFallback(['shade5']) }}>Editors / IDEs</legend>
<Checkbox
value={ atomSyntax }
onChange={ () => setAtomSyntax(!atomSyntax) }
label="Atom (syntax)"
accentSelected
/>
<Checkbox
value={ atomUi }
onChange={ () => setAtomUi(!atomUi) }
label="Atom (UI)"
accentSelected
/>
<Checkbox
value={ sublimeText }
onChange={ () => setSublimeText(!sublimeText) }
label="Sublime Text"
accentSelected
/>
<Checkbox
value={ vim }
onChange={ () => setVim(!vim) }
label="Vim"
accentSelected
/>
<Checkbox
value={ vimLightline }
onChange={ () => setVimLightline(!vimLightline) }
label="lightline.vim"
accentSelected
/>
<Checkbox
value={ vscode }
onChange={ () => setVscode(!vscode) }
label="VS Code"
accentSelected
/>
<Checkbox
value={ xcode }
onChange={ () => setXcode(!xcode) }
label="Xcode"
accentSelected
/>
<Checkbox
value={ bbedit }
onChange={ () => setBbedit(!bbedit) }
label="BBEdit"
accentSelected
/>
<Checkbox
value={ jetbrains }
onChange={ () => setJetbrains(!jetbrains) }
label="JetBrains"
accentSelected
/>
</fieldset>
<fieldset style={{ borderColor: getActiveColorOrFallback(['shade2']) }}>
<legend style={{ color: getActiveColorOrFallback(['shade5']) }}>Wallpapers</legend>
<Checkbox
value={ wallpaperBlockWave }
onChange={ () => setWallpaperBlockWave(!wallpaperBlockWave) }
label="“Block Wave”"
accentSelected
/>
<Checkbox
value={ wallpaperOctagon }
onChange={ () => setWallpaperOctagon(!wallpaperOctagon) }
label="“Octagon”"
accentSelected
/>
<Checkbox
value={ wallpaperTriangles }
onChange={ () => setWallpaperTriangles(!wallpaperTriangles) }
label="“Triangles”"
accentSelected
/>
<Checkbox
value={ wallpaperTrianglify }
onChange={ () => setWallpaperTrianglify(!wallpaperTrianglify) }
label="“Trianglify”"
accentSelected
/>
<Checkbox
value={ wallpaperShirts }
onChange={ () => setWallpaperShirts(!wallpaperShirts) }
label="“Shirts”"
accentSelected
/>
<div
className={ styles.wallpaperHint }
style={{ color: getActiveColorOrFallback(['shade3']) }}
>
Wallpapers will be rendered at the browser viewport's resolution.
</div>
<UrlStateConsumer>
{ ({ rawState }) => (
<Button
special
onClick={ async () => {
const zip = await generateZip(
{
hyper,
iterm,
gnomeTerminal,
conemu,
cmd,
termite,
kitty,
atomSyntax,
atomUi,
sublimeText,
vim,
vimLightline,
vscode,
xcode,
bbedit,
jetbrains,
wallpaperBlockWave,
wallpaperOctagon,
wallpaperTriangles,
wallpaperTrianglify,
wallpaperShirts,
slack,
alfred,
chrome,
sketchPalettes,
tmux,
},
rawState.colors,
window.innerWidth * window.devicePixelRatio,
window.innerHeight * window.devicePixelRatio,
window.location.href,
);
zip.generateAsync({ type: 'blob' }).then(contents => {
saveAs(contents, 'themer.zip');
});
} }
>Download</Button>
) }
</UrlStateConsumer>
</>
) }
</ColorState>
{ window.document.fullscreenEnabled ? (
<Button
className={ styles.fullscreen }
small
onClick={
() => window.document.documentElement.requestFullscreen()
}
>Go fullscreen</Button>
) : null }
</fieldset>
<fieldset style={{ borderColor: getActiveColorOrFallback(['shade2']) }}>
<legend style={{ color: getActiveColorOrFallback(['shade5']) }}>Other</legend>
<Checkbox
value={ slack }
onChange={ () => setSlack(!slack) }
label="Slack sidebar"
accentSelected
/>
<Checkbox
value={ alfred }
onChange={ () => setAlfred(!alfred) }
label="Alfred.app"
accentSelected
/>
<Checkbox
value={ chrome }
onChange={ () => setChrome(!chrome) }
label="Chrome"
accentSelected
/>
<Checkbox
value={ sketchPalettes }
onChange={ () => setSketchPalettes(!sketchPalettes) }
label="Sketch palettes"
accentSelected
/>
<Checkbox
value={ tmux }
onChange={ () => setTmux(!tmux) }
label="tmux"
accentSelected
/>
</fieldset>
</div>
<Button
special
disabled={ !preparedColorSet.dark && !preparedColorSet.light }
onClick={ async () => {
const zip = await generateZip(
{
hyper,
iterm,
gnomeTerminal,
conemu,
cmd,
termite,
kitty,
atomSyntax,
atomUi,
sublimeText,
vim,
vimLightline,
vscode,
xcode,
bbedit,
jetbrains,
wallpaperBlockWave,
wallpaperOctagon,
wallpaperTriangles,
wallpaperTrianglify,
wallpaperShirts,
slack,
alfred,
chrome,
sketchPalettes,
tmux,
},
preparedColorSet,
window.innerWidth * window.devicePixelRatio,
window.innerHeight * window.devicePixelRatio,
window.location.href,
);
zip.generateAsync({ type: 'blob' }).then(contents => {
saveAs(contents, 'themer.zip');
});
} }
>Download</Button>
</>
);
}

@ -1,12 +1,11 @@
import React from "react";
import ColorState from "./ColorState";
import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";
export default props => (
<ColorState>
{({ getColor }) => (
<a style={{ color: getColor("accent5", "shade7") }} { ...props }>
{ props.children }
</a>
)}
</ColorState>
);
export default props => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<a style={{ color: getActiveColorOrFallback(['accent5']) }} { ...props }>
{ props.children }
</a>
);
};

103
web/src/Main.js Normal file

@ -0,0 +1,103 @@
import React, { useState, useEffect, useContext } from 'react';
import styles from './Main.module.css';
import ColorSetInputs from './ColorSetInputs';
import TextPreviews from './TextPreviews';
import WallpaperPreview from './WallpaperPreview';
import PreBuiltList from './PreBuiltList';
import Download from './Download';
import Link from './Link';
import CopyUrl from './CopyUrl';
import StarCount from './StarCount';
import ThemeContext from './ThemeContext';
export default () => {
const [keyboarding, setKeyboarding] = useState(false);
const onKeyDown = (evt) => {
if (evt.key === 'Tab') {
setKeyboarding(true);
}
};
const onMouseDown = () => {
setKeyboarding(false);
}
useEffect(() => {
window.document.addEventListener('keydown', onKeyDown);
return () => {
window.document.removeEventListener('keydown', onKeyDown);
};
});
useEffect(() => {
window.document.addEventListener('mousedown', onMouseDown);
return () => {
window.document.removeEventListener('mousedown', onMouseDown);
}
});
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<div
className={ `${styles.app} ${keyboarding ? styles.keyboarding : ''}` }
style={{
backgroundColor: getActiveColorOrFallback(['shade0'], true),
'--selection-foreground-color': getActiveColorOrFallback(['shade0'], true),
'--selection-background-color': getActiveColorOrFallback(['accent5']),
'--focus-outline-color': getActiveColorOrFallback(['accent6']),
}}
>
<div className={ styles.container }>
<header className={ styles.header }>
<h1 className={ styles.h1 } style={{ color: getActiveColorOrFallback(['shade7']) }}>themer</h1>
<StarCount />
</header>
<hr
className={ styles.hr }
style={{
backgroundImage: `
linear-gradient(
to right,
${getActiveColorOrFallback(['accent0', 'shade2'])},
${getActiveColorOrFallback(['accent1', 'shade2'])},
${getActiveColorOrFallback(['accent2', 'shade2'])},
${getActiveColorOrFallback(['accent3', 'shade2'])},
${getActiveColorOrFallback(['accent4', 'shade2'])},
${getActiveColorOrFallback(['accent4', 'shade2'])},
${getActiveColorOrFallback(['accent5', 'shade2'])},
${getActiveColorOrFallback(['accent6', 'shade2'])},
${getActiveColorOrFallback(['accent7', 'shade2'])}
)
`,
}}
/>
<p style={{ color: getActiveColorOrFallback(['shade6'])}}>themer takes a set of colors and generates themes for your apps (editors, terminals, wallpapers, and more).</p>
<h2 className={ styles.h2 } style={{ color: getActiveColorOrFallback(['shade7']) }}>1. Define colors</h2>
<p className={ styles.help } style={{ color: getActiveColorOrFallback(['shade6']) }}>Input your colors using any CSS format (keyword, hsl, rgb, etc.).</p>
<ColorSetInputs />
<p className={ styles.preBuilt } style={{ color: getActiveColorOrFallback(['shade6']) }}>
Or start with a pre-built color set:
</p>
<PreBuiltList />
<h2 className={ styles.h2 } style={{ color: getActiveColorOrFallback(['shade7'])}}>2. Preview</h2>
<div className={ styles.previewsContainer }>
<TextPreviews />
<WallpaperPreview />
</div>
<h2 className={ styles.h2 } style={{ color: getActiveColorOrFallback(['shade7'])}}>3. Download</h2>
<p className={ styles.help } style={{ color: getActiveColorOrFallback(['shade6']) }}>Select which themes you'd like to generate from your color set.</p>
<Download />
</div>
<div className={ styles.shape } style={{ '--shape-color': getActiveColorOrFallback(['shade1'], true) }}>
<div className={ styles.container }>
<p style={{ color: getActiveColorOrFallback(['shade7']) }}>
<span style={{ color: getActiveColorOrFallback(['accent1']) }}>Pro tip:</span>
{' '}
The current URL uniquely identifies your current theme. Bookmark it, email it, or share it however you like.
<CopyUrl className={ styles.copyUrl }/>
</p>
</div>
</div>
<footer className={ styles.footer } style={{ color: getActiveColorOrFallback(['shade3']) }}>
themer is free and open source software, made by <Link href="https://mjswensen.com">mjswensen</Link> with <Link href="https://github.com/mjswensen/themer/graphs/contributors">contributors</Link>, and is released under the MIT license
</footer>
</div>
);
}

@ -1,9 +1,8 @@
import React from 'react';
import { UrlStateConsumer, paramsFromState } from './UrlState';
import ColorState from './ColorState';
import React, { useContext } from 'react';
import Link from './Link';
import { has } from 'lodash';
import styles from './PreBuiltList.module.css';
import ThemeContext, { paramsFromState } from './ThemeContext';
import { colors as defaultColors } from 'themer-colors-default';
import { colors as nightSkyColors } from 'themer-colors-night-sky';
@ -16,51 +15,46 @@ import { colors as solarizedColors } from 'themer-colors-solarized';
import { colors as githubUniverseColors } from 'themer-colors-github-universe';
import { colors as novaColors } from 'themer-colors-nova';
const PreBuiltLink = ({ colors, children }) => (
<UrlStateConsumer>
{ ({ getValueOrFallback }) => {
const activeColorSet = getValueOrFallback([['activeColorSet']]);
const oppositeColorSet = activeColorSet === 'dark' ? 'light' : 'dark';
const preparedState = {
colors,
activeColorSet: has(colors, activeColorSet) ? activeColorSet : oppositeColorSet,
calculateIntermediaryShades: {
dark: !has(colors, 'dark.shade1'),
light: !has(colors, 'light.shade1'),
},
};
return (<Link href={ paramsFromState(preparedState) }>{ children }</Link>);
} }
</UrlStateConsumer>
);
const PreBuiltLink = ({ colors, children }) => {
const { activeColorSet } = useContext(ThemeContext);
const oppositeColorSet = activeColorSet === 'dark' ? 'light' : 'dark';
const preparedState = {
colors,
activeColorSet: has(colors, activeColorSet) ? activeColorSet : oppositeColorSet,
calculateIntermediaryShades: {
dark: !has(colors, 'dark.shade1'),
light: !has(colors, 'light.shade1'),
},
};
return (<Link href={ paramsFromState(preparedState) }>{ children }</Link>);
};
export default () => (
<ColorState>
{ ({ getColor }) => (
<ul className={ styles.linksets } style={{ color: getColor('shade5', 'shade7') }}>
<li>
Original color sets:
{' '}
<ul className={ styles.links } >
<li><PreBuiltLink colors={ defaultColors }>Default</PreBuiltLink></li>
<li><PreBuiltLink colors={ nightSkyColors }>Night Sky</PreBuiltLink></li>
<li><PreBuiltLink colors={ polarIceColors }>Polar Ice</PreBuiltLink></li>
<li><PreBuiltLink colors={ fingerPaintColors }>Finger Paint</PreBuiltLink></li>
<li><PreBuiltLink colors={ monkeyColors }>Monkey</PreBuiltLink></li>
</ul>
</li>
<li>
Ports from third party themes:
{' '}
<ul className={ styles.links }>
<li><PreBuiltLink colors={ oneColors }>One</PreBuiltLink></li>
<li><PreBuiltLink colors={ lucidColors }>Lucid</PreBuiltLink></li>
<li><PreBuiltLink colors={ solarizedColors }>Solarized</PreBuiltLink></li>
<li><PreBuiltLink colors={ githubUniverseColors }>GitHub Universe</PreBuiltLink></li>
<li><PreBuiltLink colors={ novaColors }>Nova</PreBuiltLink></li>
</ul>
</li>
</ul>
) }
</ColorState>
);
export default () => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<ul className={ styles.linksets } style={{ color: getActiveColorOrFallback(['shade5']) }}>
<li>
Original color sets:
{' '}
<ul className={ styles.links } >
<li><PreBuiltLink colors={ defaultColors }>Default</PreBuiltLink></li>
<li><PreBuiltLink colors={ nightSkyColors }>Night Sky</PreBuiltLink></li>
<li><PreBuiltLink colors={ polarIceColors }>Polar Ice</PreBuiltLink></li>
<li><PreBuiltLink colors={ fingerPaintColors }>Finger Paint</PreBuiltLink></li>
<li><PreBuiltLink colors={ monkeyColors }>Monkey</PreBuiltLink></li>
</ul>
</li>
<li>
Ports from third party themes:
{' '}
<ul className={ styles.links }>
<li><PreBuiltLink colors={ oneColors }>One</PreBuiltLink></li>
<li><PreBuiltLink colors={ lucidColors }>Lucid</PreBuiltLink></li>
<li><PreBuiltLink colors={ solarizedColors }>Solarized</PreBuiltLink></li>
<li><PreBuiltLink colors={ githubUniverseColors }>GitHub Universe</PreBuiltLink></li>
<li><PreBuiltLink colors={ novaColors }>Nova</PreBuiltLink></li>
</ul>
</li>
</ul>
);
};

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useContext } from 'react';
import numeral from 'numeral';
import styles from './StarCount.module.css';
import ColorState from './ColorState';
import ThemeContext from './ThemeContext';
export default () => {
const [count, setCount] = useState('');
@ -16,34 +16,32 @@ export default () => {
})();
}, []);
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<ColorState>
{ ({ getColor }) => (
<span
style={{
'--star-count-resting-background-color': getColor('shade1', 'shade0'),
'--star-count-hover-background-color': getColor('shade2', 'shade0'),
'--star-count-active-background-color': getColor('shade0'),
}}
>
<a
className={ styles.star }
style={{ color: getColor('shade7') }}
href="https://github.com/mjswensen/themer"
target="_blank"
rel="noopener noreferrer"
>Star on GitHub</a>
{ count ? (
<a
className={ styles.stargazers }
style={{ color: getColor('shade7') }}
href="https://github.com/mjswensen/themer/stargazers"
target="_blank"
rel="noopener noreferrer"
>{ count }</a>
) : null }
</span>
) }
</ColorState>
<span
style={{
'--star-count-resting-background-color': getActiveColorOrFallback(['shade1'], true),
'--star-count-hover-background-color': getActiveColorOrFallback(['shade2'], true),
'--star-count-active-background-color': getActiveColorOrFallback(['shade0'], true),
}}
>
<a
className={ styles.star }
style={{ color: getActiveColorOrFallback(['shade7']) }}
href="https://github.com/mjswensen/themer"
target="_blank"
rel="noopener noreferrer"
>Star on GitHub</a>
{ count ? (
<a
className={ styles.stargazers }
style={{ color: getActiveColorOrFallback(['shade7']) }}
href="https://github.com/mjswensen/themer/stargazers"
target="_blank"
rel="noopener noreferrer"
>{ count }</a>
) : null }
</span>
);
}

@ -1,23 +1,26 @@
import React from 'react';
import ColorState from './ColorState';
import { useContext } from 'react';
import styles from './Tabs.module.css';
import ThemeContext from './ThemeContext';
export default ({ children }) => (
<ColorState>
{ ({ getColor }) => children({
tabClassName: styles.tab,
getTabStyle: active => ({
backgroundColor: active ? getColor('shade0') : getColor('shade2', 'shade0'),
color: getColor('shade7'),
borderColor: getColor('shade7'),
'--tab-bottom-overlap-color': active ? getColor('shade0') : getColor('shade2', 'shade0'),
'--tab-bottom-overlap-size': active ? 'calc(var(--border-size) * 2)' : 'var(--border-size)',
}),
contentClassName: styles.tabContent,
contentStyle: {
borderColor: getColor('shade7'),
backgroundColor: getColor('shade0'),
},
}) }
</ColorState>
);
export default ({ children }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return children({
tabClassName: styles.tab,
getTabStyle: active => ({
backgroundColor: active ? getActiveColorOrFallback(['shade0'], true) : getActiveColorOrFallback(['shade2'], true),
color: getActiveColorOrFallback(['shade7']),
borderColor: getActiveColorOrFallback(['shade7']),
'--tab-bottom-overlap-color': active
? getActiveColorOrFallback(['shade0'], true)
: getActiveColorOrFallback(['shade2'], true),
'--tab-bottom-overlap-size': active
? 'calc(var(--border-size) * 2)'
: 'var(--border-size)',
}),
contentClassName: styles.tabContent,
contentStyle: {
borderColor: getActiveColorOrFallback(['shade7']),
backgroundColor: getActiveColorOrFallback(['shade0'], true),
},
});
};

@ -1,73 +1,72 @@
import React from 'react';
import ColorState from './ColorState';
import React, { useContext } from 'react';
import styles from './TerminalPreview.module.css';
import { cursor } from './cursor.module.css';
import ThemeContext from './ThemeContext';
export default () => (
<ColorState>
{ ({ getColor }) => (
<pre className={ styles.pre }>
<code>
<span style={{ color: getColor('accent4') }}>~/project</span>
<span style={{ color: getColor('accent7') }}>(branch*) </span>
<span style={{ color: getColor('accent3') }}>|> </span>
<span style={{ color: getColor('shade5', 'shade7') }}>yarn test</span>
{'\n'}
<span style={{ color: getColor('shade2', 'shade7') }}>$ jest</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer/lib/</span>
<span style={{ color: getColor('shade7') }}>prepare.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-wallpaper-triangles/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-vscode/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-utils/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-wallpaper-octagon/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-atom-syntax/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('shade0'), backgroundColor: getColor('accent3', 'shade7') }}> PASS </span>
<span style={{ color: getColor('shade2', 'shade7') }}> packages/themer-chrome/lib/</span>
<span style={{ color: getColor('shade7') }}>index.spec.js</span>
{'\n'}
<span style={{ color: getColor('accent3', 'shade7') }}>...</span>
{'\n\n'}
<span style={{ color: getColor('shade7') }}>Test Suites: </span>
<span style={{ color: getColor('accent4', 'shade7') }}>42 passed</span>
<span style={{ color: getColor('shade6', 'shade7') }}>, 42 total</span>
{'\n'}
<span style={{ color: getColor('shade7') }}>Tests: </span>
<span style={{ color: getColor('accent4', 'shade7') }}>145 passed</span>
<span style={{ color: getColor('shade6', 'shade7') }}>, 145 total</span>
{'\n'}
<span style={{ color: getColor('shade7') }}>Snapshots: </span>
<span style={{ color: getColor('accent4', 'shade7') }}>102 passed</span>
<span style={{ color: getColor('shade6', 'shade7') }}>, 102 total</span>
{'\n'}
<span style={{ color: getColor('shade7') }}>Time: </span>
<span style={{ color: getColor('shade6', 'shade7') }}>5.626s</span>
{'\n'}
<span style={{ color: getColor('shade3', 'shade7') }}>Ran all test suites.</span>
{'\n'}
<span style={{ color: getColor('accent4') }}>~/project</span>
<span style={{ color: getColor('accent7') }}>(branch*) </span>
<span style={{ color: getColor('accent3') }}>|> </span>
<span style={{ backgroundColor: getColor('shade5', 'shade7') }} className={ cursor }> </span>
</code>
</pre>
) }
</ColorState>
);
export default () => {
const { activePreparedColorSet } = useContext(ThemeContext);
return (
<pre className={ styles.pre }>
<code>
<span style={{ color: activePreparedColorSet['accent4'] }}>~/project</span>
<span style={{ color: activePreparedColorSet['accent7'] }}>(branch*) </span>
<span style={{ color: activePreparedColorSet['accent3'] }}>|> </span>
<span style={{ color: activePreparedColorSet['shade5'] }}>yarn test</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade2'] }}>$ jest</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>prepare.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-wallpaper-triangles/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-vscode/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-utils/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-wallpaper-octagon/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-atom-syntax/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade0'], backgroundColor: activePreparedColorSet['accent3'] }}> PASS </span>
<span style={{ color: activePreparedColorSet['shade2'] }}> packages/themer-chrome/lib/</span>
<span style={{ color: activePreparedColorSet['shade7'] }}>index.spec.js</span>
{'\n'}
<span style={{ color: activePreparedColorSet['accent3'] }}>...</span>
{'\n\n'}
<span style={{ color: activePreparedColorSet['shade7'] }}>Test Suites: </span>
<span style={{ color: activePreparedColorSet['accent4'] }}>42 passed</span>
<span style={{ color: activePreparedColorSet['shade6'] }}>, 42 total</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade7'] }}>Tests: </span>
<span style={{ color: activePreparedColorSet['accent4'] }}>145 passed</span>
<span style={{ color: activePreparedColorSet['shade6'] }}>, 145 total</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade7'] }}>Snapshots: </span>
<span style={{ color: activePreparedColorSet['accent4'] }}>102 passed</span>
<span style={{ color: activePreparedColorSet['shade6'] }}>, 102 total</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade7'] }}>Time: </span>
<span style={{ color: activePreparedColorSet['shade6'] }}>5.626s</span>
{'\n'}
<span style={{ color: activePreparedColorSet['shade3'] }}>Ran all test suites.</span>
{'\n'}
<span style={{ color: activePreparedColorSet['accent4'] }}>~/project</span>
<span style={{ color: activePreparedColorSet['accent7'] }}>(branch*) </span>
<span style={{ color: activePreparedColorSet['accent3'] }}>|> </span>
<span style={{ backgroundColor: activePreparedColorSet['shade5'] }} className={ cursor }> </span>
</code>
</pre>
);
};

275
web/src/ThemeContext.js Normal file

@ -0,0 +1,275 @@
import React, { createContext, useState, useEffect } from "react";
import qs from 'qs';
import { get, merge } from 'lodash';
import Color from 'color';
import colorSteps from 'color-steps';
const stateFromParams = search =>
qs.parse(search, { allowDots: true, ignoreQueryPrefix: true });
export const paramsFromState = state =>
qs.stringify(state, { allowDots: true, addQueryPrefix: true });
const DEFAULT_STATE = {
colors: {
dark: {
shade0: '#000000',
shade7: '#FFFFFF',
},
light: {
shade0: '#FFFFFF',
shade7: '#000000',
},
},
activeColorSet: 'dark',
calculateIntermediaryShades: {
dark: true,
light: true,
},
};
const stringToBooleanOrDefault = (value, defaultValue) => {
switch (value) {
case 'true':
return true;
case 'false':
return false;
default:
return defaultValue;
}
}
const ThemeContext = createContext();
export const ThemeProvider = ({ history, children }) => {
const [rawState, setRawState] = useState(stateFromParams(history.location.search));
useEffect(() => {
return history.listen(location => {
setRawState(stateFromParams(location.search));
});
});
const mergeState = newState => history.replace(paramsFromState(merge({}, rawState, newState)));
const activeColorSet = ['dark', 'light'].includes(rawState.activeColorSet)
? rawState.activeColorSet
: DEFAULT_STATE.activeColorSet;
const setActiveColorSet = value => mergeState({
activeColorSet: value,
});
const calculateIntermediaryDarkShades = stringToBooleanOrDefault(
get(rawState, 'calculateIntermediaryShades.dark'),
DEFAULT_STATE.calculateIntermediaryShades.dark,
)
const calculateIntermediaryLightShades = stringToBooleanOrDefault(
get(rawState, 'calculateIntermediaryShades.light'),
DEFAULT_STATE.calculateIntermediaryShades.light,
);
const activeCalculateIntermediaryShades = activeColorSet === 'dark'
? calculateIntermediaryDarkShades
: calculateIntermediaryLightShades;
const setActiveCalculateIntermediaryShades = value => mergeState({
calculateIntermediaryShades: {
[activeColorSet]: value,
},
});
const parseSafe = v => {
try {
if (v === undefined) return;
return Color(v).hex();
} catch {}
};
const darkShade0 = parseSafe(get(rawState, 'colors.dark.shade0'));
const darkShade7 = parseSafe(get(rawState, 'colors.dark.shade7'));
const lightShade0 = parseSafe(get(rawState, 'colors.light.shade0'));
const lightShade7 = parseSafe(get(rawState, 'colors.light.shade7'));
const preparedColors = {
dark: {
shade0: darkShade0,
...(calculateIntermediaryDarkShades && darkShade0 && darkShade7
? colorSteps(darkShade0, darkShade7).reduce(
(shades, color, idx) => ({
...shades,
[`shade${idx+1}`]: Color(color).hex(),
}),
{},
)
: {
shade1: parseSafe(get(rawState, 'colors.dark.shade1')),
shade2: parseSafe(get(rawState, 'colors.dark.shade2')),
shade3: parseSafe(get(rawState, 'colors.dark.shade3')),
shade4: parseSafe(get(rawState, 'colors.dark.shade4')),
shade5: parseSafe(get(rawState, 'colors.dark.shade5')),
shade6: parseSafe(get(rawState, 'colors.dark.shade6')),
}
),
shade7: darkShade7,
accent0: parseSafe(get(rawState, 'colors.dark.accent0')),
accent1: parseSafe(get(rawState, 'colors.dark.accent1')),
accent2: parseSafe(get(rawState, 'colors.dark.accent2')),
accent3: parseSafe(get(rawState, 'colors.dark.accent3')),
accent4: parseSafe(get(rawState, 'colors.dark.accent4')),
accent5: parseSafe(get(rawState, 'colors.dark.accent5')),
accent6: parseSafe(get(rawState, 'colors.dark.accent6')),
accent7: parseSafe(get(rawState, 'colors.dark.accent7')),
},
light: {
shade0: lightShade0,
...(calculateIntermediaryLightShades && lightShade0 && lightShade7
? colorSteps(lightShade0, lightShade7).reduce(
(shades, color, idx) => ({
...shades,
[`shade${idx+1}`]: color,
}),
{},
)
: {
shade1: parseSafe(get(rawState, 'colors.light.shade1')),
shade2: parseSafe(get(rawState, 'colors.light.shade2')),
shade3: parseSafe(get(rawState, 'colors.light.shade3')),
shade4: parseSafe(get(rawState, 'colors.light.shade4')),
shade5: parseSafe(get(rawState, 'colors.light.shade5')),
shade6: parseSafe(get(rawState, 'colors.light.shade6')),
}
),
shade7: lightShade7,
accent0: parseSafe(get(rawState, 'colors.light.accent0')),
accent1: parseSafe(get(rawState, 'colors.light.accent1')),
accent2: parseSafe(get(rawState, 'colors.light.accent2')),
accent3: parseSafe(get(rawState, 'colors.light.accent3')),
accent4: parseSafe(get(rawState, 'colors.light.accent4')),
accent5: parseSafe(get(rawState, 'colors.light.accent5')),
accent6: parseSafe(get(rawState, 'colors.light.accent6')),
accent7: parseSafe(get(rawState, 'colors.light.accent7')),
},
}
const getPreparedColor = (variant, keys, fallbackKey) => {
for (const key of keys) {
const preparedColor = get(preparedColors, [variant, key]);
if (preparedColor !== undefined) {
return preparedColor;
}
}
const preparedFallback = get(preparedColors, [variant, fallbackKey]);
if (preparedFallback !== undefined) {
return preparedFallback;
} else {
return get(DEFAULT_STATE, ['colors', variant, fallbackKey]);
}
}
const getColorOrFallback = (variant, keys, background = false) =>
getPreparedColor(variant, keys, background ? 'shade0' : 'shade7');
const getActiveColorOrFallback = (keys, background) =>
getColorOrFallback(activeColorSet, keys, background);
const preparedFullColorSet = {
dark: {
shade0: getColorOrFallback('dark', ['shade0'], true),
shade1: getColorOrFallback('dark', ['shade1']),
shade2: getColorOrFallback('dark', ['shade2']),
shade3: getColorOrFallback('dark', ['shade3']),
shade4: getColorOrFallback('dark', ['shade4']),
shade5: getColorOrFallback('dark', ['shade5']),
shade6: getColorOrFallback('dark', ['shade6']),
shade7: getColorOrFallback('dark', ['shade7']),
accent0: getColorOrFallback('dark', ['accent0']),
accent1: getColorOrFallback('dark', ['accent1']),
accent2: getColorOrFallback('dark', ['accent2']),
accent3: getColorOrFallback('dark', ['accent3']),
accent4: getColorOrFallback('dark', ['accent4']),
accent5: getColorOrFallback('dark', ['accent5']),
accent6: getColorOrFallback('dark', ['accent6']),
accent7: getColorOrFallback('dark', ['accent7']),
},
light: {
shade0: getColorOrFallback('light', ['shade0'], true),
shade1: getColorOrFallback('light', ['shade1']),
shade2: getColorOrFallback('light', ['shade2']),
shade3: getColorOrFallback('light', ['shade3']),
shade4: getColorOrFallback('light', ['shade4']),
shade5: getColorOrFallback('light', ['shade5']),
shade6: getColorOrFallback('light', ['shade6']),
shade7: getColorOrFallback('light', ['shade7']),
accent0: getColorOrFallback('light', ['accent0']),
accent1: getColorOrFallback('light', ['accent1']),
accent2: getColorOrFallback('light', ['accent2']),
accent3: getColorOrFallback('light', ['accent3']),
accent4: getColorOrFallback('light', ['accent4']),
accent5: getColorOrFallback('light', ['accent5']),
accent6: getColorOrFallback('light', ['accent6']),
accent7: getColorOrFallback('light', ['accent7']),
}
};
const activePreparedColorSet = preparedFullColorSet[activeColorSet];
const colorKeys = [
'shade0',
'shade1',
'shade2',
'shade3',
'shade4',
'shade5',
'shade6',
'shade7',
'accent0',
'accent1',
'accent2',
'accent3',
'accent4',
'accent5',
'accent6',
'accent7',
];
const preparedColorSet = {
...(
colorKeys.some(key => !!get(rawState, ['colors', 'dark', key]))
? { dark: preparedFullColorSet.dark }
: null
),
...(
colorKeys.some(key => !!get(rawState, ['colors', 'light', key]))
? { light: preparedFullColorSet.light }
: null
),
};
const getActiveRawColor = key => get(rawState, ['colors', activeColorSet, key], '');
const setActiveRawColor = (key, value) => mergeState({
colors: {
[activeColorSet]: {
[key]: value,
},
},
});
return (
<ThemeContext.Provider value={{
activeColorSet,
setActiveColorSet,
activeCalculateIntermediaryShades,
setActiveCalculateIntermediaryShades,
getActiveRawColor,
getActiveColorOrFallback,
preparedColorSet,
activePreparedColorSet,
setActiveRawColor,
}}>
{ children }
</ThemeContext.Provider>
);
}
export default ThemeContext;

@ -1,83 +0,0 @@
import React, { useState, useEffect } from 'react';
import qs from 'qs';
import { merge } from 'lodash';
import getValueOrFallback from './getValueOrFallback';
const UrlStateContext = React.createContext();
const stateFromParams = search => qs.parse(search, { allowDots: true, ignoreQueryPrefix: true });
export const paramsFromState = state => qs.stringify(state, { allowDots: true, addQueryPrefix: true });
const fallbackState = {
colors: {
dark: {
shade0: '#000000',
shade1: '#000000',
shade2: '#000000',
shade3: '#000000',
shade4: '#000000',
shade5: '#000000',
shade6: '#000000',
shade7: '#FFFFFF',
accent0: '#FFFFFF',
accent1: '#FFFFFF',
accent2: '#FFFFFF',
accent3: '#FFFFFF',
accent4: '#FFFFFF',
accent5: '#FFFFFF',
accent6: '#FFFFFF',
accent7: '#FFFFFF',
},
light: {
shade0: '#FFFFFF',
shade1: '#FFFFFF',
shade2: '#FFFFFF',
shade3: '#FFFFFF',
shade4: '#FFFFFF',
shade5: '#FFFFFF',
shade6: '#FFFFFF',
shade7: '#000000',
accent0: '#000000',
accent1: '#000000',
accent2: '#000000',
accent3: '#000000',
accent4: '#000000',
accent5: '#000000',
accent6: '#000000',
accent7: '#000000',
},
},
activeColorSet: 'dark',
calculateIntermediaryShades: {
dark: true,
light: true,
},
};
export const UrlStateProvider = ({ history, children }) => {
const [state, setState] = useState(stateFromParams(history.location.search));
useEffect(() => {
return history.listen(location => {
setState(stateFromParams(location.search));
})
});
return (
<UrlStateContext.Provider value={{
rawState: state,
getValueOrFallback: (paths, parse, calculatedState) => getValueOrFallback(
state,
calculatedState,
fallbackState,
paths,
parse,
),
mergeState: newState => history.replace(paramsFromState(merge({}, state, newState))),
}}>
{ children }
</UrlStateContext.Provider>
);
}
export const UrlStateConsumer = UrlStateContext.Consumer;

@ -1,5 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import ColorState from './ColorState';
import React, { useState, useEffect, useRef, useContext } from 'react';
import Button from './Button';
import styles from './WallpaperModal.module.css';
@ -8,6 +7,7 @@ import { render as octagonRender } from 'themer-wallpaper-octagon';
import { render as shirtsRender } from 'themer-wallpaper-shirts';
import { render as trianglesRender } from 'themer-wallpaper-triangles';
import { render as trianglifyRender } from 'themer-wallpaper-trianglify';
import ThemeContext from './ThemeContext';
const getImagePromises = (pkg, colors, width, height) => {
const options = { [`${pkg}-size`]: `${width}x${height}` };
@ -42,6 +42,8 @@ export default ({ onClose, wallpaper, colors }) => {
};
});
const { getActiveColorOrFallback } = useContext(ThemeContext);
const node = useRef(null);
const button = useRef(null);
@ -72,28 +74,24 @@ export default ({ onClose, wallpaper, colors }) => {
}, [button]);
return (
<ColorState>
{ ({ getColor }) => (
<div
className={ styles.scrim }
style={{ backgroundColor: getActiveColorOrFallback(['shade0'], true) }}
ref={ node }
>
{ image ? (
<div
className={ styles.scrim }
style={{ backgroundColor: getColor('shade0') }}
ref={ node }
>
{ image ? (
<div
className={ styles.image }
style={{ backgroundImage: image }}
/>
) : (
<span style={{ color: getColor('shade2', 'shade7') }}>loading...</span>
) }
<Button
className={ styles.close }
onClick={ onClose }
ref={ button }
>close</Button>
</div>
className={ styles.image }
style={{ backgroundImage: image }}
/>
) : (
<span style={{ color: getActiveColorOrFallback(['shade2']) }}>loading...</span>
) }
</ColorState>
<Button
className={ styles.close }
onClick={ onClose }
ref={ button }
>close</Button>
</div>
);
}

@ -1,9 +1,9 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useContext } from 'react';
import Tabs from './Tabs';
import WallpaperModal from './WallpaperModal';
import Button from './Button';
import styles from './WallpaperPreview.module.css';
import ColorState from './ColorState';
import ThemeContext from './ThemeContext';
const wallpaperOptions = [
{ value: 'themer-wallpaper-block-wave', label: '"Block Wave"'},
@ -26,6 +26,8 @@ export default () => {
setActivePreview(null);
}
const { activePreparedColorSet } = useContext(ThemeContext);
return (
<Tabs>
{ ({ tabClassName, getTabStyle, contentClassName, contentStyle }) => (
@ -45,34 +47,13 @@ export default () => {
)) }
</div>
{ activePreview ? (
<ColorState>
{ ({ getColor }) => (
<WallpaperModal
wallpaper={ activePreview }
colors={{
current: {
shade0: getColor('shade0'),
shade1: getColor('shade1'),
shade2: getColor('shade2'),
shade3: getColor('shade3'),
shade4: getColor('shade4'),
shade5: getColor('shade5'),
shade6: getColor('shade6'),
shade7: getColor('shade7'),
accent0: getColor('accent0'),
accent1: getColor('accent1'),
accent2: getColor('accent2'),
accent3: getColor('accent3'),
accent4: getColor('accent4'),
accent5: getColor('accent5'),
accent6: getColor('accent6'),
accent7: getColor('accent7'),
}
}}
onClose={ onModalClose }
/>
) }
</ColorState>
<WallpaperModal
wallpaper={ activePreview }
colors={{
current: activePreparedColorSet
}}
onClose={ onModalClose }
/>
) : null }
</div>
</div>