create vulcan:ui-material, update to Apollo2 RR4

This commit is contained in:
eric-burel 2019-03-15 10:11:56 +01:00
parent a743022545
commit b7b872591a
21 changed files with 217 additions and 479 deletions

View file

@ -6,7 +6,7 @@ import { STATES } from '../../helpers.js';
class AccountsEnrollAccount extends PureComponent { class AccountsEnrollAccount extends PureComponent {
componentDidMount() { componentDidMount() {
const token = this.props.params.token; const token = this.props.match.params.token;
Accounts._loginButtonsSession.set('enrollAccountToken', token); Accounts._loginButtonsSession.set('enrollAccountToken', token);
} }

View file

@ -1,15 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.js]
trim_trailing_whitespace = false
[*.md]
trim_trailing_whitespace = false

View file

@ -1,86 +0,0 @@
{
"extends": [
"eslint:recommended",
//"airbnb",
"plugin:meteor/recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true,
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"babel/generator-star-spacing": 0,
"babel/new-cap": [1, {
"capIsNewExceptions": [
"Optional",
"OneOf",
"Maybe",
"MailChimpAPI",
"Juice",
"Run",
"AppComposer",
"Query",
"InArray"
]
}],
"babel/array-bracket-spacing": 0,
"babel/object-curly-spacing": 0,
"babel/object-shorthand": 0,
"babel/arrow-parens": 0,
"babel/no-await-in-loop": 1,
"comma-dangle": 0,
"key-spacing": 0,
"no-extra-boolean-cast": 0,
"no-undef": 1,
"no-unused-vars": [1, {
"vars": "all",
"args": "none",
"varsIgnorePattern": "React|PropTypes|Component"
}],
"react/prop-types": 0,
"react/display-name": 0,
"meteor/audit-argument-checks": 0,
"meteor/no-session": 0,
"no-case-declarations": 0,
"no-console": 0,
"semi": "error",
"quotes": [
1,
"single"
]
},
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"meteor": true,
"node": true
},
"plugins": [
"babel",
"meteor",
"react",
"jsx-a11y"
],
"settings": {
"import/resolver": {
"meteor": {
"paths": [
"/usr/local/share/global_modules"
],
"moduleDirectory": [
"node_modules",
"packages"
]
}
}
},
"root": true,
"globals": {
"param": true,
"returns": true
}
}

View file

@ -1,3 +1,6 @@
npm-debug.log npm-debug.log
node_modules node_modules
.idea/workspace.xml .idea/workspace.xml
### eslint-config
.eslintrc

View file

