Custom themes (#632)

* Custom themes

* Fix theme selection

* fixes and add more theme highlights

* Fix remove bug

* change create button hoverBackground

* change id format

* fixes

* remove default highlights, style tweaks

* move consturctor to componentDidMount, create constants

* simplify color picker style api'

* replace notAllowed with disabled

* remove itemWrapperProps from Dropdown

* create itemWrapper class prop

* remove onClose from Dropdown

* fix ColorPicker and font sizes

* increase specificity of selector
main
Sean 6 years ago committed by Michael Fix
parent f45d133d3c
commit a1e05af5c0

@ -11,7 +11,6 @@ const Button = ({
hoverBackground = COLORS.HOVER, hoverBackground = COLORS.HOVER,
hoverColor, hoverColor,
disabled, disabled,
notAllowed,
selected, selected,
children, children,
border, border,
@ -22,13 +21,7 @@ const Button = ({
padding = 0, padding = 0,
margin = 0 margin = 0
}) => ( }) => (
<button <button id={id} onClick={onClick} className={className} disabled={disabled} style={style}>
id={id}
onClick={onClick}
className={className}
disabled={disabled || notAllowed}
style={style}
>
{children} {children}
<style jsx> <style jsx>
{` {`
@ -38,7 +31,7 @@ const Button = ({
background-color: ${background}; background-color: ${background};
color: ${color}; color: ${color};
box-shadow: ${border ? `inset 0px 0px 0px ${selected ? 2 : 1}px ${color}` : 'initial'}; box-shadow: ${border ? `inset 0px 0px 0px ${selected ? 2 : 1}px ${color}` : 'initial'};
cursor: ${notAllowed ? 'not-allowed' : 'pointer'}; cursor: ${disabled ? 'not-allowed' : 'pointer'};
outline: none; outline: none;
border: none; border: none;
padding: ${padding}; padding: ${padding};

@ -4,18 +4,15 @@ import { SketchPicker } from 'react-color'
import { COLORS } from '../lib/constants' import { COLORS } from '../lib/constants'
const pickerStyle = { const pickerStyle = {
picker: { backgroundColor: COLORS.BLACK,
backgroundColor: COLORS.BLACK, padding: '8px 8px 0',
padding: '8px 8px 0', margin: '0 auto 1px'
margin: '0 auto 1px',
flex: '1 0 200px'
}
} }
const ColorPicker = ({ onChange = () => {}, color, presets }) => ( const ColorPicker = ({ onChange = () => {}, color, presets, style }) => (
<React.Fragment> <React.Fragment>
<SketchPicker <SketchPicker
styles={pickerStyle} styles={{ picker: style || pickerStyle }}
color={color} color={color}
onChangeComplete={onChange} onChangeComplete={onChange}
presetColors={presets} presetColors={presets}

@ -43,10 +43,9 @@ class Dropdown extends React.PureComponent {
// clear on open // clear on open
if (changes.isOpen) { if (changes.isOpen) {
inputValue = '' inputValue = ''
} this.props.onOpen && this.props.onOpen()
} else if (changes.isOpen === false && !inputValue) {
// set on close // set on close
if (changes.isOpen === false && !inputValue) {
inputValue = this.props.selected.name inputValue = this.props.selected.name
} }
} }
@ -58,7 +57,7 @@ class Dropdown extends React.PureComponent {
userInputtedValue = '' userInputtedValue = ''
render() { render() {
const { color, selected, onChange, itemWrapper, icon } = this.props const { color, selected, onChange, itemWrapper, icon, disableInput } = this.props
const { itemsToShow, inputValue } = this.state const { itemsToShow, inputValue } = this.state
const minWidth = calcMinWidth(itemsToShow) const minWidth = calcMinWidth(itemsToShow)
@ -78,14 +77,15 @@ class Dropdown extends React.PureComponent {
selected, selected,
minWidth, minWidth,
itemWrapper, itemWrapper,
icon icon,
disableInput
})} })}
</Downshift> </Downshift>
) )
} }
} }
const renderDropdown = ({ color, list, minWidth, itemWrapper, icon }) => ({ const renderDropdown = ({ color, list, minWidth, itemWrapper, icon, disableInput }) => ({
isOpen, isOpen,
highlightedIndex, highlightedIndex,
selectedItem, selectedItem,
@ -103,6 +103,7 @@ const renderDropdown = ({ color, list, minWidth, itemWrapper, icon }) => ({
isOpen={isOpen} isOpen={isOpen}
color={color} color={color}
hasIcon={!!icon} hasIcon={!!icon}
disabled={disableInput}
> >
{selectedItem.name} {selectedItem.name}
</SelectedItem> </SelectedItem>
@ -112,10 +113,11 @@ const renderDropdown = ({ color, list, minWidth, itemWrapper, icon }) => ({
<ListItem <ListItem
key={index} key={index}
color={color} color={color}
item={item}
itemWrapper={itemWrapper} itemWrapper={itemWrapper}
{...getItemProps({ {...getItemProps({
item, item,
isSelected: selectedItem === item, isSelected: selectedItem.name === item.name,
isHighlighted: highlightedIndex === index isHighlighted: highlightedIndex === index
})} })}
> >
@ -181,7 +183,8 @@ const SelectedItem = ({
children, children,
isOpen, isOpen,
color, color,
hasIcon hasIcon,
disabled
}) => { }) => {
const itemColor = color || COLORS.SECONDARY const itemColor = color || COLORS.SECONDARY
@ -192,7 +195,7 @@ const SelectedItem = ({
className={`dropdown-display ${isOpen ? 'is-open' : ''}`} className={`dropdown-display ${isOpen ? 'is-open' : ''}`}
> >
<input <input
{...getInputProps({ placeholder: children, id: `downshift-input-${children}` })} {...getInputProps({ placeholder: children, id: `downshift-input-${children}`, disabled })}
className="dropdown-display-text" className="dropdown-display-text"
/> />
<div className="dropdown-arrow"> <div className="dropdown-arrow">
@ -260,13 +263,13 @@ const ListItems = ({ children, color }) => {
) )
} }
const ListItem = ({ children, color, isHighlighted, isSelected, itemWrapper, ...rest }) => { const ListItem = ({ children, color, isHighlighted, isSelected, itemWrapper, item, ...rest }) => {
const itemColor = color || COLORS.SECONDARY const itemColor = color || COLORS.SECONDARY
return ( return (
<li {...rest} role="option" className="dropdown-list-item"> <li {...rest} role="option" className="dropdown-list-item">
{itemWrapper ? ( {itemWrapper ? (
itemWrapper({ children, color: itemColor }) itemWrapper({ children, color: itemColor, item, isSelected })
) : ( ) : (
<span className="dropdown-list-item-text">{children}</span> <span className="dropdown-list-item-text">{children}</span>
)} )}

@ -16,14 +16,12 @@ import Toolbar from './Toolbar'
import Overlay from './Overlay' import Overlay from './Overlay'
import Carbon from './Carbon' import Carbon from './Carbon'
import ExportMenu from './ExportMenu' import ExportMenu from './ExportMenu'
import Themes from './Themes'
import { import {
THEMES,
THEMES_HASH,
LANGUAGES, LANGUAGES,
LANGUAGE_MIME_HASH, LANGUAGE_MIME_HASH,
LANGUAGE_MODE_HASH, LANGUAGE_MODE_HASH,
LANGUAGE_NAME_HASH, LANGUAGE_NAME_HASH,
DEFAULT_THEME,
DEFAULT_EXPORT_SIZE, DEFAULT_EXPORT_SIZE,
COLORS, COLORS,
EXPORT_SIZES_HASH, EXPORT_SIZES_HASH,
@ -35,9 +33,7 @@ import {
import { serializeState, getQueryStringState } from '../lib/routing' import { serializeState, getQueryStringState } from '../lib/routing'
import { getSettings, escapeHtml, unescapeHtml, formatCode, omit } from '../lib/util' import { getSettings, escapeHtml, unescapeHtml, formatCode, omit } from '../lib/util'
import LanguageIcon from './svg/Language' import LanguageIcon from './svg/Language'
import ThemeIcon from './svg/Theme'
const themeIcon = <ThemeIcon />
const languageIcon = <LanguageIcon /> const languageIcon = <LanguageIcon />
class Editor extends React.Component { class Editor extends React.Component {
@ -256,7 +252,7 @@ class Editor extends React.Component {
} }
updateTheme(theme) { updateTheme(theme) {
this.updateSetting('theme', theme.id) this.updateSetting('theme', theme)
} }
updateLanguage(language) { updateLanguage(language) {
@ -325,12 +321,7 @@ class Editor extends React.Component {
<React.Fragment> <React.Fragment>
<div className="editor"> <div className="editor">
<Toolbar> <Toolbar>
<Dropdown <Themes key={theme} updateTheme={this.updateTheme} theme={theme} />
icon={themeIcon}
selected={THEMES_HASH[theme] || DEFAULT_THEME}
list={THEMES}
onChange={this.updateTheme}
/>
<Dropdown <Dropdown
icon={languageIcon} icon={languageIcon}
selected={ selected={

@ -11,7 +11,8 @@ const Input = ({
title, title,
type, type,
value, value,
align = 'right' align = 'right',
maxLength
}) => { }) => {
return ( return (
<React.Fragment> <React.Fragment>
@ -23,6 +24,7 @@ const Input = ({
value={value} value={value}
name={name} name={name}
onChange={onChange} onChange={onChange}
maxLength={maxLength}
/> />
<style jsx> <style jsx>
{` {`

@ -5,6 +5,11 @@ import { COLORS } from '../lib/constants'
import { toggle } from '../lib/util' import { toggle } from '../lib/util'
class ListSetting extends React.Component { class ListSetting extends React.Component {
static defaultProps = {
onOpen: () => {},
onClose: () => {}
}
state = { isVisible: false } state = { isVisible: false }
select = id => { select = id => {
@ -13,7 +18,11 @@ class ListSetting extends React.Component {
} }
} }
toggle = () => this.setState(toggle('isVisible')) toggle = () => {
const handler = this.state.isVisible ? this.props.onClose : this.props.onOpen
handler()
this.setState(toggle('isVisible'))
}
renderListItems() { renderListItems() {
return this.props.items.map(item => ( return this.props.items.map(item => (
@ -45,15 +54,16 @@ class ListSetting extends React.Component {
} }
render() { render() {
const selectedItem = this.props.items.filter(item => item.id === this.props.selected)[0] || {} const { items, selected, title, children } = this.props
const { isVisible } = this.state
const selectedItem = items.filter(item => item.id === selected)[0] || {}
return ( return (
<div className="list-select-container"> <div className="list-select-container">
<div <div className={`display ${isVisible ? 'is-visible' : ''}`} onClick={this.toggle}>
className={`display ${this.state.isVisible ? 'is-visible' : ''}`} <span className="label">{title}</span>
onClick={this.toggle} {children(selectedItem)}
>
<span className="label">{this.props.title}</span>
{this.props.children(selectedItem)}
</div> </div>
<div className="list">{this.renderListItems()}</div> <div className="list">{this.renderListItems()}</div>
<style jsx> <style jsx>

@ -80,12 +80,12 @@ const Presets = React.memo(
{show && ( {show && (
<Button <Button
margin="0 0 0 8px" margin="0 0 0 8px"
flex="0 0 48px" flex="0 0 54px"
color={COLORS.GRAY} color={COLORS.GRAY}
hoverBackground="transparent" hoverBackground="transparent"
hoverColor={disabledCreate ? COLORS.GRAY : COLORS.SECONDARY} hoverColor={disabledCreate ? COLORS.GRAY : COLORS.SECONDARY}
onClick={create} onClick={create}
notAllowed={disabledCreate} disabled={disabledCreate}
> >
create + create +
</Button> </Button>
@ -145,10 +145,6 @@ const Presets = React.memo(
align-items: center; align-items: center;
} }
.settings-presets-header > span {
font-size: 14px;
}
.settings-presets-content { .settings-presets-content {
display: flex; display: flex;
overflow-x: scroll; overflow-x: scroll;

@ -9,7 +9,7 @@ import Popout, { managePopout } from './Popout'
import Button from './Button' import Button from './Button'
import Presets from './Presets' import Presets from './Presets'
import { COLORS, DEFAULT_PRESETS } from '../lib/constants' import { COLORS, DEFAULT_PRESETS } from '../lib/constants'
import { toggle, getPresets, savePresets } from '../lib/util' import { toggle, getPresets, savePresets, generateId } from '../lib/util'
import SettingsIcon from './svg/Settings' import SettingsIcon from './svg/Settings'
import * as Arrows from './svg/Arrows' import * as Arrows from './svg/Arrows'
@ -275,9 +275,7 @@ class Settings extends React.PureComponent {
createPreset = async () => { createPreset = async () => {
const newPreset = this.getSettingsFromProps() const newPreset = this.getSettingsFromProps()
newPreset.id = `preset:${Math.random() newPreset.id = `preset:${generateId()}`
.toString(36)
.slice(2)}`
newPreset.custom = true newPreset.custom = true
newPreset.icon = await this.props.getCarbonImage({ newPreset.icon = await this.props.getCarbonImage({

@ -0,0 +1,402 @@
import React from 'react'
import Dropdown from './Dropdown'
import Input from './Input'
import Button from './Button'
import ListSetting from './ListSetting'
import Popout, { managePopout } from './Popout'
import ColorPicker from './ColorPicker'
import ThemeIcon from './svg/Theme'
import RemoveIcon from './svg/Remove'
import { THEMES, HIGHLIGHT_KEYS, COLORS, DEFAULT_THEME } from '../lib/constants'
import { getThemes, saveThemes, capitalize, stringifyRGBA, generateId } from '../lib/util'
const colorPickerStyle = {
backgroundColor: COLORS.BLACK,
padding: 0,
margin: '4px'
}
const colorPresets = []
const HighlightPicker = ({ title, onChange, color }) => (
<div className="color-picker-container">
<div className="color-picker-header">
<span>{title}</span>
</div>
<ColorPicker
color={color}
onChange={onChange}
presets={colorPresets}
style={colorPickerStyle}
/>
<style jsx>
{`
.color-picker-container {
width: 218px;
border-left: 2px solid ${COLORS.SECONDARY};
padding: 2px;
}
.color-picker-header {
background-color: ${COLORS.BLACK};
display: flex;
justify-content: center;
align-items: center;
padding: 8px 0;
}
`}
</style>
</div>
)
const ThemeCreate = ({
theme,
themes,
highlights,
name,
preset,
selected,
createTheme,
applyPreset,
updateName,
selectHighlight,
updateHighlight
}) => (
<Popout pointerLeft="15px" style={{ display: 'flex' }}>
<div className="theme-settings">
<div className="field name-field">
<span>Name</span>
<Input
title="name"
name="name"
placeholder="Custom Theme"
value={name}
onChange={updateName}
maxLength="32"
/>
</div>
<div className="theme-select">
<ListSetting
title="Preset"
items={themes}
selected={preset || theme}
onOpen={selectHighlight(null)}
onChange={applyPreset}
>
{({ name }) => <span>{name}</span>}
</ListSetting>
</div>
<div className="theme-colors">
{HIGHLIGHT_KEYS.map(key => (
<div className="field" key={key}>
<Button
padding="4px 4px 4px 8px"
onClick={selectHighlight(key)}
background={selected === key ? COLORS.HOVER : COLORS.BLACK}
>
<div className="row">
<span>{capitalize(key)}</span>
<span
className="color-square"
style={{
backgroundColor: highlights[key],
boxShadow: `inset 0px 0px 0px ${selected === key ? 2 : 1}px ${COLORS.SECONDARY}`
}}
/>
</div>
</Button>
</div>
))}
</div>
<Button
center
disabled={!name}
className="create-button"
padding="8px 0"
background={COLORS.SECONDARY}
hoverBackground={COLORS.SECONDARY}
color={COLORS.BLACK}
onClick={createTheme}
>
Create +
</Button>
</div>
{selected && (
<HighlightPicker
title={capitalize(selected)}
color={highlights[selected]}
onChange={updateHighlight}
/>
)}
<style jsx>
{`
.field {
align-items: center;
border-bottom: solid 1px ${COLORS.SECONDARY};
display: flex;
height: 35px;
justify-content: space-between;
position: relative;
}
.field:nth-last-child(-n + 2) {
border-width: 2px;
}
.row {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
}
.name-field {
padding: 8px;
}
.theme-select {
width: 100%;
}
.theme-settings {
display: flex;
flex-direction: column;
width: 224px;
}
.theme-colors {
display: flex;
flex-wrap: wrap;
border-top: 2px solid ${COLORS.SECONDARY};
text-transform: capitalize;
}
.theme-colors .field {
width: 50%;
}
.theme-colors .field:nth-child(odd) {
border-right: 1px solid ${COLORS.SECONDARY};
}
.color-square {
border-radius: 3px;
padding: 12px;
}
`}
</style>
</Popout>
)
const ThemeItem = ({ children, item, isSelected, onClick }) => (
<div className="theme-item">
{children}
{item.custom && !isSelected && (
<div className="icon" onClick={onClick(item.id)}>
<RemoveIcon color={COLORS.SECONDARY} />
</div>
)}
<style jsx>
{`
.theme-item {
display: flex;
flex: 1;
justify-content: ${item.id === 'create' ? 'center' : 'space-between'};
align-items: center;
}
.icon {
display: flex;
margin-right: 6px;
}
`}
</style>
</div>
)
const themeIcon = <ThemeIcon />
class Themes extends React.PureComponent {
selectedTheme = DEFAULT_THEME
state = {
themes: THEMES,
preset: this.props.theme,
highlights: {},
name: 'Custom Theme',
selected: null
}
componentDidMount() {
const storedThemes = getThemes(localStorage) || []
this.setState(({ themes }) => {
const newThemes = [...storedThemes, ...themes]
const name = `Custom Theme ${newThemes.filter(({ name }) => name.startsWith('Custom Theme'))
.length + 1}`
this.selectedTheme = newThemes.find(({ id }) => id === this.props.theme) || DEFAULT_THEME
return {
themes: newThemes,
highlights: this.selectedTheme.highlights,
name
}
})
}
applyPreset = preset =>
this.setState(({ themes }) => ({
preset,
highlights: themes.find(({ id }) => id === preset).highlights
}))
handleDropdown = ({ id }) => {
if (id === 'create') {
this.props.toggleVisibility()
} else {
this.props.updateTheme(id)
}
}
updateName = ({ target: { value: name } }) => this.setState({ name })
selectHighlight = highlight => () =>
this.setState(({ selected }) => ({
selected: selected === highlight ? null : highlight
}))
updateHighlight = ({ rgb }) =>
this.setState({
highlights: {
...this.state.highlights,
[this.state.selected]: stringifyRGBA(rgb)
}
})
removeTheme = id => event => {
event.stopPropagation()
const { themes } = this.state
const newThemes = themes.filter(t => t.id !== id)
saveThemes(localStorage, newThemes.filter(({ custom }) => custom))
if (this.props.theme === id) {
this.props.updateTheme(DEFAULT_THEME.id)
} else {
this.setState({ themes: newThemes })
}
}
createTheme = () => {
const { themes, name, highlights } = this.state
const id = `theme:${generateId()}`
const newTheme = {
id,
name,
highlights,
custom: true
}
const customThemes = [newTheme, ...themes.filter(({ custom }) => custom)]
saveThemes(localStorage, customThemes)
this.props.updateTheme(id)
}
itemWrapper = props => <ThemeItem {...props} onClick={this.removeTheme} />
render() {
const { theme, isVisible, toggleVisibility } = this.props
const { name, themes, highlights, selected, preset } = this.state
const dropdownValue = isVisible ? { name } : { id: theme, name: this.selectedTheme.name }
const dropdownList = [
{
id: 'create',
name: 'Create +'
},
...themes
]
return (
<div className="themes">
<Dropdown
icon={themeIcon}
disableInput={isVisible}
inputValue={dropdownValue}
selected={dropdownValue}
list={dropdownList}
itemWrapper={this.itemWrapper}
onChange={this.handleDropdown}
onOpen={isVisible && toggleVisibility}
/>
{isVisible && (
<ThemeCreate
key={theme}
preset={preset}
name={name}
theme={theme}
themes={themes}
highlights={highlights}
selected={selected}
applyPreset={this.applyPreset}
createTheme={this.createTheme}
updateName={this.updateName}
selectHighlight={this.selectHighlight}
updateHighlight={this.updateHighlight}
/>
)}
<style jsx>
{`
.themes {
position: relative;
}
:global(.react-codemirror2 .CodeMirror) {
color: ${highlights.text} !important;
background-color: ${highlights.background} !important;
}
:global(.cm-string),
:global(.cm-string-2) {
color: ${highlights.string} !important;
}
:global(.cm-comment) {
color: ${highlights.comment} !important;
}
:global(.cm-variable),
:global(.cm-variable-2),
:global(.cm-variable-3) {
color: ${highlights.variable} !important;
}
:global(.cm-number) {
color: ${highlights.number} !important;
}
:global(.cm-keyword) {
color: ${highlights.keyword} !important;
}
:global(.cm-property) {
color: ${highlights.property} !important;
}
:global(.cm-def) {
color: ${highlights.definition} !important;
}
:global(.cm-meta) {
color: ${highlights.meta} !important;
}
`}
</style>
</div>
)
}
}
export default managePopout(Themes)

@ -15,116 +15,519 @@ export const FONTS = [
{ id: 'Ubuntu Mono', name: 'Ubuntu Mono' } { id: 'Ubuntu Mono', name: 'Ubuntu Mono' }
] ]
export const HIGHLIGHT_KEYS = [
'background',
'text',
'variable',
'attribute',
'definition',
'keyword',
'operator',
'property',
'number',
'string',
'comment',
'meta'
]
export const THEMES = [ export const THEMES = [
{ {
id: '3024-night', id: '3024-night',
name: '3024 Night' name: '3024 Night',
highlights: {
background: '#090300',
text: '#d6d5d4',
variable: '#01a0e4',
attribute: '#00c',
definition: '#e8bbd0',
keyword: '#db2d20',
operator: '#fff',
property: '#01a252',
number: '#a16a94',
string: '#fded02',
comment: '#cdab53',
meta: '#555'
}
}, },
{ {
id: 'blackboard', id: 'blackboard',
name: 'Blackboard' name: 'Blackboard',
highlights: {
background: '#0C1021',
text: '#F8F8F8',
variable: '#FF6400',
attribute: '#8DA6CE',
definition: '#8DA6CE',
keyword: '#FBDE2D',
operator: '#fff',
property: '#fff',
number: '#D8FA3C',
string: '#61CE3C',
comment: '#AEAEAE',
meta: '#D8FA3C',
tag: '#8DA6CE'
}
}, },
{ {
id: 'base16-dark', id: 'base16-dark',
name: 'Base 16 (Dark)' name: 'Base 16 (Dark)',
highlights: {
background: '#151515',
text: '#e0e0e0',
variable: '#6a9fb5',
attribute: '#00c',
definition: '#d28445',
keyword: '#ac4142',
operator: '#fff',
property: '#90a959',
number: '#aa759f',
string: '#f4bf75',
comment: '#8f5536',
meta: '#555',
tag: '#ac4142'
}
}, },
{ {
id: 'base16-light', id: 'base16-light',
name: 'Base 16 (Light)', name: 'Base 16 (Light)',
light: true light: true,
highlights: {
background: '#f5f5f5',
text: '#202020',
variable: '#90a959',
attribute: '#90a959',
definition: '#d28445',
keyword: '#ac4142',
operator: '#fff',
property: '#90a959',
number: '#aa759f',
string: '#f4bf75',
comment: '#8f5536',
meta: '#555',
tag: '#ac4142'
}
}, },
{ {
id: 'cobalt', id: 'cobalt',
name: 'Cobalt' name: 'Cobalt',
highlights: {
background: '#002240',
text: '#fff',
variable: '#9effff',
attribute: '#ff80e1',
definition: '#fff',
keyword: '#ffee80',
operator: '#fff',
property: '#fff',
number: '#ff80e1',
string: '#3ad900',
comment: '#08f',
meta: '#ff9d00',
tag: '#9effff'
}
}, },
{ {
id: 'dracula', id: 'dracula',
name: 'Dracula' name: 'Dracula',
highlights: {
background: '#282a36',
text: '#f8f8f2',
variable: '#fff',
attribute: '#50fa7b',
definition: '#50fa7b',
keyword: '#ff79c6',
operator: '#ff79c6',
property: '#66d9ef',
number: '#bd93f9',
string: '#f1fa8c',
comment: '#6272a4',
meta: '#f8f8f2',
tag: '#ff79c6'
}
}, },
{ {
id: 'duotone-dark', id: 'duotone-dark',
name: 'Duotone' name: 'Duotone',
highlights: {
background: '#2a2734',
text: '#6c6783',
variable: '#ffcc99',
attribute: '#ffcc99',
definition: '#eeebff',
keyword: '#ffcc99',
operator: '#ffad5c',
property: '#9a86fd',
number: '#ffcc99',
string: '#ffb870',
comment: '#6c6783',
meta: '#555',
tag: '#eeebff'
}
}, },
{ {
id: 'hopscotch', id: 'hopscotch',
name: 'Hopscotch' name: 'Hopscotch',
highlights: {
background: '#322931',
text: '#d5d3d5',
variable: '#0066ff',
attribute: '#8fc13e',
definition: '#fd8b19',
keyword: '#dd464c',
operator: '#fff',
property: '#8fc13e',
number: '#c85e7c',
string: '#fdcc59',
comment: '#b33508',
meta: '#555',
tag: '#dd464c'
}
}, },
{ {
id: 'lucario', id: 'lucario',
name: 'Lucario' name: 'Lucario',
highlights: {
background: '#2b3e50',
text: '#f8f8f2',
variable: '#f8f8f2',
attribute: '#66D9EF',
definition: '#72C05D',
keyword: '#ff6541',
operator: '#66D9EF',
property: '#f8f8f2',
number: '#ca94ff',
string: '#E6DB74',
comment: '#5c98cd',
meta: '#f8f8f2',
tag: '#ff6541'
}
}, },
{ {
id: 'material', id: 'material',
name: 'Material' name: 'Material',
highlights: {
background: '#263238',
text: 'rgba(233, 237, 237, 1)',
variable: '#80CBC4',
attribute: '#FFCB6B',
definition: 'rgba(233, 237, 237, 1)',
keyword: 'rgba(199, 146, 234, 1)',
operator: 'rgba(233, 237, 237, 1)',
property: '#80CBAE',
number: '#F77669',
string: '#C3E88D',
comment: '#546E7A',
meta: '#80CBC4',
tag: 'rgba(255, 83, 112, 1)'
}
}, },
{ {
id: 'monokai', id: 'monokai',
name: 'Monokai' name: 'Monokai',
highlights: {
background: '#272822',
text: '#f8f8f2',
variable: '#9effff',
attribute: '#a6e22e',
definition: '#fd971f',
keyword: '#f92672',
operator: '#fff',
property: '#a6e22e',
number: '#ae81ff',
string: '#e6db74',
comment: '#75715e',
meta: '#555',
tag: '#bc6283'
}
}, },
{ {
id: 'night-owl', id: 'night-owl',
name: 'Night Owl' name: 'Night Owl',
highlights: {
background: '#011627',
text: '#abb2bf',
variable: '#82AAFF',
attribute: '#F78C6C',
definition: '#82AAFF',
keyword: '#c792ea',
operator: '#c792ea',
property: '#fff',
number: '#F78C6C',
string: '#ecc48d',
comment: '#5c6370',
meta: '#7fdbca'
}
}, },
{ {
id: 'nord', id: 'nord',
name: 'Nord' name: 'Nord',
highlights: {
background: '#2e3440',
text: '#d8dee9',
variable: '#88C0D0',
attribute: '#8FBCBB',
definition: '#D8DEE9',
keyword: '#81A1C1',
operator: '#81A1C1',
property: '#D8DEE9',
number: '#B48EAD',
string: '#A3BE8C',
comment: '#4C566A',
meta: '#81A1C1',
tag: '#81A1C1'
}
}, },
{ {
id: 'oceanic-next', id: 'oceanic-next',
name: 'Oceanic Next' name: 'Oceanic Next',
highlights: {
background: '#304148',
text: '#f8f8f2',
variable: '#f8f8f2',
attribute: '#C594C5',
definition: '#6699CC',
keyword: '#C594C5',
operator: '#fff',
property: '#99C794',
number: '#F99157',
string: '#99C794',
comment: '#65737E',
meta: '#555',
tag: '#C594C5'
}
}, },
{ {
id: 'one-light', id: 'one-light',
name: 'One Light', name: 'One Light',
light: true light: true,
highlights: {
background: '#fafafa',
text: '#383a42',
variable: '#e06c75',
attribute: '#d19a66',
definition: '#4078f2',
keyword: '#a626a4',
operator: '#0184bc',
property: '#4078f2',
number: '#986801',
string: '#50a14f',
comment: '#a0a1a7',
meta: '#383a42',
tag: '#e45649'
}
}, },
{ {
id: 'one-dark', id: 'one-dark',
name: 'One Dark' name: 'One Dark',
highlights: {
background: '#282c34',
text: '#abb2bf',
variable: '#e06c75',
attribute: '#d19a66',
definition: '#e5c07b',
keyword: '#c678dd',
operator: '#56b6c2',
property: '#56b6c2',
number: '#d19a66',
string: '#98c379',
comment: '#5c6370',
meta: '#abb2bf',
tag: '#e06c75'
}
}, },
{ {
id: 'panda-syntax', id: 'panda-syntax',
name: 'Panda' name: 'Panda',
highlights: {
background: '#292A2B',
text: '#E6E6E6',
variable: '#ff9ac1',
attribute: '#ffb86c',
definition: '#e6e6e6',
keyword: '#FF75B5',
operator: '#f3f3f3',
property: '#f3f3f3',
number: '#FFB86C',
string: '#19F9D8',
comment: '#676B79',
meta: '#b084eb',
tag: '#ff2c6d'
}
}, },
{ {
id: 'paraiso-dark', id: 'paraiso-dark',
name: 'Paraiso' name: 'Paraiso',
highlights: {
background: '#2f1e2e',
text: '#b9b6b0',
variable: '#06b6ef',
attribute: '#48b685',
definition: '#f99b15',
keyword: '#ef6155;',
operator: '#fff',
property: '#48b685',
number: '#815ba4',
string: '#fec418',
comment: '#e96ba8',
meta: '#555',
tag: '#ef6155'
}
}, },
{ {
id: 'seti', id: 'seti',
name: 'Seti' name: 'Seti',
highlights: {
background: '#151718',
text: '#CFD2D1',
variable: '#a074c4',
attribute: '#9fca56',
definition: '#55b5db',
keyword: '#e6cd69',
operator: '#9fca56',
property: '#a074c4',
number: '#cd3f45',
string: '#55b5db',
comment: '#41535b',
meta: '#55b5db',
tag: '#55b5db'
}
}, },
{ {
id: 'solarized dark', id: 'solarized dark',
name: 'Solarized (Dark)', name: 'Solarized (Dark)',
link: 'solarized' link: 'solarized',
highlights: {
background: '#002b36',
text: '#839496',
variable: '#b58900',
attribute: '#2aa198',
definition: '#2aa198',
keyword: '#cb4b16',
operator: '#6c71c4',
property: '#2aa198',
number: '#d33682',
string: '#859900',
comment: '#586e75',
meta: '#859900',
tag: '#93a1a1'
}
}, },
{ {
id: 'solarized light', id: 'solarized light',
name: 'Solarized (Light)', name: 'Solarized (Light)',
link: 'solarized', link: 'solarized',
light: true light: true,
highlights: {
background: '#fdf6e3',
text: '#657b83',
variable: '#839496',
attribute: '#2aa198',
definition: '#2aa198',
keyword: '#cb4b16',
operator: '#6c71c4',
property: '#2aa198',
number: '#d33682',
string: '#859900',
comment: '#586e75',
meta: '#859900',
tag: '#93a1a1'
}
}, },
{ {
id: 'tomorrow-night-bright', id: 'tomorrow-night-bright',
name: 'Tomorrow Night' name: 'Tomorrow Night',
highlights: {
background: '#000000',
text: '#eaeaea',
variable: '#7aa6da',
attribute: '#99cc99',
definition: '#e78c45',
keyword: '#d54e53',
operator: '#fff',
property: '#99cc99',
number: '#a16a94',
string: '#e7c547',
comment: '#d27b53',
meta: '#555',
tag: '#d54e53'
}
}, },
{ {
id: 'twilight', id: 'twilight',
name: 'Twilight' name: 'Twilight',
highlights: {
background: '#141414',
text: '#f7f7f7',
variable: '#607392',
attribute: '#d6bb6d',
definition: '#607392',
keyword: '#f9ee98',
operator: '#cda869',
property: '#fff',
number: '#ca7841',
string: '#8f9d6a',
comment: '#777',
meta: '#f7f7f7',
tag: '#997643'
}
}, },
{ {
id: 'verminal', id: 'verminal',
name: 'Verminal' name: 'Verminal',
highlights: {
background: 'rgba(0, 0, 0, 0.85)',
text: '#fff',
variable: '#ff9ba3',
attribute: '#d19a66',
definition: '#34B7FF',
keyword: '#9AE1FF',
operator: '#FA78C3',
property: '#0af',
number: '#d19a66',
string: '#98c379',
comment: '#5c6370',
meta: '#abb2bf',
tag: '#e06c75'
}
}, },
{ {
id: 'yeti', id: 'yeti',
name: 'Yeti', name: 'Yeti',
light: true light: true,
highlights: {
background: '#ECEAE8',
text: '#d1c9c0',
variable: '#a074c4',
attribute: '#9fb96e',
definition: '#55b5db',
keyword: '#9fb96e',
operator: '#9fb96e',
property: '#a074c4',
number: '#a074c4',
string: '#96c0d8',
comment: '#d4c8be',
meta: '#96c0d8',
tag: '#96c0d8'
}
}, },
{ {
id: 'zenburn', id: 'zenburn',
name: 'Zenburn' name: 'Zenburn',
highlights: {
background: '#3f3f3f',
text: '#dcdccc',
variable: '#dcdccc',
attribute: '#dfaf8f',
definition: '#dcdccc',
keyword: '#f0dfaf',
operator: '#f0efd0',
property: '#dfaf8f',
number: '#dcdccc',
string: '#cc9393',
comment: '#7f9f7f',
meta: '#f0dfaf',
tag: '#93e0e3'
}
} }
] ]

@ -4,9 +4,17 @@ import { unescape } from 'escape-goat'
const SETTINGS_KEY = 'CARBON_STATE' const SETTINGS_KEY = 'CARBON_STATE'
const PRESETS_KEY = 'CARBON_PRESETS' const PRESETS_KEY = 'CARBON_PRESETS'
const THEMES_KEY = 'CARBON_THEMES'
const assignSettings = morph.assign(SETTINGS_KEY) const createAssigner = key => {
const assignPresets = morph.assign(PRESETS_KEY) const assign = morph.assign(key)
return (window, v) => assign(window, JSON.stringify(v))
}
export const saveSettings = createAssigner(SETTINGS_KEY)
export const savePresets = createAssigner(PRESETS_KEY)
export const saveThemes = createAssigner(THEMES_KEY)
const parse = v => { const parse = v => {
try { try {
@ -46,9 +54,10 @@ export const getPresets = morph.compose(
morph.get(PRESETS_KEY) morph.get(PRESETS_KEY)
) )
export const saveSettings = (window, v) => assignSettings(window, JSON.stringify(v)) export const getThemes = morph.compose(
parse,
export const savePresets = (window, v) => assignPresets(window, JSON.stringify(v)) morph.get(THEMES_KEY)
)
export const clearSettings = () => localStorage.removeItem(SETTINGS_KEY) export const clearSettings = () => localStorage.removeItem(SETTINGS_KEY)
@ -76,3 +85,8 @@ export const omit = (object, keys) => omitBy(object, (_, k) => keys.indexOf(k) >
export const stringifyRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})` export const stringifyRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`
export const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1) export const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
export const generateId = () =>
Math.random()
.toString(36)
.slice(2)

Loading…
Cancel
Save