feat(ui): add ui package (#21)

This commit is contained in:
Miralem Drek
2019-04-18 16:13:17 +02:00
committed by GitHub
parent 6577309c12
commit 7e710e71c1
39 changed files with 676 additions and 341 deletions

View File

@@ -32,8 +32,8 @@
"@babel/preset-react": "^7.0.0",
"@commitlint/cli": "^7.4.0",
"@commitlint/config-conventional": "^7.3.1",
"babel-plugin-istanbul": "^5.1.0",
"babel-loader": "^8.0.5",
"babel-plugin-istanbul": "^5.1.0",
"cross-env": "^5.2.0",
"enigma.js": "^2.4.0",
"eslint": "^5.12.1",

View File

@@ -10,6 +10,7 @@ describe('<AppSelections />', () => {
canGoForward: () => 'canGoForward',
canGoBack: () => 'canGoBack',
canClear: () => 'canClear',
layout: () => (null),
back: sinon.spy(),
forward: sinon.spy(),
clear: sinon.spy(),
@@ -24,6 +25,7 @@ describe('<AppSelections />', () => {
back: 'canGoBack',
forward: 'canGoForward',
clear: 'canClear',
items: [],
});
});
});
@@ -46,20 +48,29 @@ describe('<AppSelections />', () => {
it('should render a toolbar', () => {
api.canGoBack = () => false;
const Button = ({ className, disabled, children }) => <b c={className} d={disabled}>{children}</b>;
const Icon = ({ name }) => `i:${name}`;
const Button = ({ disabled, children }) => <b d={disabled}>{children}</b>;
const Toolbar = ({ children }) => <t>{children}</t>;
const Grid = ({ children }) => <g>{children}</g>;
const [{ AppSelections: AS }] = aw.mock([
['**/Button.jsx', () => Button],
['**/Icon.jsx', () => Icon],
['**/SelectionsBack.jsx', () => () => 'Back'],
['**/SelectionsForward.jsx', () => () => 'Forward'],
['**/ClearSelections.jsx', () => () => 'Clear'],
['**/ButtonInline.jsx', () => Button],
['**/Toolbar.jsx', () => Toolbar],
['**/Grid.jsx', () => Grid],
['**/Text.jsx', () => Button],
], ['../../src/components/AppSelections']);
const html = render.render(<AS api={api} />);
expect(html).to.equal(`
<div class="nebula-toolbar">
<div class="nebula-selections-nav">
<b c="lui-fade-button" d>i:selections-back</b>
<b c="lui-fade-button">i:selections-forward</b>
<b c="lui-fade-button">i:clear-selections</b>
</div>
</div>`.replace(/\n(\s+)/g, ''));
<t>
<g>
<g>
<b d>Back</b>
<b>Forward</b>
<b>Clear</b>
</g>
<g></g>
</g>
</t>`.replace(/\n(\s+)/g, ''));
});
});

View File

@@ -14,10 +14,10 @@ describe('<Header />', () => {
const layout = { showTitles: true, title: 'foo' };
const html = render(<Header layout={layout} />);
looksLike(html, `
<header class="nucleus-cell__header">
<div class="nucleus-type--m">foo</div>
<div class="nucleus-type--s"></div>
</header>
<div>
<span>foo</span>
<span></span>
</div>
`);
});
@@ -25,10 +25,10 @@ describe('<Header />', () => {
const layout = { showTitles: true, subtitle: 'foo' };
const html = render(<Header layout={layout} />);
looksLike(html, `
<header class="nucleus-cell__header">
<div class="nucleus-type--m"></div>
<div class="nucleus-type--s">foo</div>
</header>
<div>
<span></span>
<span>foo</span>
</div>
`);
});
});

View File

@@ -109,10 +109,12 @@ describe('<SelectionToolbar />', () => {
},
};
const STItem = ({ key, isCustom }) => `-${key}:${isCustom}-`;
const styled = () => ['a'];
const [{ default: STB }] = aw.mock([
['**/SelectionToolbarItem.jsx', () => STItem],
['**/styled.js', () => styled],
], ['../../src/components/SelectionToolbar']);
const html = render.render(<STB sn={props.sn} />);
expect(html).to.equal('<div class="nucleus-selection-toolbar">-mine:true--clear:false--cancel:false--confirm:false-</div>');
expect(html).to.equal('<div class="a">-mine:true--clear:false--cancel:false--confirm:false-</div>');
});
});

View File

@@ -21,6 +21,7 @@
},
"devDependencies": {
"@nebula.js/supernova": "^0.1.0",
"@nebula.js/ui": "^0.1.0",
"html-looks-like": "^1.0.3",
"node-event-emitter": "^0.0.1",
"preact": "^8.4.2",

View File

@@ -1,7 +1,125 @@
import preact from 'preact';
import Button from './Button';
import Icon from './Icon';
import SelectionsBack from '@nebula.js/ui/icons/SelectionsBack';
import SelectionsForward from '@nebula.js/ui/icons/SelectionsForward';
import ClearSelections from '@nebula.js/ui/icons/ClearSelections';
import Remove from '@nebula.js/ui/icons/Remove';
import Lock from '@nebula.js/ui/icons/Lock';
import ButtonInline from '@nebula.js/ui/components/ButtonInline';
import Toolbar from '@nebula.js/ui/components/Toolbar';
import Grid from '@nebula.js/ui/components/Grid';
import Text from '@nebula.js/ui/components/Text';
function collect(qSelectionObject, fields, state = '$') {
qSelectionObject.qSelections.forEach((selection) => {
const name = selection.qField;
const field = fields[name] = fields[name] || { name, states: [], selections: [] }; // eslint-disable-line
if (field.states.indexOf(state) === -1) {
field.states.push(state);
field.selections.push(selection);
}
});
}
function getItems(layout) {
if (!layout) {
return [];
}
const fields = {};
collect(layout.qSelectionObject, fields);
if (layout.alternateStates || layout.selectionsInStates) {
layout.alternateStates.forEach(s => collect(s.qSelectionObject, fields, s.stateName));
}
return Object.keys(fields).map(key => fields[key]);
}
function OneState({
field,
api,
}) {
const selection = field.selections[0];
const counts = selection.qStateCounts;
const green = (counts.qSelected + counts.qLocked) / selection.qTotal;
const white = counts.qOption / selection.qTotal;
const grey = counts.qAlternative / selection.qTotal;
const numSelected = counts.qSelected + counts.qSelectedExcluded + counts.qLocked + counts.qLockedExcluded;
let label = '&nbsp;'; // FIXME translate
if (selection.qTotal === numSelected && selection.qTotal > 1) {
label = 'All';
} else if (numSelected > 1 && selection.qTotal) {
label = `${numSelected} of ${selection.qTotal}`;
} else {
label = selection.qSelectedFieldSelectionInfo.map(v => v.qName).join(', ');
}
if (field.states[0] !== '$') {
label = `${field.states[0]}: ${label}`;
}
return (
<Grid
spacing="small"
style={{
position: 'relative',
width: '148px',
justifyContent: 'space-between',
background: '$grey100',
borderRight: '1px solid $alpha15',
}}
>
<Grid vertical spacing="small" style={{ alignItems: 'normal', overflow: 'hidden', opacity: selection.qLocked ? '0.3' : '' }}>
<Text size="small" weight="semibold" nowrap>{selection.qField}</Text>
<Text size="small" faded nowrap>{label}</Text>
</Grid>
{selection.qLocked ? (<Grid><Lock /></Grid>) : (
<Grid spacing="none">
<ButtonInline
onClick={() => api.clearField(selection.qField, field.states[0])}
>
<Remove />
</ButtonInline>
</Grid>
)}
<Grid
spacing="none"
style={{
height: '4px',
position: 'absolute',
bottom: '0',
left: '0',
width: '100%',
}}
>
<div style={{ background: '#6CB33F', height: '100%', width: `${green * 100}%` }} />
<div style={{ background: '#D8D8D8', height: '100%', width: `${white * 100}%` }} />
<div style={{ background: '#B4B4B4', height: '100%', width: `${grey * 100}%` }} />
</Grid>
</Grid>
);
}
function MultiState({
field,
}) {
return (
<Grid
spacing="small"
style={{
width: '148px',
justifyContent: 'space-between',
height: '48px',
boxSizing: 'border-box',
background: '$grey100',
borderRight: '1px solid $alpha15',
}}
className="nebula-ui-cs-group"
>
<Grid vertical spacing="small" style={{ overflow: 'hidden' }}>
<Text size="small" weight="semibold" nowrap>{field.name}</Text>
</Grid>
</Grid>
);
}
export class AppSelections extends preact.Component {
constructor(props) {
@@ -11,6 +129,7 @@ export class AppSelections extends preact.Component {
forward: this.props.api.canGoForward(),
back: this.props.api.canGoBack(),
clear: this.props.api.canClear(),
items: getItems(this.props.api.layout()),
};
this.onBack = () => {
@@ -30,6 +149,7 @@ export class AppSelections extends preact.Component {
forward: this.props.api.canGoForward(),
back: this.props.api.canGoBack(),
clear: this.props.api.canClear(),
items: getItems(this.props.api.layout()),
});
};
}
@@ -44,31 +164,35 @@ export class AppSelections extends preact.Component {
render() {
return (
<div className="nebula-toolbar">
<div className="nebula-selections-nav">
<Button
className="lui-fade-button"
disabled={!this.state.back}
onClick={this.onBack}
>
<Icon name="selections-back" />
</Button>
<Button
className="lui-fade-button"
disabled={!this.state.forward}
onClick={this.onForward}
>
<Icon name="selections-forward" />
</Button>
<Button
className="lui-fade-button"
disabled={!this.state.clear}
onClick={this.onClear}
>
<Icon name="clear-selections" />
</Button>
</div>
</div>
<Toolbar>
<Grid spacing="none">
<Grid style={{ background: '$grey100', borderRight: '1px solid $alpha15' }}>
<ButtonInline
style={{ marginRight: '8px' }}
disabled={!this.state.back}
onClick={this.onBack}
>
<SelectionsBack />
</ButtonInline>
<ButtonInline
style={{ marginRight: '8px' }}
disabled={!this.state.forward}
onClick={this.onForward}
>
<SelectionsForward />
</ButtonInline>
<ButtonInline
disabled={!this.state.clear}
onClick={this.onClear}
>
<ClearSelections />
</ButtonInline>
</Grid>
<Grid spacing="none">
{this.state.items.map(s => (s.states.length > 1 ? <MultiState field={s} /> : <OneState field={s} api={this.props.api} />))}
</Grid>
</Grid>
</Toolbar>
);
}
}

View File

@@ -1,42 +0,0 @@
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/button/button.js
import preact from 'preact';
import { luiClassName } from '../lui/util';
export default class Button extends preact.Component {
render() {
const {
children,
className,
variant,
size,
block,
rounded,
active,
...extraProps
} = this.props;
const finalClassName = luiClassName('button', {
className,
modifiers: {
variant,
size,
block,
rounded,
},
states: { active },
});
return (
<button
type="button"
ref={(element) => { this.element = element; }}
className={finalClassName}
{...extraProps}
>
{children}
</button>
);
}
}

View File

@@ -1,6 +1,7 @@
import preact from 'preact';
import { prefixer } from '../utils/utils';
import Grid from '@nebula.js/ui/components/Grid';
import styled from '@nebula.js/ui/components/styled';
import Requirements from './Requirements';
import CError from './Error';
@@ -11,10 +12,10 @@ import Placeholder from './Placeholder';
import SelectionToolbar from './SelectionToolbar';
const showRequirements = (sn, layout) => {
if (!sn || !sn.definition || !sn.definition.qae || !layout || !layout.qHyperCube) {
if (!sn || !sn.generator || !sn.generator.qae || !layout || !layout.qHyperCube) {
return false;
}
const def = sn.definition.qae.data.targets[0];
const def = sn.generator.qae.data.targets[0];
if (!def) {
return false;
}
@@ -25,14 +26,34 @@ const showRequirements = (sn, layout) => {
};
const Content = ({ children }) => (
<div className={prefixer(['content'])}>
<div className={prefixer(['content__body'])}>
<div style={{ position: 'relative', height: '100%' }}>
<div
className="nebulajs-sn"
style={{
position: 'absolute',
top: '8px',
left: '8px',
right: '8px',
bottom: '8px',
}}
>
{children}
</div>
</div>
);
class Cell extends preact.Component {
constructor(...args) {
super(...args);
this.styledClasses = ['nebulajs', ...styled({
fontSize: '$fontSize',
lineHeight: '$lineHeight',
fontWeight: '400',
fontFamily: '$fontFamily',
color: '$grey25',
})].join(' ');
}
componentDidCatch() {
this.setState({
error: {
@@ -51,31 +72,33 @@ class Cell extends preact.Component {
const Comp = !objectProps.sn ? Placeholder : SN;
const err = objectProps.error || this.state.error;
return (
<div className={prefixer(['cell-wrapper'])}>
<div className={this.styledClasses} style={{ height: '100%' }}>
{
objectProps.sn
&& objectProps.layout.qSelectionInfo
&& objectProps.layout.qSelectionInfo.qInSelections
&& <SelectionToolbar sn={objectProps.sn} />
}
<div className={prefixer(['cell'])}>
<Header layout={objectProps.layout} />
<Content>
{err
? (<CError {...err} />)
: (
<Comp
key={objectProps.layout.visualization}
sn={objectProps.sn}
snContext={userProps.context}
snOptions={userProps.options}
layout={objectProps.layout}
/>
)
}
</Content>
<Grid vertical style={{ height: '100%' }}>
<Header layout={objectProps.layout}>&nbsp;</Header>
<Grid.Item>
<Content>
{err
? (<CError {...err} />)
: (
<Comp
key={objectProps.layout.visualization}
sn={objectProps.sn}
snContext={userProps.context}
snOptions={userProps.options}
layout={objectProps.layout}
/>
)
}
</Content>
</Grid.Item>
<Footer layout={objectProps.layout} />
</div>
</Grid>
</div>
);
}

View File

@@ -1,62 +0,0 @@
.#{$ns}-cell {
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
display: flex;
flex-direction: column;
font-size: 14px;
line-height: 16px;
color: #404040;
font-family: 'Source Sans Pro', Arial, 'sans-serif';
.#{$ns}-cell__header {
flex: 0 0 auto;
padding: $spacing;
box-sizing: border-box;
}
.#{$ns}-cell__footnote {
flex: 0 0 auto;
padding: $spacing;
box-sizing: border-box;
}
.#{$ns}-content {
flex: 1 0 auto;
}
}
.#{$ns}-cell__error {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAM0lEQVQYV2P8z8BgzMjAcJYBCUDFkIUYGECCIBFGdJVQwbNwCSSVYGPBEuiCYAlsgiAJANbwDGtj7MYdAAAAAElFTkSuQmCC);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.#{$ns}-content {
position: relative;
.#{$ns}-content__body {
position: absolute;
top: $spacing;
left: $spacing;
bottom: $spacing;
right: $spacing;
}
}
.#{$ns}-type--s {
font-size: 12px;
line-height: 16px;
}
.#{$ns}-type--m {
font-size: 16px;
line-height: 20px;
}

View File

@@ -1,41 +0,0 @@
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/fade-button/fade-button.js
import preact from 'preact';
import { luiClassName } from '../lui/util';
const FadeButton = ({
className,
children,
variant,
size,
block,
rounded,
active,
...extraProps
}) => {
const finalClassName = luiClassName('fade-button', {
className,
modifiers: {
variant,
size,
block,
rounded,
},
states: { active },
});
return (
<button
type="button"
className={finalClassName}
{
...extraProps
}
>
{children}
</button>
);
};
export default FadeButton;

View File

@@ -1,11 +1,12 @@
import preact from 'preact';
import { prefixer } from '../utils/utils';
import Text from '@nebula.js/ui/components/Text';
import Grid from '@nebula.js/ui/components/Grid';
const Footer = ({ layout }) => (
layout && layout.showTitles && layout.footnote ? (
<footer className={prefixer(['cell__footnote'])}>
<div className={prefixer(['type', 'type--s'])}>{layout.footnote}</div>
</footer>
<Grid>
<Text nowrap size="small">{layout.footnote}</Text>
</Grid>
) : null
);

View File

@@ -1,14 +1,14 @@
import preact from 'preact';
import { prefixer } from '../utils/utils';
import Text from '@nebula.js/ui/components/Text';
const Header = ({ layout }) => (
const Header = ({
layout,
}) => (
layout && layout.showTitles && (layout.title || layout.subtitle) ? (
<header className={prefixer(['cell__header'])}>
<div className={prefixer(['type--m'])}>
{layout.title}
</div>
<div className={prefixer(['type--s'])}>{layout.subtitle}</div>
</header>
<div style={{ background: 'transparent', padding: '0 8px' }}>
<Text size="large" nowrap block>{layout.title}</Text>
<Text faded size="small" nowrap block>{layout.subtitle}</Text>
</div>
) : null
);

View File

@@ -1,22 +0,0 @@
// https://github.com/ricsv/react-leonardo-ui/blob/master/src/icon/icon.js
import preact from 'preact';
import { luiClassName } from '../lui/util';
const Icon = ({
className,
name,
size,
...extraProps
}) => {
const finalClassName = luiClassName('icon', {
className,
modifiers: { name, size },
});
return (
<span className={finalClassName} aria-hidden="true" {...extraProps} />
);
};
export default Icon;

View File

@@ -1,10 +1,8 @@
import preact from 'preact';
import styled from '@nebula.js/ui/components/styled';
import Item from './SelectionToolbarItem';
import { prefixer } from '../utils/utils';
/* eslint react/no-multi-comp: 0 */
class Component extends preact.Component {
constructor(props) {
super(props);
@@ -16,6 +14,18 @@ class Component extends preact.Component {
clearable: api.canClear(),
};
this.styledClasses = styled({
position: 'absolute',
left: '0',
right: '0',
top: '-48px',
padding: '8px',
boxSizing: 'border-box',
background: '$grey100',
display: 'flex',
justifyContent: 'flex-end',
});
const items = [];
// TODO - translations
@@ -72,7 +82,7 @@ class Component extends preact.Component {
render() {
return (
<div className={prefixer('selection-toolbar')}>
<div className={this.styledClasses}>
{this.state.items.map(itm => <Item key={itm.key} item={itm} isCustom={!!this.custom[itm.key]} />)}
</div>
);

View File

@@ -1,16 +0,0 @@
.#{$ns}-selection-toolbar {
position: absolute;
left: 0;
right: 0;
top: -48px;
padding: $spacing;
line-height: 24px;
box-sizing: border-box;
background: white;
display: flex;
justify-content: flex-end;
> * {
margin-left: 8px;
}
}

View File

@@ -1,8 +1,16 @@
import preact from 'preact';
import Button from './Button';
import FadeButton from './FadeButton';
import Icon from './Icon';
import ButtonInline from '@nebula.js/ui/components/ButtonInline';
import CloseIcon from '@nebula.js/ui/icons/Close';
import TickIcon from '@nebula.js/ui/icons/Tick';
import ClearSelections from '@nebula.js/ui/icons/ClearSelections';
const ICONS = {
close: CloseIcon,
tick: TickIcon,
'clear-selections': ClearSelections,
};
export default class Item extends preact.Component {
constructor(props) {
@@ -41,16 +49,15 @@ export default class Item extends preact.Component {
render() {
const props = this.props.item;
const Btn = props.type === 'fade-button' || this.props.isCustom ? FadeButton : Button;
const Icon = ICONS[props.icon] || null;
return (
<Btn
<ButtonInline
onClick={() => props.action()}
variant={props.variant}
active={this.state.active}
disabled={this.state.disabled}
>
{props.icon ? <Icon name={props.icon} /> : props.label}
</Btn>
{<Icon />}
</ButtonInline>
);
}
}

View File

@@ -1,13 +0,0 @@
$ns: nucleus;
$spacing: 8px;
@import 'SelectionToolbar';
@import 'Cell';
.nebula-toolbar {
background: #fff;
.nebula-selections-nav > * {
margin: 4px;
}
}

View File

@@ -1,8 +1,6 @@
import preact from 'preact';
import Cell from './Cell';
import './Style.scss';
export default function boot({
element,
model,

View File

@@ -1,38 +0,0 @@
export function camelToKebabCase(value) {
return value.replace(/([A-Z])/g, '-$1').toLowerCase();
}
export function luiClassName(name, opts = {}) {
const {
className,
modifiers = {},
states = {},
} = opts;
const baseClass = `lui-${name}`;
let resClassName = baseClass;
Object.keys(modifiers).forEach((key) => {
// Modifiers can be booleans or key-value pair of strings
if (typeof modifiers[key] === 'boolean') {
if (modifiers[key]) {
resClassName += ` ${baseClass}--${key}`;
}
} else if (modifiers[key]) {
resClassName += ` ${baseClass}--${modifiers[key]}`;
}
});
Object.keys(states).forEach((key) => {
// States are always booleans
if (states[key]) {
resClassName += ` lui-${key}`;
}
});
if (className) {
resClassName += ` ${className}`;
}
return resClassName;
}

View File

@@ -11,6 +11,7 @@ const create = (app) => {
let modalObject;
let mounted;
let lyt;
const api = {
switchModal(object, path, accept = true) {
if (object === modalObject) {
@@ -52,6 +53,9 @@ const create = (app) => {
canClear() {
return canClear;
},
layout() {
return lyt;
},
forward() {
this.switchModal();
return app.forward();
@@ -64,6 +68,9 @@ const create = (app) => {
this.switchModal();
return app.clearAll();
},
clearField(field, state = '$') {
return app.getField(field, state).then(f => f.clear());
},
mount(element) {
if (mounted) {
console.error('Already mounted');
@@ -83,17 +90,24 @@ const create = (app) => {
eventmixin(api);
const prom = app.getObject('CurrentSelection');
const prom = app.getObject('CurrentSelectionB');
const obj = new Promise((resolve) => {
prom.then((sel) => {
resolve(sel);
}).catch(() => {
app.createSessionObject({
qInfo: {
qId: 'CurrentSelection',
qType: 'CurrentSelection',
qId: 'CurrentSelectionB',
qType: 'CurrentSelectionB',
},
qSelectionObjectDef: {},
alternateStates: [{
stateName: 'andal',
qSelectionObjectDef: { qStateName: 'andal' },
}, {
stateName: 'forest',
qSelectionObjectDef: { qStateName: 'forest' },
}],
}).then((sel) => {
resolve(sel);
});
@@ -101,9 +115,11 @@ const create = (app) => {
});
obj.then((model) => {
const onChanged = () => model.getLayout().then((layout) => {
// FIXME - if all currently selected fields are locked, no actions should be allowed
canGoBack = layout.qSelectionObject && layout.qSelectionObject.qBackCount > 0;
canGoForward = layout.qSelectionObject && layout.qSelectionObject.qForwardCount > 0;
canClear = layout.qSelectionObject && layout.qSelectionObject.qSelections.length > 0;
lyt = layout;
api.emit('changed');
});
model.on('changed', onChanged);

View File

@@ -99,7 +99,7 @@ export default function ({
takeSnapshot() {
return c.then((x) => {
if (x.reference) {
const content = x.reference.querySelector('.nucleus-content__body');
const content = x.reference.querySelector('.nebulajs-sn');
if (content) {
const rect = content.getBoundingClientRect();
if (objectProps.sn) {

View File

@@ -0,0 +1,47 @@
import preact from 'preact';
import styled from './styled';
export default function ButtonInline({
children,
disabled,
...props
}) {
const s = {
};
const classes = styled({
padding: '8px',
border: '0px solid transparent',
background: 'transparent',
borderRadius: '2px',
cursor: 'pointer',
color: '$grey25',
font: 'normal 14px/0 "Source Sans Pro", Arial, sans-serif',
'&:hover': {
background: 'rgba(0, 0, 0, 0.03)',
},
'&:active': {
background: 'rgba(0, 0, 0, 0.1)',
},
'&:disabled': {
opacity: '0.3',
cursor: 'default',
},
});
return (
<button
className={classes.join(' ')}
type="button"
disabled={disabled}
style={s}
{...props}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,53 @@
import preact from 'preact';
import styled from './styled';
export default function Grid({
children,
vertical,
spacing,
style,
...props
}) {
// const classNames = [
// 'nebula-ui-grid',
// vertical ? 'vertical' : 'horizontal',
// noSpacing ? 'no-spacing' : '',
// ].join(' ');
let space = '8px';
if (spacing === 'none') {
space = '0px';
} else if (spacing === 'small') {
space = '4px';
}
const inlineStyle = {
padding: space,
display: 'flex',
flexDirection: vertical ? 'column' : '',
alignItems: vertical ? '' : 'center',
};
const classes = styled([inlineStyle, style, { boxSizing: 'border-box' }]);
return (
<div
className={classes.join(' ')}
{...props}
>
{children}
</div>
);
}
Grid.Item = function GridItem({
children,
style,
}) {
const classes = styled([{
flex: '1 0 auto',
}, style]);
return (
<div className={classes.join(' ')}>
{children}
</div>
);
};

View File

@@ -0,0 +1,53 @@
import preact from 'preact';
import styled from './styled';
const sizes = {
small: { fontSize: '12px', lineHeight: '16px' },
medium: { fontSize: '14px', lineHeight: '16px' },
large: { fontSize: '16px', lineHeight: '24px' },
xlarge: { fontSize: '24px', lineHeight: '32px' },
};
const weights = {
light: 300,
regular: 400,
semibold: 600,
};
export default function Text({
children,
style,
size = 'medium',
weight = 'regular',
nowrap,
faded,
block,
}) {
const { fontSize, lineHeight } = sizes[size];
const fontWeight = weights[weight];
const nowrapStyle = nowrap ? {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
} : {};
const family = {
fontFamily: '$fontFamily',
};
const inlineStyle = {
fontSize,
fontWeight,
lineHeight,
display: block ? 'block' : 'inline-block',
color: faded ? '$alpha55' : '',
};
const classes = styled([family, inlineStyle, nowrapStyle, style]);
return (
<span className={classes.join(' ')}>
{children}
</span>
);
}

View File

@@ -0,0 +1,24 @@
import preact from 'preact';
import styled from './styled';
export default function Toolbar({
children,
style,
}) {
const classes = styled([{
background: '$grey90',
color: '$grey25',
height: '48px',
boxShadow: '0 1px 0 $alpha15',
fontFamily: '"Source Sans Pro", Arial, sans-serif',
}, style]);
return (
<div
className={classes.join(' ')}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,90 @@
const theme = {
$grey100: '#fff',
$grey98: 'fafafa',
$grey95: 'f2f2f2',
$grey90: '#e6e6e6',
$grey25: '#404040',
$alpha15: 'rgba(0, 0, 0, 0.15)',
$alpha55: 'rgba(0, 0, 0, 0.55)',
$green: '#6CB33F',
$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}`);
}
});
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,7 @@
import SvgIcon from './SvgIcon';
export default function SelectionsBack() {
return SvgIcon({
d: 'M6,15.5 L6,15.5 C6,15.2238576 6.22385763,15 6.5,15 L9.5,15 C9.77614237,15 10,15.2238576 10,15.5 L10,15.5 C10,15.7761424 9.77614237,16 9.5,16 L6.5,16 C6.22385763,16 6,15.7761424 6,15.5 Z M1,13.5 L1,14.5 C1,14.7761424 1.22385763,15 1.5,15 L2.5,15 C2.77614237,15 3,15.2238576 3,15.5 L3,15.5 C3,15.7761424 2.77614237,16 2.5,16 L1,16 C0.44771525,16 6.76353751e-17,15.5522847 0,15 L0,13.5 C-3.38176876e-17,13.2238576 0.223857625,13 0.5,13 L0.5,13 C0.776142375,13 1,13.2238576 1,13.5 Z M1,6.5 L1,9.5 C1,9.77614237 0.776142375,10 0.5,10 L0.5,10 C0.223857625,10 3.38176876e-17,9.77614237 0,9.5 L0,6.5 C-3.38176876e-17,6.22385763 0.223857625,6 0.5,6 L0.5,6 C0.776142375,6 1,6.22385763 1,6.5 Z M0,2.5 L0,1 C-6.76353751e-17,0.44771525 0.44771525,1.01453063e-16 1,0 L2.5,0 C2.77614237,-5.07265313e-17 3,0.223857625 3,0.5 L3,0.5 C3,0.776142375 2.77614237,1 2.5,1 L1.5,1 C1.22385763,1 1,1.22385763 1,1.5 L1,2.5 C1,2.77614237 0.776142375,3 0.5,3 L0.5,3 C0.223857625,3 3.38176876e-17,2.77614237 0,2.5 Z M6,0.5 L6,0.5 C6,0.223857625 6.22385763,5.07265313e-17 6.5,0 L9.5,0 C9.77614237,-5.07265313e-17 10,0.223857625 10,0.5 L10,0.5 C10,0.776142375 9.77614237,1 9.5,1 L6.5,1 C6.22385763,1 6,0.776142375 6,0.5 Z M15,2.5 L15,1.5 C15,1.22385763 14.7761424,1 14.5,1 L13.5,1 C13.2238576,1 13,0.776142375 13,0.5 L13,0.5 C13,0.223857625 13.2238576,5.07265313e-17 13.5,0 L15,0 C15.5522847,-1.01453063e-16 16,0.44771525 16,1 L16,2.5 C16,2.77614237 15.7761424,3 15.5,3 L15.5,3 C15.2238576,3 15,2.77614237 15,2.5 Z M9.1661442,6.1661442 C10.7210031,4.61128527 13.2789969,4.61128527 14.8338558,6.1661442 C16.3887147,7.72100313 16.3887147,10.2789969 14.8338558,11.8338558 C13.2789969,13.3887147 10.7210031,13.3887147 9.1661442,11.8338558 C7.61128527,10.2789969 7.61128527,7.77115987 9.1661442,6.1661442 Z M14.1316614,7.72100313 C14.3322884,7.52037618 14.3824451,7.169279 14.1316614,6.9184953 C13.8808777,6.6677116 13.5297806,6.71786834 13.3291536,6.9184953 L12.0250784,8.22257053 L10.7210031,6.9184953 C10.5203762,6.6677116 10.1191223,6.6677116 9.9184953,6.9184953 C9.6677116,7.11912226 9.6677116,7.52037618 9.9184953,7.72100313 L11.2225705,9.02507837 L9.9184953,10.3291536 C9.6677116,10.5297806 9.6677116,10.8808777 9.9184953,11.1316614 C10.169279,11.3824451 10.5203762,11.3824451 10.7210031,11.1316614 L12.0250784,9.82758621 L13.3291536,11.1316614 C13.5297806,11.3824451 13.8808777,11.3824451 14.1316614,11.1316614 C14.3322884,10.9310345 14.3824451,10.5297806 14.1316614,10.3291536 L12.8275862,9.02507837 L14.1316614,7.72100313 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function Close() {
return SvgIcon({
d: 'M9.34535242,8 L13.3273238,11.9819714 C13.6988326,12.3534802 13.6988326,12.955815 13.3273238,13.3273238 C12.955815,13.6988326 12.3534802,13.6988326 11.9819714,13.3273238 L8,9.34535242 L4.01802863,13.3273238 C3.64651982,13.6988326 3.04418502,13.6988326 2.67267621,13.3273238 C2.3011674,12.955815 2.3011674,12.3534802 2.67267621,11.9819714 L6.65464758,8 L2.67267621,4.01802863 C2.3011674,3.64651982 2.3011674,3.04418502 2.67267621,2.67267621 C3.04418502,2.3011674 3.64651982,2.3011674 4.01802863,2.67267621 L8,6.65464758 L11.9819714,2.67267621 C12.3534802,2.3011674 12.955815,2.3011674 13.3273238,2.67267621 C13.6988326,3.04418502 13.6988326,3.64651982 13.3273238,4.01802863 L9.34535242,8 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function Lasso() {
return SvgIcon({
d: 'M15.9488039,5.20769129 C16.0487326,6.70662306 15.3492311,8.30548361 14.050157,9.30477145 C12.651154,10.5039169 10.8524359,10.8037032 8.85386017,10.4039881 L7.3549284,10.0042729 L5.75606786,9.70448659 C5.75606786,9.90434416 5.65613907,10.0042729 5.4562815,10.2041305 C5.05656637,10.6038456 4.55692244,10.8037032 4.05727852,10.8037032 C3.75749217,10.8037032 3.45770582,10.7037744 3.15791946,10.6038456 C3.05799068,10.903632 3.15791946,11.2034184 3.45770582,11.7030623 C5.05656637,14.0014243 3.85742095,15.9000712 3.75749217,16 L2.2585604,15.3004985 C2.2585604,15.2005697 2.95806189,14.0014243 1.95877405,12.6024213 C1.6589877,12.0028486 1.15934378,11.0035608 1.55905891,10.0042729 C1.6589877,9.80441537 1.75891648,9.6045578 1.95877405,9.40470024 C1.6589877,8.90505631 1.55905891,8.30548361 1.85884527,7.7059109 C1.55905891,7.40612455 1.25927256,7.1063382 1.15934378,6.70662306 C0.859557424,5.90719279 0.959486209,4.5081898 1.6589877,3.30904439 C1.95877405,2.6095429 2.55834676,2.0099702 3.15791946,1.51032628 C3.95734974,0.91075357 4.95663758,0.610967217 6.15578299,0.311180864 C9.05371774,-0.388320626 11.9516525,0.111323295 13.9502282,1.61025506 C15.1493736,2.50961412 15.8488751,3.80868831 15.9488039,5.20769129 Z M13.0508691,8.10562604 C13.8502994,7.40612455 14.3499433,6.40683671 14.3499433,5.30762008 C14.2500145,4.20840345 13.550513,3.40897318 12.9509403,2.90932926 C12.1515101,2.40968533 11.252151,2.0099702 10.1529344,1.81011263 C8.95378895,1.61025506 7.75464354,1.71018384 6.45556935,1.91004141 C4.75678001,2.30975655 3.65756338,3.00925804 3.05799068,4.10847467 C2.55834676,5.00783373 2.65827554,5.90719279 2.75820433,6.20697914 C2.75820433,6.30690792 2.85813311,6.40683671 3.05799068,6.40683671 C3.15791946,6.40683671 3.25784825,6.40683671 3.35777703,6.40683671 C3.45770582,6.40683671 3.45770582,6.40683671 3.45770582,6.40683671 L3.5576346,6.40683671 L3.65756338,6.40683671 C4.65685123,6.40683671 5.4562815,6.90648063 5.85599664,7.80583969 L5.85599664,8.00569726 C6.35564056,8.10562604 7.05514205,8.30548361 7.75464354,8.50534118 L9.25357531,8.90505631 C10.0530056,9.0049851 10.7525071,9.0049851 11.4520086,8.80512753 C12.0515813,8.70519875 12.5512252,8.40541239 13.0508691,8.10562604 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function Lock() {
return SvgIcon({
d: 'M13,7 L8,7 L13,7 L13,4.98151367 C13,2.23029964 10.7614237,0 8,0 C5.23857625,0 3,2.23029964 3,4.98151367 L3,7 L3.75,7 L3,7 L4.5,7 L4.5,5.33193359 C4.5,3.21561511 5.54860291,1.5 8,1.5 C10.4513971,1.5 11.5,3.21561511 11.5,5.33193359 L11.5,7 L12.25,7 L3,7 C2.44771525,7 2,7.44771525 2,8 L2,15 C2,15.5522847 2.44771525,16 3,16 L13,16 C13.5522847,16 14,15.5522847 14,15 L14,8 C14,7.44771525 13.5522847,7 13,7 L3,7 L13,7 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function Remove() {
return SvgIcon({
d: 'M9.41421356,8 L11.8890873,5.52512627 C12.065864,5.34834957 12.0305087,4.95944084 11.8183766,4.74730881 L11.2526912,4.18162338 C11.0405592,3.96949135 10.6516504,3.93413601 10.4748737,4.1109127 L8,6.58578644 L5.52512627,4.1109127 C5.34834957,3.93413601 4.95944084,3.96949135 4.74730881,4.18162338 L4.25233406,4.67659813 C3.96949135,4.95944084 3.93413601,5.34834957 4.1109127,5.52512627 L6.58578644,8 L4.1109127,10.4748737 C3.93413601,10.6516504 3.96949135,11.0405592 4.18162338,11.2526912 L4.67659813,11.7476659 C4.95944084,12.0305087 5.34834957,12.065864 5.52512627,11.8890873 L8,9.41421356 L10.4748737,11.8890873 C10.6516504,12.065864 11.0405592,12.0305087 11.2526912,11.8183766 L11.8183766,11.2526912 C12.0305087,11.0405592 12.065864,10.6516504 11.8890873,10.4748737 L9.41421356,8 Z M8,0 C12.4,0 16,3.6 16,8 C16,12.4 12.4,16 8,16 C3.6,16 0,12.4 0,8 C0,3.6 3.6,0 8,0 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function SelectionsBack() {
return SvgIcon({
d: 'M10,15.5 C10,15.7761424 9.77614237,16 9.5,16 L6.5,16 C6.22385763,16 6,15.7761424 6,15.5 C6,15.2238576 6.22385763,15 6.5,15 L9.5,15 C9.77614237,15 10,15.2238576 10,15.5 Z M15,13.5 C15,13.2238576 15.2238576,13 15.5,13 C15.7761424,13 16,13.2238576 16,13.5 L16,15 C16,15.5522847 15.5522847,16 15,16 L13.5,16 C13.2238576,16 13,15.7761424 13,15.5 C13,15.2238576 13.2238576,15 13.5,15 L14.5,15 C14.7761424,15 15,14.7761424 15,14.5 L15,13.5 Z M15,6.5 C15,6.22385763 15.2238576,6 15.5,6 C15.7761424,6 16,6.22385763 16,6.5 L16,9.5 C16,9.77614237 15.7761424,10 15.5,10 C15.2238576,10 15,9.77614237 15,9.5 L15,6.5 Z M16,2.5 C16,2.77614237 15.7761424,3 15.5,3 C15.2238576,3 15,2.77614237 15,2.5 L15,1.5 C15,1.22385763 14.7761424,1 14.5,1 L13.5,1 C13.2238576,1 13,0.776142375 13,0.5 C13,0.223857625 13.2238576,-5.07265313e-17 13.5,0 L15,0 C15.5522847,1.01453063e-16 16,0.44771525 16,1 L16,2.5 Z M10,0.5 C10,0.776142375 9.77614237,1 9.5,1 L6.5,1 C6.22385763,1 6,0.776142375 6,0.5 C6,0.223857625 6.22385763,-5.07265313e-17 6.5,0 L9.5,0 C9.77614237,5.07265313e-17 10,0.223857625 10,0.5 Z M1,2.5 C1,2.77614237 0.776142375,3 0.5,3 C0.223857625,3 5.18696197e-13,2.77614237 5.18696197e-13,2.5 L5.18696197e-13,1 C5.18696197e-13,0.44771525 0.44771525,-1.01453063e-16 1,0 L2.5,0 C2.77614237,5.07265313e-17 3,0.223857625 3,0.5 C3,0.776142375 2.77614237,1 2.5,1 L1.5,1 C1.22385763,1 1,1.22385763 1,1.5 L1,2.5 Z M1,13.5 L1,14.5 C1,14.7761424 1.22385763,15 1.5,15 L2.5,15 C2.77614237,15 3,15.2238576 3,15.5 C3,15.7761424 2.77614237,16 2.5,16 L1,16 C0.44771525,16 5.18696197e-13,15.5522847 5.18696197e-13,15 L5.18696197e-13,13.5 C5.18696197e-13,13.2238576 0.223857625,13 0.5,13 C0.776142375,13 1,13.2238576 1,13.5 Z M4,7 C7.49095643,7 10,10.1337595 10,12.1872632 C10,12.1872632 8.16051135,9.86624054 4,10 L4,12 C4,12 2.66666667,10.8333333 -1.0658141e-14,8.5 C-2.59348099e-13,8.5 1.33333333,7.33333333 4,5 C4,5 4,5.66666667 4,7 Z',
});
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function SelectionsForward() {
return SvgIcon({
d: 'M6,15.5 L6,15.5 C6,15.2238576 6.22385763,15 6.5,15 L9.5,15 C9.77614237,15 10,15.2238576 10,15.5 L10,15.5 C10,15.7761424 9.77614237,16 9.5,16 L6.5,16 C6.22385763,16 6,15.7761424 6,15.5 Z M1,13.5 L1,14.5 C1,14.7761424 1.22385763,15 1.5,15 L2.5,15 C2.77614237,15 3,15.2238576 3,15.5 L3,15.5 C3,15.7761424 2.77614237,16 2.5,16 L1,16 C0.44771525,16 6.76353751e-17,15.5522847 0,15 L0,13.5 C-3.38176876e-17,13.2238576 0.223857625,13 0.5,13 L0.5,13 C0.776142375,13 1,13.2238576 1,13.5 Z M1,6.5 L1,9.5 C1,9.77614237 0.776142375,10 0.5,10 L0.5,10 C0.223857625,10 3.38176876e-17,9.77614237 0,9.5 L0,6.5 C-3.38176876e-17,6.22385763 0.223857625,6 0.5,6 L0.5,6 C0.776142375,6 1,6.22385763 1,6.5 Z M0,2.5 L0,1 C-6.76353751e-17,0.44771525 0.44771525,1.01453063e-16 1,0 L2.5,0 C2.77614237,-5.07265313e-17 3,0.223857625 3,0.5 L3,0.5 C3,0.776142375 2.77614237,1 2.5,1 L1.5,1 C1.22385763,1 1,1.22385763 1,1.5 L1,2.5 C1,2.77614237 0.776142375,3 0.5,3 L0.5,3 C0.223857625,3 3.38176876e-17,2.77614237 0,2.5 Z M6,0.5 L6,0.5 C6,0.223857625 6.22385763,5.07265313e-17 6.5,0 L9.5,0 C9.77614237,-5.07265313e-17 10,0.223857625 10,0.5 L10,0.5 C10,0.776142375 9.77614237,1 9.5,1 L6.5,1 C6.22385763,1 6,0.776142375 6,0.5 Z M15,2.5 L15,1.5 C15,1.22385763 14.7761424,1 14.5,1 L13.5,1 C13.2238576,1 13,0.776142375 13,0.5 L13,0.5 C13,0.223857625 13.2238576,5.07265313e-17 13.5,0 L15,0 C15.5522847,-1.01453063e-16 16,0.44771525 16,1 L16,2.5 C16,2.77614237 15.7761424,3 15.5,3 L15.5,3 C15.2238576,3 15,2.77614237 15,2.5 Z M15,13.5 C15,13.2238576 15.2238576,13 15.5,13 C15.7761424,13 16,13.2238576 16,13.5 L16,15 C16,15.5522847 15.5522847,16 15,16 L13.5,16 C13.2238576,16 13,15.7761424 13,15.5 C13,15.2238576 13.2238576,15 13.5,15 L14.5,15 C14.7761424,15 15,14.7761424 15,14.5 L15,13.5 Z M12,7 C12,5.66666667 12,5 12,5 C14.6666667,7.33333333 16,8.5 16,8.5 C13.3333333,10.8333333 12,12 12,12 L12,10 C7.83948865,9.86624054 6,12.1872632 6,12.1872632 C6,10.1337595 8.50904357,7 12,7 Z',
});
}

View File

@@ -0,0 +1,44 @@
import preact from 'preact';
function getFontSize(size) {
if (size === 'large') {
return '20px';
}
if (size === 'small') {
return '12px';
}
return '16px';
}
export default function SvgIcon({
d,
size,
style = {},
}) {
const s = {
fontSize: getFontSize(size),
display: 'inline-block',
fontStyle: 'normal',
lineHeight: '0',
textAlign: 'center',
textTransform: 'none',
verticalAlign: '-.125em',
textRendering: 'optimizeLegibility',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
...style,
};
return (
<i style={s}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d={d} />
</svg>
</i>
);
}

View File

@@ -0,0 +1,7 @@
import SvgIcon from './SvgIcon';
export default function Tick() {
return SvgIcon({
d: 'M6,10 L13,3 L15,5 L8,12 L6,14 L1,9 L3,7 L6,10 Z',
});
}

13
packages/ui/package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "@nebula.js/ui",
"private": true,
"version": "0.1.0",
"description": "",
"license": "MIT",
"author": "QlikTech International AB",
"keywords": [],
"scripts": {},
"devDependencies": {
"preact": "^8.4.2"
}
}

View File

@@ -3,7 +3,6 @@ const nodeResolve = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');
const babel = require('rollup-plugin-babel');
const replace = require('rollup-plugin-replace');
const sass = require('rollup-plugin-sass');
const { terser } = require('rollup-plugin-terser');
const cwd = process.cwd();
@@ -62,9 +61,6 @@ const config = (isEsm) => {
replace({
'process.env.NODE_ENV': JSON.stringify(isEsm ? 'development' : 'production'),
}),
sass({
insert: true,
}),
nodeResolve({
extensions: ['.js', '.jsx'],
}),

View File

@@ -1,5 +1,5 @@
describe('sn', () => {
const content = '.nucleus-content__body';
const content = '.nebulajs-sn';
it('should say hello', async () => {
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
await page.goto(`http://localhost:8085/render/app/${app}`);

View File

@@ -1,5 +1,5 @@
describe('sn', () => {
const content = '.nucleus-content__body';
const content = '.nebulajs-sn';
it('should say hello', async () => {
const app = encodeURIComponent(process.env.APP_ID || '/apps/ctrl00.qvf');
await page.goto(`${process.testServer.url}/render/app/${app}`);