@ -4,31 +4,40 @@ import { Components, registerComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n'; import { intlShape } from 'meteor/vulcan:i18n';
import EditIcon from 'mdi-material-ui/Pencil'; import EditIcon from 'mdi-material-ui/Pencil';
const EditButton = (
const EditButton = ({ {
collection, collection,
document, document,
color = 'default', color = 'default',
variant, variant,
triggerClasses, triggerClasses,
buttonClasses, buttonClasses,
showRemove,
...props ...props
}, { intl }) => ( },
{ intl }
) => (
<Components.ModalTrigger <Components.ModalTrigger
classes={triggerClasses} classes={triggerClasses}
component={<Components.TooltipIconButton titleId="datatable.edit" component={
<Components.TooltipIconButton
titleId="datatable.edit"
icon={<EditIcon />} icon={<EditIcon />}
color={color} color={color}
variant={variant} variant={variant}
classes={buttonClasses} classes={buttonClasses}
/>} />
}
> >
<Components.EditForm collection={collection} document={document} {...props}/> <Components.EditForm
collection={collection}
document={document}
showRemove={showRemove}
{...props}
/>
</Components.ModalTrigger> </Components.ModalTrigger>
); );
EditButton.propTypes = { EditButton.propTypes = {
collection: PropTypes.object.isRequired, collection: PropTypes.object.isRequired,
document: PropTypes.object.isRequired, document: PropTypes.object.isRequired,
@ -36,27 +45,32 @@ EditButton.propTypes = {
variant: PropTypes.string, variant: PropTypes.string,
triggerClasses: PropTypes.object, triggerClasses: PropTypes.object,
buttonClasses: PropTypes.object, buttonClasses: PropTypes.object,
showRemove: PropTypes.bool
}; };
EditButton.contextTypes = { EditButton.contextTypes = {
intl: intlShape intl: intlShape
}; };
EditButton.displayName = 'EditButton'; EditButton.displayName = 'EditButton';
registerComponent('EditButton', EditButton); registerComponent('EditButton', EditButton);
/* /*
EditForm Component EditForm Component
*/ */
const EditForm = ({ collection, document, closeModal, options, successCallback, removeSuccessCallback,...props }) => { const EditForm = ({
collection,
document,
closeModal,
options,
successCallback,
removeSuccessCallback,
showRemove,
...props
}) => {
const success = successCallback const success = successCallback
? () => { ? () => {
successCallback(); successCallback();
@ -76,11 +90,11 @@ const EditForm = ({ collection, document, closeModal, options, successCallback,
{...props} {...props}
collection={collection} collection={collection}
documentId={document && document._id} documentId={document && document._id}
showRemove={true} showRemove={showRemove ? true : showRemove}
successCallback={success} successCallback={success}
removeSuccessCallback={remove} removeSuccessCallback={remove}
/> />
); );
} };
registerComponent('EditForm', EditForm); registerComponent('EditForm', EditForm);

View file

@ -1,12 +1,15 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n'; import { intlShape } from 'meteor/vulcan:i18n';
import { registerComponent } from 'meteor/vulcan:core'; import { registerComponent, Components } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles'; import withStyles from '@material-ui/core/styles/withStyles';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
import Close from 'mdi-material-ui/Close';
import classNames from 'classnames'; import classNames from 'classnames';
@ -17,25 +20,27 @@ const styles = theme => ({
button: {}, button: {},
anchor: {}, anchor: {},
dialog: {}, dialog: {},
dialogPaper: {}, dialogPaper: {
dialogTitle: {}, overflowY: 'visible',
},
dialogTitle: {
padding: theme.spacing.unit * 4,
},
dialogContent: { dialogContent: {
paddingTop: '4px', paddingTop: '4px',
}, },
dialogOverflow: { closeButton: {
overflowY: 'visible', position: 'absolute',
}, right: theme.spacing.unit,
top: theme.spacing.unit,
}
}); });
class ModalTrigger extends PureComponent { class ModalTrigger extends PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { modalIsOpen: false }; this.state = { modalIsOpen: false };
} }
componentDidMount() { componentDidMount() {
@ -92,24 +97,28 @@ class ModalTrigger extends PureComponent {
<span className={classNames('modal-trigger', classes.root, className)}> <span className={classNames('modal-trigger', classes.root, className)}>
{triggerComponent} {triggerComponent}
<Dialog className={classNames(dialogClassName)} <Dialog className={classNames(dialogClassName)}
open={this.state.modalIsOpen} open={this.state.modalIsOpen}
onClose={this.closeModal} onClose={this.closeModal}
fullWidth={true} fullWidth={true}
classes={{ paper: classNames(classes.dialogPaper, overflowClass) }} classes={{ paper: classes.paper }}
> >
<DialogTitle className={classes.dialogTitle}>
{title}
{ <Components.Button iconButton aria-label="Close" className={classes.closeButton} onClick={this.closeModal}>
title && <Tooltip title={intl.formatMessage({ id: 'modal.close' })}>
<Close />
</Tooltip>
</Components.Button>
<DialogTitle className={classes.dialogTitle}>{title}</DialogTitle> </DialogTitle>
}
<DialogContent className={classNames(classes.dialogContent, overflowClass)}> <DialogContent className={classes.dialogContent}>
<Components.ErrorCatcher>
{childrenComponent} {childrenComponent}
</Components.ErrorCatcher>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</span> </span>

View file

@ -118,7 +118,7 @@ FormComponentInner.propTypes = {
showCharsRemaining: PropTypes.bool.isRequired, showCharsRemaining: PropTypes.bool.isRequired,
charsRemaining: PropTypes.number, charsRemaining: PropTypes.number,
charsCount: PropTypes.number, charsCount: PropTypes.number,
max: PropTypes.number, max: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
formInput: PropTypes.func.isRequired, formInput: PropTypes.func.isRequired,
}; };

View file

@ -1,205 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent, instantiateComponent, } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import withStyles from '@material-ui/core/styles/withStyles';
import Collapse from '@material-ui/core/Collapse';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px'
},
paper: {
padding: theme.spacing.unit * 3
},
subtitle1: {
display: 'flex',
alignItems: 'center',
paddingLeft: theme.spacing.unit / 2,
marginTop: theme.spacing.unit * 5,
marginBottom: theme.spacing.unit,
color: theme.palette.primary[500],
},
collapsible: {
cursor: 'pointer',
},
label: {},
toggle: {
'& svg': {
width: 21,
height: 21,
display: 'block',
}
},
container: {
paddingLeft: 4,
paddingRight: 4,
marginLeft: -4,
marginRight: -4,
},
entered: {
overflow: 'visible',
},
});
class FormGroup extends PureComponent {
constructor (props) {
super(props);
this.isAdmin = props.name === 'admin';
this.state = {
collapsed: props.startCollapsed || this.isAdmin,
};
}
toggle = () => {
const collapsible = this.props.collapsible || this.isAdmin;
if (!collapsible) return;
this.setState({
collapsed: !this.state.collapsed
});
};
renderHeading = () => {
const { classes, label } = this.props;
const collapsible = this.props.collapsible || this.isAdmin;
return (
<Typography className={classNames(classes.subtitle1, collapsible && classes.collapsible)}
variant="subtitle1"
onClick={this.toggle}
>
<div className={classes.label}>
{label}
</div>
{
collapsible &&
<div className={classes.toggle}>
{
this.state.collapsed
?
<ExpandMoreIcon/>
:
<ExpandLessIcon/>
}
</div>
}
</Typography>
);
};
// if at least one of the fields in the group has an error, the group as a whole has an error
hasErrors = () => _.some(this.props.fields, field => {
return !!this.props.errors.filter(error => error.path === field.path).length;
});
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const collapseIn = !this.state.collapsed || this.hasErrors();
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{
name === 'default'
?
null
:
this.renderHeading()
}
<Collapse classes={{ container: classes.container, entered: classes.entered }} in={collapseIn}>
<Paper className={classes.paper}>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</Paper>
</Collapse>
</div>
);
}
}
FormGroup.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
order: PropTypes.number,
hidden: PropTypes.bool,
fields: PropTypes.array,
collapsible: PropTypes.bool,
startCollapsed: PropTypes.bool,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
replaceComponent('FormGroup', FormGroup, [withStyles, styles]);

