feat: improve themeability

This commit is contained in:
Miralem Drek
2019-05-03 17:56:40 +02:00
parent f7fa010802
commit cd14ad207f
20 changed files with 577 additions and 335 deletions

View File

@@ -41,8 +41,12 @@ const argv = yargs
const CONFIGS = {
unit: {
glob: [`${argv.scope}/__tests__/unit/**/*.spec.{js,jsx}`, `${argv.scope}/src/**/__tests__/**/*.spec.{js,jsx}`],
src: [`${argv.scope}/src/**/*.{js,jsx}`],
glob: [
`${argv.scope}/__tests__/unit/**/*.spec.{js,jsx}`,
`${argv.scope}/src/**/__tests__/**/*.spec.{js,jsx}`,
`${argv.scope}/**/__tests__/**/*.spec.js`,
],
src: [`${argv.scope}/src/**/*.{js,jsx}`, `${argv.scope}/**/*.{js,jsx}`],
coverage: true,
nyc: {
include: [`${argv.scope}/src/**/*.{js,jsx}`],
@@ -54,7 +58,7 @@ const CONFIGS = {
mocks: [
['**/*.scss', '{}'],
['**/*.css', '{}'],
['**/styled.js', () => () => ['classes']],
['**/theme.js', () => () => ({ style: () => 'classname' })],
// mock nebula modules to avoid parsing errors without build.
// these modules should be mocked properly in the unit test

View File

@@ -115,10 +115,10 @@ describe('<SelectionToolbar />', () => {
},
};
const STItem = ({ isCustom }) => `-${isCustom}-`;
const styled = () => ['a'];
const styled = () => ({ style: () => 'a' });
const [{ default: STB }] = aw.mock([
['**/SelectionToolbarItem.jsx', () => STItem],
['**/styled.js', () => styled],
['**/theme.js', () => styled],
], ['../../src/components/SelectionToolbar']);
const c = renderer.create(<STB sn={props.sn} />);
expect(c.toJSON()).to.eql({

View File

@@ -48,8 +48,8 @@ function MultiState({
justifyContent: 'space-between',
height: '48px',
boxSizing: 'border-box',
background: '$grey100',
borderRight: '1px solid $alpha15',
background: '$palette.background.default',
borderRight: '1px solid $palette.divider',
}}
>
<Grid vertical spacing="small" style={{ overflow: 'hidden' }}>
@@ -102,9 +102,9 @@ export class AppSelections extends React.Component {
render() {
return (
<Toolbar>
<Toolbar style={{ backgroundColor: '#E5E5E5' }}>
<Grid spacing="none">
<Grid styled={{ background: '$grey100', borderRight: '1px solid $alpha15' }}>
<Grid styled={{ background: '$palette.background.default', borderRight: '1px solid $palette.divider' }}>
<ButtonInline
style={{ marginRight: '8px' }}
disabled={!this.state.back}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import Grid from '@nebula.js/ui/components/Grid';
import styled from '@nebula.js/ui/components/styled';
import themes from '@nebula.js/ui/theme';
import Requirements from './Requirements';
import CError from './Error';
@@ -45,12 +45,13 @@ const Content = ({ children }) => (
class Cell extends React.Component {
constructor(...args) {
super(...args);
this.styledClasses = ['nebulajs', ...styled({
fontSize: '$fontSize',
lineHeight: '$lineHeight',
fontWeight: '400',
fontFamily: '$fontFamily',
color: '$grey25',
const theme = themes('light');
this.styledClasses = ['nebulajs', ...theme.style({
fontSize: '$typography.medium.fontSize',
lineHeight: '$typography.medium.lineHeight',
fontWeight: '$typography.weight.regular',
fontFamily: '$typography.fontFamily',
color: '$palette.text.primary',
})].join(' ');
this.state = {};
}

View File

@@ -1,11 +1,12 @@
import React, {
useRef,
useState,
useMemo,
} from 'react';
import Remove from '@nebula.js/ui/icons/Remove';
import Lock from '@nebula.js/ui/icons/Lock';
import styled from '@nebula.js/ui/components/styled';
import themes from '@nebula.js/ui/theme';
import ButtonInline from '@nebula.js/ui/components/ButtonInline';
import Grid from '@nebula.js/ui/components/Grid';
@@ -13,29 +14,30 @@ import Text from '@nebula.js/ui/components/Text';
import ListBoxPopover from './listbox/ListBoxPopover';
const [classes] = styled({
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
cursor: 'pointer',
'&:hover': {
background: '$alpha03',
},
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 2px $bluePale',
},
});
export default function SelectedField({
field,
api,
theme = themes('light'),
}) {
const alignTo = useRef();
const [isActive, setIsActive] = useState(false);
const classes = useMemo(() => theme.style({
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
cursor: 'pointer',
'&:hover': {
background: '$palette.background.hover',
},
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 2px $bluePale',
},
}), [theme]);
const toggleActive = () => setIsActive(!isActive);
const keyToggleActive = e => e.key === ' ' && setIsActive(!isActive);
@@ -65,8 +67,8 @@ export default function SelectedField({
position: 'relative',
width: '148px',
justifyContent: 'space-between',
background: '$grey100',
borderRight: '1px solid $alpha15',
background: '$palette.background.default',
borderRight: '1px solid $palette.divider',
}}
>
<Grid

View File

@@ -1,5 +1,5 @@
import React from 'react';
import styled from '@nebula.js/ui/components/styled';
import themes from '@nebula.js/ui/theme';
import Item from './SelectionToolbarItem';
@@ -14,17 +14,19 @@ class Component extends React.Component {
clearable: api.canClear(),
};
this.styledClasses = styled({
const theme = themes('light');
this.styledClasses = theme.style({
position: 'absolute',
left: '0',
right: '0',
top: '-48px',
padding: '8px',
boxSizing: 'border-box',
background: '$grey100',
background: '$palette.background.default',
display: 'flex',
justifyContent: 'flex-end',
}).join(' ');
});
const items = [];

View File

@@ -5,17 +5,21 @@ import React, {
useState,
useCallback,
useRef,
useMemo,
} from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import themes from '@nebula.js/ui/theme';
import useLayout from '../../hooks/useLayout';
import Row from './ListBoxRow';
export default function ListBox({
model,
theme = themes('light'),
}) {
const [layout] = useLayout(model);
const [pages, setPages] = useState(null);
@@ -29,6 +33,10 @@ export default function ListBox({
pages: [],
});
const listClass = theme.style({
backgroundColor: '$palette.background.lightest',
});
const onClick = useCallback((e) => {
const elemNumber = +e.target.getAttribute('data-n');
if (!Number.isNaN(elemNumber)) {
@@ -105,8 +113,7 @@ export default function ListBox({
return (
<FixedSizeList
useIsScrolling
className="List"
style={{ background: 'white' }}
className={listClass}
height={8 * ITEM_HEIGHT}
itemCount={count}
itemData={{ onClick, pages }}

View File

@@ -1,41 +1,46 @@
import React from 'react';
import React, { useMemo } from 'react';
import styled from '@nebula.js/ui/components/styled';
import themes from '@nebula.js/ui/theme';
import Text from '@nebula.js/ui/components/Text';
import Grid from '@nebula.js/ui/components/Grid';
import Lock from '@nebula.js/ui/icons/Lock';
import Tick from '@nebula.js/ui/icons/Tick';
const defaultClasses = styled({
boxSizing: 'border-box',
borderBottom: '1px solid rgba(0, 0, 0, 0.15)',
background: '$grey100',
color: '$grey25',
justifyContent: 'space-between',
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 2px $bluePale',
},
});
const S = styled({
background: '$green',
color: '$grey100',
});
const A = styled({
background: '$grey85',
});
const X = styled({
background: '$grey70',
});
export default function Row({
index,
style,
data,
theme = themes('light'),
}) {
const {
D,
S,
A,
X,
} = useMemo(() => ({
D: theme.style({
boxSizing: 'border-box',
borderBottom: '1px solid $palette.divider',
background: '$palette.background.default',
color: '$palette.text.primary',
justifyContent: 'space-between',
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 2px $bluePale',
},
}),
S: theme.style({
background: '$palette.green',
color: '$palette.grey.100',
}),
A: theme.style({
background: '$palette.grey.85',
}),
X: theme.style({
background: '$palette.grey.70',
}),
}), [theme]);
let label = '';
const { onClick, pages } = data;
let cell;
@@ -48,7 +53,7 @@ export default function Row({
}
}
}
const classes = [...defaultClasses];
const classes = [D];
let locked = false;
let selected = false;
if (cell) {
@@ -56,11 +61,11 @@ export default function Row({
locked = cell.qState === 'L' || cell.qState === 'XL';
selected = cell.qState === 'S' || cell.qState === 'XS';
if (cell.qState === 'S' || cell.qState === 'L') {
classes.push(...S);
classes.push(S);
} else if (cell.qState === 'A') {
classes.push(...A);
classes.push(A);
} else if (cell.qState === 'X' || cell.qState === 'XS' || cell.qState === 'XL') {
classes.push(...X);
classes.push(X);
}
}
return (

View File

@@ -1,41 +1,42 @@
import React from 'react';
import React, { useMemo } from 'react';
import styled from './styled';
import themes from '../theme';
export default function ButtonInline({
children,
disabled,
theme = themes('light'),
...props
}) {
const s = {
};
const classes = styled({
padding: '8px',
const className = useMemo(() => theme.style({
padding: '$spacing.4',
border: '0px solid transparent',
background: 'transparent',
borderRadius: '2px',
borderRadius: '$shape.borderRadius',
cursor: 'pointer',
color: '$grey25',
font: 'normal 14px/0 "Source Sans Pro", Arial, sans-serif',
color: '$palette.text.primary',
font: 'normal 14px/0 $typography.fontFamily',
'&:hover': {
background: 'rgba(0, 0, 0, 0.03)',
background: '$palette.background.hover',
},
'&:active': {
background: 'rgba(0, 0, 0, 0.1)',
background: '$palette.background.active',
},
'&:disabled': {
opacity: '0.3',
cursor: 'default',
},
});
}), [theme]);
return (
<button
className={classes.join(' ')}
className={className}
type="button"
disabled={disabled}
style={s}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import styledFn from './styled';
import themes from '../theme';
export default function Grid({
children,
@@ -9,6 +9,7 @@ export default function Grid({
style,
styled,
className = '',
theme = themes('light'),
...props
}) {
let space = '8px';
@@ -23,10 +24,10 @@ export default function Grid({
flexDirection: vertical ? 'column' : '',
alignItems: vertical ? '' : 'center',
};
const classes = styledFn([inlineStyle, styled, { boxSizing: 'border-box' }]);
const classes = theme.style([inlineStyle, styled, { boxSizing: 'border-box' }]);
return (
<div
className={[className, ...classes].join(' ')}
className={[className, classes].join(' ')}
style={style}
{...props}
>
@@ -38,13 +39,14 @@ export default function Grid({
Grid.Item = function GridItem({
children,
style,
theme = themes('light'),
}) {
const classes = styledFn([{
const className = theme.style([{
flex: '1 0 auto',
}, style]);
return (
<div className={classes.join(' ')}>
<div className={className}>
{children}
</div>
);

View File

@@ -1,18 +1,18 @@
import React from 'react';
import React, { useMemo } from 'react';
import styled from './styled';
import themes from '../theme';
const sizes = {
small: { fontSize: '12px', lineHeight: '16px' },
medium: { fontSize: '14px', lineHeight: '16px' },
large: { fontSize: '16px', lineHeight: '24px' },
xlarge: { fontSize: '24px', lineHeight: '32px' },
small: { fontSize: '$typography.small.fontSize', lineHeight: '$typography.small.lineHeight' },
medium: { fontSize: '$typography.medium.fontSize', lineHeight: '$typography.small.lineHeight' },
large: { fontSize: '$typography.large.fontSize', lineHeight: '$typography.large.lineHeight' },
xlarge: { fontSize: '$typography.xlarge.fontSize', lineHeight: '$typography.xlarge.lineHeight' },
};
const weights = {
light: 300,
regular: 400,
semibold: 600,
light: '$typography.weight.light',
regular: '$typography.weight.regular',
semibold: '$typography.weight.semibold',
};
export default function Text({
@@ -23,30 +23,33 @@ export default function Text({
nowrap,
faded,
block,
theme = themes('light'),
}) {
const { fontSize, lineHeight } = sizes[size];
const fontWeight = weights[weight];
const nowrapStyle = nowrap ? {
const nowrapClass = useMemo(() => (nowrap ? theme.style({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
} : {};
}) : ''), [nowrap]);
const family = {
fontFamily: '$fontFamily',
};
const familyClass = useMemo(() => theme.style({
fontFamily: '$typography.fontFamily',
}), [theme]);
const inlineStyle = {
const inlineClass = theme.style({
fontSize,
fontWeight,
lineHeight,
display: block ? 'block' : 'inline-block',
color: faded ? '$alpha55' : '',
};
const classes = styled([family, inlineStyle, nowrapStyle, style]);
color: faded ? '$palette.text.secondary' : '$palette.text.primary',
});
const classes = [nowrapClass, familyClass, inlineClass];
return (
<span className={classes.join(' ')}>
<span className={classes.join(' ')} style={style}>
{children}
</span>
);

View File

@@ -1,22 +1,23 @@
import React from 'react';
import React, { useMemo } from 'react';
import styled from './styled';
import themes from '../theme';
export default function Toolbar({
children,
style,
theme = themes('light'),
}) {
const classes = styled([{
background: '$grey90',
color: '$grey25',
const className = useMemo(() => theme.style({
background: '$palette.grey.98',
color: '$palette.text.primary',
height: '48px',
boxShadow: '0 1px 0 $alpha15',
fontFamily: '"Source Sans Pro", Arial, sans-serif',
}, style]);
fontFamily: '$typography.fontFamily',
}), [theme]);
return (
<div
className={classes.join(' ')}
className={className}
style={style}
>
{children}
</div>

View File

@@ -1,131 +1,141 @@
import React, { useState, useRef, useEffect } from 'react';
import React, {
useState,
useRef,
useEffect,
useMemo,
} from 'react';
import { oppositeDock, positionToElement } from 'react-leonardo-ui/src/positioner';
import styled from '../styled';
const classes = styled({
position: 'relative',
display: 'flex',
flexDirection: 'column',
borderRadius: '2px',
margin: 'auto',
minWidth: '250px',
border: '1px solid transparent',
zIndex: '1021',
backgroundColor: '$grey100',
borderColor: '$alpha15',
boxShadow: '0 1px 2px $alpha03',
});
const [arrowC] = styled({
position: 'absolute',
'&::before': {
content: '""',
position: 'absolute',
width: 0,
height: 0,
},
'&::after': {
content: '""',
position: 'absolute',
width: 0,
height: 0,
},
});
const [arrowTop] = styled({
position: 'absolute',
top: 0,
'&::before': {
left: '-8px',
bottom: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid $alpha15',
},
'&::after': {
left: '-8px',
bottom: '-1px',
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid $grey100',
},
});
const [arrowBottom] = styled({
position: 'absolute',
bottom: 0,
'&::before': {
left: '-8px',
top: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderTop: '8px solid $alpha15',
},
'&::after': {
left: '-8px',
top: '-1px',
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderTop: '8px solid $grey100',
},
});
const [arrowLeft] = styled({
position: 'absolute',
left: 0,
top: '50%',
'&::before': {
top: '-8px',
right: 0,
borderRight: '8px solid $alpha15',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
'&::after': {
top: '-8px',
right: '-1px',
borderRight: '8px solid $grey100',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
});
const [arrowRight] = styled({
position: 'absolute',
left: 0,
top: '50%',
'&::before': {
top: '-8px',
left: 0,
borderLeft: '8px solid $alpha15',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
'&::after': {
top: '-8px',
left: '-1px',
borderLeft: '8px solid $grey100',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
});
const arrows = {
left: arrowLeft,
right: arrowRight,
top: arrowTop,
bottom: arrowBottom,
};
import themes from '../../theme';
export default function PopoverContent({
children,
alignTo,
theme = themes('light'),
}) {
const [p, setP] = useState(null);
const ref = useRef(null);
const {
container,
arrowContent,
arrowLeft,
arrowTop,
arrowBottom,
arrowRight,
} = useMemo(() => ({
container: theme.style({
position: 'relative',
display: 'flex',
flexDirection: 'column',
borderRadius: '$shape.borderRadius',
margin: 'auto',
minWidth: '250px',
border: '1px solid transparent',
zIndex: '1021',
backgroundColor: '$palette.background.lightest',
borderColor: '$palette.black.05',
boxShadow: '$shadows.3',
}),
arrowContent: theme.style({
position: 'absolute',
'&::before': {
content: '""',
position: 'absolute',
width: 0,
height: 0,
},
'&::after': {
content: '""',
position: 'absolute',
width: 0,
height: 0,
},
}),
arrowTop: theme.style({
position: 'absolute',
top: 0,
'&::before': {
left: '-8px',
bottom: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid $palette.black.05',
},
'&::after': {
left: '-8px',
bottom: '-1px',
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid $palette.background.lightest',
},
}),
arrowBottom: theme.style({
position: 'absolute',
bottom: 0,
'&::before': {
left: '-8px',
top: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderTop: '8px solid $palette.black.05',
},
'&::after': {
left: '-8px',
top: '-1px',
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderTop: '8px solid $palette.background.lightest',
},
}),
arrowLeft: theme.style({
position: 'absolute',
left: 0,
top: '50%',
'&::before': {
top: '-8px',
right: 0,
borderRight: '8px solid $palette.black.05',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
'&::after': {
top: '-8px',
right: '-1px',
borderRight: '8px solid $palette.background.lightest',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
}),
arrowRight: theme.style({
position: 'absolute',
left: 0,
top: '50%',
'&::before': {
top: '-8px',
left: 0,
borderLeft: '8px solid $palette.black.05',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
'&::after': {
top: '-8px',
left: '-1px',
borderLeft: '8px solid $palette.background.lightest',
borderTop: '8px solid transparent',
borderBottom: '8px solid transparent',
},
}),
}), [theme]);
const arrows = {
left: arrowLeft,
right: arrowRight,
top: arrowTop,
bottom: arrowBottom,
};
const style = {
visibility: p ? 'visible' : 'hidden',
position: 'absolute',
@@ -135,11 +145,11 @@ export default function PopoverContent({
};
useEffect(() => {
const pp = positionToElement(ref.current, alignTo, 'right', {
dock: 'right',
const pp = positionToElement(ref.current, alignTo, 'bottom', {
dock: 'bottom',
offset: 8,
minWindowOffset: 10,
minEdgeOffset: 5,
minWindowOffset: 8,
minEdgeOffset: 4,
});
setP(pp);
}, [alignTo]);
@@ -158,13 +168,13 @@ export default function PopoverContent({
}
const arrowElem = (
<div
className={[arrowC, arrows[arrow.dock]].join(' ')}
className={[arrowContent, arrows[arrow.dock]].join(' ')}
style={arrow.style}
/>
);
return (
<div
className={classes.join(' ')}
className={container}
ref={ref}
role="dialog"
style={style}

View File

@@ -1,95 +0,0 @@
const theme = {
$grey100: '#fff',
$grey98: 'fafafa',
$grey95: 'f2f2f2',
$grey90: '#e6e6e6',
$grey85: '#d9d9d9',
$grey70: '#b3b3b3',
$grey25: '#404040',
$alpha03: 'rgba(0, 0, 0, 0.03)',
$alpha15: 'rgba(0, 0, 0, 0.15)',
$alpha55: 'rgba(0, 0, 0, 0.55)',
$green: '#6CB33F',
$bluePale: '#469DCD',
$fontFamily: '"Source Sans Pro", Arial, sans-serif',
$fontSize: '14px',
$lineHeight: '16px',
};
const rxCap = /[A-Z]/g;
const rxAppend = /^&/;
const rxVar = /\$[A-z0-9]+/g;
const idGen = [[10, 31], [0, 31], [0, 31], [0, 31], [0, 31], [0, 31]];
function toChar([min, max]) {
return (min + (Math.random() * (max - min) | 0)).toString(32);
}
function uid() {
return idGen.map(toChar).join('');
}
function replaceVar(match) {
return theme[match];
}
function parse(id, style, sheet) {
const rule = [];
const post = {};
Object.keys(style).forEach((prop) => {
if (rxAppend.test(prop)) {
post[prop.slice(1)] = style[prop];
} else {
let value = style[prop];
if (rxVar.test(style[prop])) {
value = value.replace(rxVar, replaceVar);
}
rule.push(`${prop.replace(rxCap, '-$&').toLowerCase()}: ${value}`);
}
});
sheet.insertRule(`.${id} { ${rule.join(';')} }`, sheet.cssRules.length);
Object.keys(post).forEach((key) => {
parse(`${id}${key}`, post[key], sheet);
});
}
function addRule(style, sheet) {
const id = uid();
parse(id, style, sheet);
return [id];
}
function initiate() {
let injectedSheet;
if (typeof window !== 'undefined') {
const el = document.createElement('style');
el.setAttribute('data-id', 'nebula-ui');
el.setAttribute('data-version', '0.1.0');
injectedSheet = document.head.appendChild(el).sheet;
}
const cache = {};
return function addStyle(s) {
const styles = Array.isArray(s) ? s : [s];
return styles.filter(Boolean).map((style) => {
const key = JSON.stringify(style);
if (cache[key]) {
return cache[key].classes[0];
}
const classes = addRule(style, injectedSheet);
cache[key] = {
style,
classes,
};
return classes[0];
});
};
}
const styled = initiate();
export default styled;

View File

@@ -0,0 +1,68 @@
import resolver from '../resolver';
describe('resolver', () => {
describe('creator', () => {
it('should flatten theme', () => {
const r = resolver({
foo: {
bar: 'red',
},
});
expect(r.references()).to.eql({
'foo.bar': 'red',
});
});
it('should resolve theme variables', () => {
const r = resolver({
shadow: {
colorful: '2px $palette.bright.primary',
},
palette: {
bright: {
primary: '$palette.light',
},
light: 'ocean',
},
});
expect(r.references()).to.eql({
'shadow.colorful': '2px ocean',
'palette.bright.primary': 'ocean',
'palette.light': 'ocean',
});
});
it('should throw when reference is cyclical', () => {
const fn = () => resolver({
foo: {
bar: '4px $foo.bar',
},
});
expect(fn).to.throw('Cyclical reference for "$foo.bar"');
});
});
describe('resolver', () => {
it('should resolve variable references', () => {
const r = resolver({
typography: {
fontFamily: 'Arial',
},
});
expect(r.resolve({
font: '16px $typography.fontFamily',
})).to.eql({
font: '16px Arial',
});
});
it('should resolve value references', () => {
const r = resolver({
typography: {
fontFamily: 'Arial',
},
});
expect(r.get('16px $typography.fontFamily')).to.eql('16px Arial');
});
});
});

View File

@@ -0,0 +1,79 @@
const theme = {
palette: {
grey: {
100: '#ffffff',
98: '#fafafa',
95: '#f2f2f2',
90: '#e6e6e6',
85: '#D9D9D9',
70: '#B3B3B3',
25: '#404040',
},
black: {
'03': 'rgba(0, 0, 0, 0.03)',
'05': 'rgba(0, 0, 0, 0.05)',
10: 'rgba(0, 0, 0, 0.10)',
15: 'rgba(0, 0, 0, 0.15)',
55: 'rgba(0, 0, 0, 0.55)',
},
text: {
primary: '$palette.grey.25',
secondary: '$palette.black.55',
},
divider: '$palette.black.15',
background: {
lightest: '$palette.grey.100',
lighter: '$palette.grey.98',
darker: '$palette.grey.95',
darkest: '$palette.grey.90',
default: '$palette.background.lightest',
hover: '$palette.black.03',
focus: '$palette.black.03',
active: '$palette.black.05',
},
green: '#6CB33F',
},
typography: {
fontFamily: '"Source Sans Pro", Arial, sans-serif',
weight: {
light: '300',
regular: '400',
semibold: '600',
},
small: {
fontSize: '12px',
lineHeight: '16px',
},
medium: {
fontSize: '14px',
lineHeight: '16px',
},
large: {
fontSize: '16px',
lineHeight: '24px',
},
xlarge: {
fontSize: '24px',
lineHeight: '32px',
},
},
shadows: {
0: 'none',
1: '0 1px 2px $palette.black.15',
2: '0 2px 4px $palette.black.15',
3: '0 4px 10px $palette.black.15',
4: '0 6px 20px $palette.black.15',
},
shape: {
borderRadius: '2px',
},
spacing: {
0: '0',
1: '2px',
3: '4px',
4: '8px',
5: '16px',
},
};
export default theme;

View File

@@ -0,0 +1,16 @@
import theme from './theme';
import light from './definitions/light';
const cache = {};
export default function (name) {
if (name !== 'light') {
throw new Error('No theme');
}
if (!cache[name]) {
cache[name] = theme(light);
}
return cache[name];
}

View File

@@ -0,0 +1,69 @@
/* eslint no-param-reassign: 0 */
const VARIABLE_RX = /\$[A-z0-9.]+/g;
function throwCyclical(s) {
throw new Error(`Cyclical reference for "${s}"`);
}
function resolveStyle(style, references, replacer) {
const s = {};
Object.keys(style).forEach((key) => {
let value = style[key];
if (VARIABLE_RX.test(value)) {
value = value.replace(VARIABLE_RX, replacer);
}
s[key] = value;
});
return s;
}
function createRefs(obj, s = '', refs) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
refs[`${s}${key}`] = obj[key];
} else if (typeof obj[key] === 'object') {
createRefs(obj[key], `${s}${key}.`, refs);
}
});
}
function resolver(theme = {}) {
const refs = {};
createRefs(theme, '', refs);
const replacer = match => refs[match.substring(1)];
function resolveValueReferences(value, path = []) {
const variables = typeof value === 'string' ? value.match(VARIABLE_RX) : null;
if (variables && variables.length) {
variables.forEach((v) => {
const ref = v.substring(1);
if (path.indexOf(ref) !== -1) {
throwCyclical(v);
}
const refValue = resolveValueReferences(refs[ref], [...path, ref]);
value = value.replace(v, refValue);
});
}
return value;
}
Object.keys(refs).forEach((key) => {
refs[key] = resolveValueReferences(refs[key], [key]);
});
return {
get(value) {
return resolveValueReferences(value);
},
resolve(style) {
return resolveStyle(style, refs, replacer);
},
references() {
return refs;
},
};
}
export default resolver;

View File

@@ -0,0 +1,67 @@
import resolver from './resolver';
const rxAppend = /^&/;
const rxCap = /[A-Z]/g;
const idGen = [[10, 31], [0, 31], [0, 31], [0, 31], [0, 31], [0, 31]];
function toChar([min, max]) {
return (min + (Math.random() * (max - min) | 0)).toString(32);
}
function uid() {
return idGen.map(toChar).join('');
}
function parse(id, style, sheet, r) {
const rule = [];
const post = {};
Object.keys(style).forEach((prop) => {
if (rxAppend.test(prop)) {
post[prop.slice(1)] = style[prop];
} else {
const value = r.get(style[prop]);
rule.push(`${prop.replace(rxCap, '-$&').toLowerCase()}: ${value}`);
}
});
sheet.insertRule(`.${id} { ${rule.join(';')} }`, sheet.cssRules.length);
Object.keys(post).forEach((key) => {
parse(`${id}${key}`, post[key], sheet, r);
});
}
const theme = (definition) => {
const res = resolver(definition);
let injectedSheet;
if (typeof window !== 'undefined') {
const el = document.createElement('style');
el.setAttribute('data-id', 'nebula-ui');
el.setAttribute('data-version', '0.1.0');
injectedSheet = document.head.appendChild(el).sheet;
}
const cache = {};
return {
style(s) {
const styles = Array.isArray(s) ? s : [s];
return styles.filter(Boolean).map((style) => {
const key = JSON.stringify(style);
if (cache[key]) {
return cache[key].className;
}
const className = uid();
parse(className, style, injectedSheet, res);
cache[key] = {
style,
className,
};
return className;
}).join(' ');
},
clear() {
},
};
};
export default theme;

View File

@@ -73,7 +73,7 @@ const config = (isEsm) => {
}),
commonjs({
namedExports: {
react: ['useState', 'useEffect', 'useRef', 'useContext', 'useCallback', 'createElement', 'PureComponent'],
react: ['useState', 'useEffect', 'useRef', 'useContext', 'useCallback', 'useMemo', 'createElement', 'PureComponent'],
'react-dom': ['createPortal'],
},
}),