mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 09:48:18 -05:00
feat: improve themeability
This commit is contained in:
10
aw.config.js
10
aw.config.js
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = {};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
68
packages/ui/theme/__tests__/resolver.spec.js
Normal file
68
packages/ui/theme/__tests__/resolver.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
79
packages/ui/theme/definitions/light.js
Normal file
79
packages/ui/theme/definitions/light.js
Normal 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;
|
||||
16
packages/ui/theme/index.js
Normal file
16
packages/ui/theme/index.js
Normal 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];
|
||||
}
|
||||
69
packages/ui/theme/resolver.js
Normal file
69
packages/ui/theme/resolver.js
Normal 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;
|
||||
67
packages/ui/theme/theme.js
Normal file
67
packages/ui/theme/theme.js
Normal 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;
|
||||
@@ -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'],
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user