View file

@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
const FormNestedArrayLayout = ({ hasErrors, label, content }) => (
<div>
<Typography component="label" variant="caption" style={{ fontSize: 16 }}>
{label}
</Typography>
<div>{content}</div>
</div>
);
FormNestedArrayLayout.propTypes = {
hasErrors: PropTypes.bool,
label: PropTypes.node,
content: PropTypes.node,
};
replaceComponent({
name: 'FormNestedArrayLayout',
component: FormNestedArrayLayout,
});

View file

@ -1,23 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { registerComponent } from 'meteor/vulcan:core'; import { replaceComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n'; // import { FormattedMessage } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles'; import withStyles from '@material-ui/core/styles/withStyles';
import Divider from '@material-ui/core/Divider'; import Divider from '@material-ui/core/Divider';
const styles = theme => ({ const styles = theme => ({
divider: { divider: {
marginLeft: -24, // marginLeft: -24,
marginRight: -24, // marginRight: -24,
marginTop: 16, marginTop: 16,
marginBottom: 23, marginBottom: 23,
}, },
}); });
const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider} />; const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider} />;
FormNestedDivider.propTypes = { FormNestedDivider.propTypes = {
@ -26,4 +22,4 @@ FormNestedDivider.propTypes = {
addItem: PropTypes.func, addItem: PropTypes.func,
}; };
registerComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]); replaceComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);

View file

@ -5,8 +5,8 @@ import { FormattedMessage } from 'meteor/vulcan:i18n';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
const FormNestedFoot = ({ label, addItem }) => ( const FormNestedFoot = ({ label, addItem }) => (
<Grid container spacin={0} justify="flex-end"> <Grid container spacing={0} justify="flex-end">
<Components.Button color="primary" variant="fab" mini onClick={addItem} className="form-nested-add"> <Components.Button color="primary" iconButton onClick={addItem}>
<Components.IconAdd/> <Components.IconAdd/>
</Components.Button> </Components.Button>
</Grid> </Grid>

View file

@ -73,11 +73,7 @@ const MuiRadioGroup = createReactClass({
options: PropTypes.array.isRequired options: PropTypes.array.isRequired
}, },
getInitialState: function () {
if (this.props.refFunction) {
this.props.refFunction(this);
}
},
getDefaultProps: function () { getDefaultProps: function () {
return { return {

View file

@ -105,6 +105,7 @@ export default {
'description', 'description',
'clearField', 'clearField',
'regEx', 'regEx',
'allowedValues',
'mustComplete', 'mustComplete',
'renderComponent', 'renderComponent',
'formInput', 'formInput',

View file

@ -7,7 +7,7 @@ import './accounts/AccountsPasswordOrService';
import './accounts/AccountsSocialButtons'; import './accounts/AccountsSocialButtons';
import './bonus/LoadMore'; import './bonus/LoadMore';
import './bonus/SearchInput'; // import './bonus/SearchInput';
import './bonus/TooltipIntl'; import './bonus/TooltipIntl';
import './bonus/TooltipIconButton'; import './bonus/TooltipIconButton';
@ -20,10 +20,11 @@ import './core/NewButton';
import './forms/FormComponentInner'; import './forms/FormComponentInner';
import './forms/FormErrors'; import './forms/FormErrors';
import './forms/FormGroup'; //import './forms/FormGroup';
import './forms/FormGroupNone'; import './forms/FormGroupNone';
import './forms/FormGroupWithLine'; import './forms/FormGroupWithLine';
import './forms/FormNested'; import './forms/FormNested';
import './forms/FormNestedArrayLayout';
import './forms/FormNestedDivider'; import './forms/FormNestedDivider';
import './forms/FormNestedFoot'; import './forms/FormNestedFoot';
import './forms/FormNestedHead'; import './forms/FormNestedHead';

View file

@ -3,10 +3,10 @@ import { addStrings } from 'meteor/vulcan:core';
addStrings('en', { addStrings('en', {
"search.search": "Search", 'search.search': 'Search',
"search.clear": "Clear search", 'search.clear': 'Clear search',
"load_more.load_more": "Load more", 'load_more.load_more': 'Load more',
"load_more.loaded_count": "Loaded {count} of {totalCount}", 'load_more.loaded_count': 'Loaded {count} of {totalCount}',
"load_more.loaded_all": "{totalCount, plural, =0 {No items} one {One item} other {# items}}", 'load_more.loaded_all': '{totalCount, plural, =0 {No items} one {One item} other {# items}}',
}); });

View file

@ -74,6 +74,7 @@ const styles = theme => {
}; };
class Layout extends React.Component { class Layout extends React.Component {
state = { state = {
isOpen: { sideNav: true } isOpen: { sideNav: true }

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core'; import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from '@material-ui/core/ListItemIcon';
@ -38,15 +38,14 @@ class SideNavigation extends React.Component {
}; };
render () { render () {
const currentUser = this.props.currentUser; const { currentUser, classes, history } = this.props;
const classes = this.props.classes;
const isOpen = this.state.isOpen; const isOpen = this.state.isOpen;
return ( return (
<div className={classes.root}> <div className={classes.root}>
<List> <List>
<ListItem button onClick={() => {this.props.history.push('/');}}> <ListItem button onClick={() => {history.push('/');}}>
<ListItemIcon> <ListItemIcon>
<HomeIcon/> <HomeIcon/>
</ListItemIcon> </ListItemIcon>
@ -69,14 +68,14 @@ class SideNavigation extends React.Component {
</ListItem> </ListItem>
<Collapse in={isOpen.admin} transitionduration="auto" unmountOnExit> <Collapse in={isOpen.admin} transitionduration="auto" unmountOnExit>
<ListItem button className={classes.nested} <ListItem button className={classes.nested}
onClick={() => {this.props.history.push('/admin');}}> onClick={() => {browserHistory.push('/admin');}}>
<ListItemIcon> <ListItemIcon>
<UsersIcon/> <UsersIcon/>
</ListItemIcon> </ListItemIcon>
<ListItemText inset primary="Users"/> <ListItemText inset primary="Users"/>
</ListItem> </ListItem>
<ListItem button className={classes.nested} <ListItem button className={classes.nested}
onClick={() => {this.props.history.push('/theme');}}> onClick={() => {browserHistory.push('/theme');}}>
<ListItemIcon> <ListItemIcon>
<ThemeIcon/> <ThemeIcon/>
</ListItemIcon> </ListItemIcon>

View file

@ -1,8 +1,7 @@
import { addStrings } from 'meteor/vulcan:core'; import { addStrings } from 'meteor/vulcan:core';
addStrings('fr', { addStrings('fr', {
'search.search': 'Recherche',
"search.search": "Recherche", 'search.clear': 'Effacer la recherche',
"search.clear": "Effacer la recherche", 'modal.close': 'Fermer',
}); });

View file

@ -1,8 +1,10 @@
import { addRoute } from 'meteor/vulcan:core'; import { addRoute } from 'meteor/vulcan:core';
//Only create route on dev mode, not production.
if (Meteor.isDevelopment) {
addRoute({ addRoute({
name: 'theme', name: 'theme',
path: '/theme', path: '/theme',
componentName: 'ThemeStyles', componentName: 'ThemeStyles',
}); });
}

View file

@ -5,7 +5,6 @@ Package.describe({
documentation: 'README.md' documentation: 'README.md'
}); });
Package.onUse(function (api) { Package.onUse(function (api) {
api.versionsFrom('METEOR@1.6'); api.versionsFrom('METEOR@1.6');

View file

@ -1,10 +1,10 @@
# erikdakoda:vulcan-material-ui 1.12.8_13 # vulcan:ui-material 1.12.8_13
Package initially created by [Erik Dakoda](https://github.com/ErikDakoda) ([`erikdakoda:vulcan-material-ui`](https://github.com/ErikDakoda/vulcan-material-ui))
Replacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/). Replacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/).
This version has been tested against Vulcan 1.12.8 and Material-UI 3.1.0.
To give me feedback open an issue on GitHub or you can reach me on the [Vulcan Slack](https://vulcanjs.slack.com)
channel as erikdakoda.
There are some nice bonus features like a CountrySelect with autocomplete and theming. There are some nice bonus features like a CountrySelect with autocomplete and theming.
@ -15,9 +15,9 @@ All components in vulcan:ui-bootstrap, vulcan:forms and vulcan:accounts have bee
To add vulcan-material-ui to an existing Vulcan project, enter the following: To add vulcan-material-ui to an existing Vulcan project, enter the following:
``` sh ``` sh
meteor add erikdakoda:vulcan-material-ui meteor add vulcan:ui-material
meteor npm install --save @material-ui/core meteor npm install --save @material-ui/core@3.1.0
meteor npm install --save react-jss meteor npm install --save react-jss
meteor npm install --save mdi-material-ui meteor npm install --save mdi-material-ui
meteor npm install --save react-autosuggest meteor npm install --save react-autosuggest
@ -48,7 +48,7 @@ For an example theme see `modules/sampleTheme.js`. For a complete list of values
see the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/). see the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/).
Register your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`. Register your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`.
You can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` public setting. You can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` **public** setting.
In addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles. In addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles.
For example the sample theme contains For example the sample theme contains