change(ui): sessions settings moved to preferences (#1380)

* change(ui): sessions settings

* dev merge
This commit is contained in:
Shekar Siri 2023-06-28 13:26:11 +02:00 committed by GitHub
parent d3a411f852
commit ee1a775379
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 363 additions and 374 deletions

View file

@ -15,6 +15,7 @@ import cn from 'classnames';
import PreferencesMenu from './PreferencesMenu';
import Notifications from './Notifications';
import Roles from './Roles';
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
@withRouter
export default class Client extends React.PureComponent {
@ -29,6 +30,7 @@ export default class Client extends React.PureComponent {
renderActiveTab = () => (
<Switch>
<Route exact strict path={clientRoute(CLIENT_TABS.PROFILE)} component={ProfileSettings} />
<Route exact strict path={clientRoute(CLIENT_TABS.SESSIONS_LISTING)} component={SessionsListingSettings} />
<Route exact strict path={clientRoute(CLIENT_TABS.INTEGRATIONS)} component={Integrations} />
<Route exact strict path={clientRoute(CLIENT_TABS.MANAGE_USERS)} component={UserView} />
<Route exact strict path={clientRoute(CLIENT_TABS.SITES)} component={Sites} />

View file

@ -1,132 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem } from 'UI';
import stl from './preferencesMenu.module.css';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
import { withRouter } from 'react-router-dom';
function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
const isAdmin = account.admin || account.superAdmin;
const setTab = (tab) => {
history.push(clientRoute(tab));
};
return (
<div className={cn(stl.wrapper, 'h-full overflow-y-auto pb-24')}>
<div className={cn(stl.header, 'flex items-end')}>
<div className={stl.label}>
<span>Preferences</span>
</div>
</div>
<div className="mb-2">
<SideMenuitem
active={activeTab === CLIENT_TABS.PROFILE}
title="Account"
iconName="user-circle"
onClick={() => setTab(CLIENT_TABS.PROFILE)}
/>
</div>
<div className="mb-2">
<SideMenuitem
active={activeTab === CLIENT_TABS.INTEGRATIONS}
title="Integrations"
iconName="puzzle-piece"
onClick={() => setTab(CLIENT_TABS.INTEGRATIONS)}
/>
</div>
<div className="mb-2">
<SideMenuitem
iconName="tags"
active={activeTab === CLIENT_TABS.CUSTOM_FIELDS}
onClick={() => setTab(CLIENT_TABS.CUSTOM_FIELDS)}
title="Metadata"
/>
</div>
{
<div className="mb-2">
<SideMenuitem
active={activeTab === CLIENT_TABS.WEBHOOKS}
title="Webhooks"
iconName="anchor"
onClick={() => setTab(CLIENT_TABS.WEBHOOKS)}
/>
</div>
}
<div className="mb-2">
<SideMenuitem
active={activeTab === CLIENT_TABS.SITES}
title="Projects"
iconName="window-restore"
onClick={() => setTab(CLIENT_TABS.SITES)}
/>
</div>
{isEnterprise && isAdmin && (
<div className="mb-2 relative">
<SideMenuitem
active={activeTab === CLIENT_TABS.MANAGE_ROLES}
title="Roles & Access"
iconName="diagram-3"
onClick={() => setTab(CLIENT_TABS.MANAGE_ROLES)}
leading={<AdminOnlyBadge />}
/>
</div>
)}
{isEnterprise && isAdmin && (
<div className="mb-2 relative">
<SideMenuitem
active={activeTab === CLIENT_TABS.AUDIT}
title="Audit"
iconName="list-ul"
onClick={() => setTab(CLIENT_TABS.AUDIT)}
leading={<AdminOnlyBadge />}
/>
</div>
)}
{isAdmin && (
<div className="mb-2 relative">
<SideMenuitem
active={activeTab === CLIENT_TABS.MANAGE_USERS}
title="Team"
iconName="users"
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS)}
leading={<AdminOnlyBadge />}
/>
</div>
)}
<div className="mb-2">
<SideMenuitem
active={activeTab === CLIENT_TABS.NOTIFICATIONS}
title="Notifications"
iconName="bell"
onClick={() => setTab(CLIENT_TABS.NOTIFICATIONS)}
/>
</div>
</div>
);
}
export default connect((state) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account']),
}))(withRouter(PreferencesMenu));
function AdminOnlyBadge() {
return (
<div
className="ml-1 rounded-full bg-gray-light text-xs flex items-center px-2 color-gray-medium"
style={{ marginTop: '', height: '20px', whiteSpace: 'nowrap' }}
>
Admin Only
</div>
);
}

View file

@ -0,0 +1,100 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import cn from 'classnames';
import { SideMenuitem } from 'UI';
import stl from './preferencesMenu.module.css';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
const mapStateToProps = (state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account'])
});
const connector = connect(mapStateToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;
type Props = PropsFromRedux & RouteComponentProps & {
activeTab: string;
};
function PreferencesMenu({ account, activeTab, history, isEnterprise }: Props) {
const isAdmin = account.admin || account.superAdmin;
const setTab = (tab: string) => {
history.push(clientRoute(tab));
};
const AdminOnlyBadge = () => (
<div
className='ml-1 rounded-full bg-gray-light text-xs flex items-center px-2 color-gray-medium'
style={{ marginTop: '', height: '20px', whiteSpace: 'nowrap' }}
>
Admin Only
</div>
);
const menuItems = React.useMemo(() => {
return [
{ title: 'Account', iconName: 'user-circle', tab: CLIENT_TABS.PROFILE },
{ title: 'Sessions Listing', iconName: 'play', tab: CLIENT_TABS.SESSIONS_LISTING },
{ title: 'Integrations', iconName: 'puzzle-piece', tab: CLIENT_TABS.INTEGRATIONS },
{ title: 'Metadata', iconName: 'tags', tab: CLIENT_TABS.CUSTOM_FIELDS },
{ title: 'Webhooks', iconName: 'anchor', tab: CLIENT_TABS.WEBHOOKS },
{ title: 'Projects', iconName: 'window-restore', tab: CLIENT_TABS.SITES },
{
title: 'Roles & Access',
iconName: 'diagram-3',
tab: CLIENT_TABS.MANAGE_ROLES,
isAdminOnly: true,
isEnterpriseOnly: true
},
{
title: 'Audit',
iconName: 'list-ul',
tab: CLIENT_TABS.AUDIT,
isAdminOnly: true,
isEnterpriseOnly: true
},
{ title: 'Team', iconName: 'users', tab: CLIENT_TABS.MANAGE_USERS, isAdminOnly: true },
{ title: 'Notifications', iconName: 'bell', tab: CLIENT_TABS.NOTIFICATIONS }
].reduce((acc, item) => {
if (item.isAdminOnly && !isAdmin) {
return acc;
}
if (item.isEnterpriseOnly && !isEnterprise) {
return acc;
}
return [...acc, item];
}, []);
}, [isAdmin, isEnterprise]);
// @ts-ignore
return (
<div className={cn(stl.wrapper, 'h-full overflow-y-auto pb-24')}>
<div className={cn(stl.header, 'flex items-end')}>
<div className={stl.label}>
<span>Preferences</span>
</div>
</div>
{menuItems.map((menuItem) => (
<div className='mb-2' key={menuItem.title}>
<SideMenuitem
active={activeTab === menuItem.tab}
title={menuItem.title}
// @ts-ignore
iconName={menuItem.iconName}
onClick={() => setTab(menuItem.tab)}
leading={menuItem.isAdminOnly ? <AdminOnlyBadge /> : undefined}
/>
</div>
))}
</div>
);
}
export default connector(withRouter(PreferencesMenu));

View file

@ -0,0 +1,55 @@
import React from 'react';
import { connect } from 'react-redux';
import { PageTitle, Divider } from 'UI';
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying';
import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone';
import withPageTitle from 'HOCs/withPageTitle';
import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings';
type Props = {}
const mapStateToProps = (state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account'])
});
const connector = connect(mapStateToProps);
function SessionsListingSettings(props: Props) {
return (
<div className='p-5'>
<PageTitle title={<div>Sessions Listings</div>} />
<div className='flex flex-col mt-4'>
<div className='max-w-lg'>
<ListingVisibility />
</div>
<Divider />
<div>
<DefaultPlaying />
</div>
<Divider />
<div>
<DefaultTimezone />
</div>
<Divider />
<div>
<MouseTrailSettings />
</div>
</div>
</div>
);
}
export default connector(
withPageTitle('Sessions Listings - OpenReplay Preferences')(SessionsListingSettings)
);

View file

@ -1,32 +1,31 @@
import React from 'react';
import { Toggler } from 'UI';
import { Switch } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { toast } from 'react-toastify';
function DefaultPlaying(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
function DefaultPlaying() {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
const toggleSkipToIssue = () => {
sessionSettings.updateKey('skipToIssue', !sessionSettings.skipToIssue)
toast.success("Default playing option saved successfully");
}
const toggleSkipToIssue = () => {
sessionSettings.updateKey('skipToIssue', !sessionSettings.skipToIssue);
toast.success('Default playing option saved successfully');
};
return useObserver(() => (
<>
<h3 className="text-lg">Default Playing Option</h3>
<div className="my-1">Always start playing the session from the first issue</div>
<div className="mt-2">
<Toggler
checked={sessionSettings.skipToIssue}
name="test"
onChange={toggleSkipToIssue}
/>
</div>
</>
));
return useObserver(() => (
<>
<h3 className='text-lg'>Default Playing Option</h3>
<div className='my-1'>Always start playing the session from the first issue</div>
<div className='mt-2'>
<Switch
checked={sessionSettings.skipToIssue}
onChange={toggleSkipToIssue}
/>
</div>
</>
));
}
export default DefaultPlaying;

View file

@ -0,0 +1,26 @@
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { Switch } from 'UI';
function MouseTrailSettings() {
const { settingsStore } = useStore();
const sessionSettings = settingsStore.sessionSettings;
const mouseTrail = sessionSettings.mouseTrail;
const updateSettings = (checked: boolean) => {
settingsStore.sessionSettings.updateKey('mouseTrail', !mouseTrail);
};
return (
<>
<h3 className='text-lg'>Mouse Trail</h3>
<div className='my-1'>See mouse trail to easily spot user activity.</div>
<div className='mt-2'>
<Switch onChange={updateSettings} checked={mouseTrail} />
</div>
</>
);
}
export default observer(MouseTrailSettings);

View file

@ -0,0 +1,13 @@
import React from 'react';
import { Divider as AntdDivider, DividerProps as AntdDividerProps } from 'antd';
interface DividerProps extends AntdDividerProps {
customProp?: boolean;
}
const Divider: React.FC<DividerProps> = ({ customProp, ...restProps }) => {
return <AntdDivider {...restProps} />;
};
export default Divider;

View file

@ -0,0 +1,81 @@
import React from 'react';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import stl from './sideMenuItem.module.css';
import { IconNames } from 'UI/SVG';
type Props = {
title: string;
iconName?: IconNames;
iconBg?: boolean;
iconColor?: string;
iconSize?: number;
className?: string;
active?: boolean;
disabled?: boolean;
tooltipTitle?: string;
onClick?: () => void;
deleteHandler?: () => void;
leading?: React.ReactNode;
id?: string;
};
function SideMenuItem({
iconBg = false,
iconColor = 'gray-dark',
iconSize = 18,
className = '',
iconName,
title,
active = false,
disabled = false,
tooltipTitle = '',
onClick,
deleteHandler,
leading = null,
...props
}: Props) {
const handleClick = () => {
if (disabled) return;
if (onClick) onClick();
};
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (deleteHandler) deleteHandler();
};
return (
<Tooltip disabled={!disabled} title={tooltipTitle} placement='top'>
<div
className={cn(
className,
stl.menuItem,
'flex items-center py-2 justify-between shrink-0',
{ [stl.active]: active, [stl.disabled]: disabled }
)}
onClick={handleClick}
{...props}
>
<div className={cn('flex items-center w-full', stl.iconLabel)}>
{iconName && (
<div className='flex items-center justify-center w-8 h-8 mr-2'>
{iconBg &&
<div className={cn('w-8 h-8 rounded-full relative opacity-20', iconBg)} style={{ opacity: 0.2 }} />}
<Icon name={iconName} size={iconSize} color={active ? 'teal' : iconColor} className='absolute' />
</div>
)}
<span className={cn(stl.title, 'capitalize-first')}>{title}</span>
</div>
{leading && leading}
{deleteHandler && (
<div onClick={handleDeleteClick} className={stl.actions}>
<Icon name='trash' size={14} />
</div>
)}
</div>
</Tooltip>
);
}
export default SideMenuItem;

View file

@ -0,0 +1 @@
export { default } from './SideMenuItem';

View file

@ -1,72 +0,0 @@
import React from 'react';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import stl from './sideMenuItem.module.css';
import { IconNames } from 'UI/SVG';
function SideMenuitem({
iconBg = false,
iconColor = "gray-dark",
iconSize = 18,
className = '',
iconName,
title,
active = false,
disabled = false,
tooltipTitle = '',
onClick,
deleteHandler,
leading = null,
...props
}: {
title: string;
iconName?: IconNames;
iconBg?: boolean;
iconColor?: string;
iconSize?: number;
className?: string;
active?: boolean;
disabled?: boolean;
tooltipTitle?: string;
onClick?: () => void;
deleteHandler?: () => void;
leading?: React.ReactNode;
id?: string;
}) {
return (
<Tooltip
disabled={ !disabled }
title={ tooltipTitle }
placement="top"
>
<div
className={ cn(
className,
stl.menuItem,
"flex items-center py-2 justify-between shrink-0",
{ [stl.active] : active }
)}
onClick={disabled ? null : onClick}
{...props}
>
<div className={ cn('flex items-center w-full', { [stl.disabled] : disabled })}>
<div className={cn("flex items-center", stl.iconLabel)}>
{ iconName && (
<div className="flex items-center justify-center w-8 h-8 mr-2">
<div className={cn({ "w-8 h-8 rounded-full relative opacity-20" : iconBg }, iconBg)} style={{ opacity: '0.2'}} />
<Icon name={ iconName } size={ iconSize } color={active ? 'teal' : iconColor} className="absolute" />
</div>
)}
<span className={cn(stl.title, 'capitalize-first')}>{ title }</span>
</div>
{ leading && leading }
</div>
{deleteHandler &&
<div onClick={deleteHandler} className={stl.actions}><Icon name="trash" size="14" /></div>
}
</div>
</Tooltip>
)
}
export default SideMenuitem

View file

@ -1 +0,0 @@
export { default } from './SideMenuitem';

View file

@ -0,0 +1,13 @@
import React from 'react';
import { Switch as AntdSwitch, SwitchProps as AntdSwitchProps } from 'antd';
interface SwitchProps extends AntdSwitchProps {
customProp?: boolean;
// Add any additional custom props here
}
const Switch: React.FC<SwitchProps> = ({ customProp, ...restProps }) => {
return <AntdSwitch {...restProps} />;
};
export default Switch;

View file

@ -31,7 +31,7 @@ export { default as CountryFlag } from './CountryFlag';
export { default as RandomElement } from './RandomElement';
export { default as SplitButton } from './SplitButton';
export { default as confirm } from './Confirmation';
export { default as SideMenuitem } from './SideMenuitem';
export { default as SideMenuitem } from './SideMenuItem';
export { default as Avatar } from './Avatar';
export { default as TimezoneDropdown } from './TimezoneDropdown';
export { default as ErrorItem } from './ErrorItem';
@ -57,3 +57,5 @@ export { default as Form } from './Form';
export { default as Modal } from './Modal';
export { default as Message } from './Message';
export { default as Popover } from './Popover';
export { default as Switch } from './Switch';
export { default as Divider } from './Divider';

View file

@ -1,5 +1,5 @@
import { storiesOf } from '@storybook/react';
import SideMenuitem from './SideMenuitem';
import SideMenuItem from './SideMenuItem';
import { Avatar, ErrorItem, ErrorFrame, ErrorDetails, TimelinePointer } from 'UI';
import Error from 'Types/session/error';
import ErrorStackModel from 'Types/session/errorStack';
@ -65,10 +65,10 @@ const errors = [
storiesOf('UI Components', module)
.add('SideMenuItem', () => (
<SideMenuitem title="Menu Label" />
<SideMenuItem title="Menu Label" />
))
.add('SideMenuItem active', () => (
<SideMenuitem title="Menu Label" active />
<SideMenuItem title="Menu Label" active />
))
.add('Avatar', () => (
<Avatar />

View file

@ -6,3 +6,4 @@ export const GLOBAL_DESTINATION_PATH = "__$global-destinationPath$__"
export const GLOBAL_HAS_NO_RECORDINGS = "__$global-hasNoRecordings$__"
export const SITE_ID_STORAGE_KEY = "__$user-siteId$__"
export const GETTING_STARTED = "__$user-gettingStarted$__"
export const MOUSE_TRAIL = "__$session-mouseTrail$__"

View file

@ -9,6 +9,7 @@ import { StoreProvider, RootStore } from './mstore';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
// @ts-ignore
window.getCommitHash = () => console.log(window.env.COMMIT_HASH)

View file

@ -1,6 +1,6 @@
import { makeAutoObservable, runInAction } from 'mobx';
import moment from 'moment';
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys';
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER, MOUSE_TRAIL } from 'App/constants/storageKeys';
export type Timezone = {
label: string;
@ -69,6 +69,8 @@ export default class SessionSettings {
durationFilter: any = JSON.parse(localStorage.getItem(DURATION_FILTER) || JSON.stringify(defaultDurationFilter));
captureRate: string = '0';
captureAll: boolean = false;
mouseTrail: boolean = localStorage.getItem(MOUSE_TRAIL) === 'true';
constructor() {
// compatibility fix for old timezone storage

View file

@ -59,6 +59,7 @@ export const forgotPassword = () => '/reset-password';
export const CLIENT_TABS = {
INTEGRATIONS: 'integrations',
PROFILE: 'account',
SESSIONS_LISTING: 'sessions-listing',
MANAGE_USERS: 'team',
MANAGE_ROLES: 'roles',
SITES: 'projects',

View file

@ -22,3 +22,7 @@ input.no-focus:focus {
@apply inline-block;
}
}
.border-b {
border-bottom: 1px solid #eee;
}

View file

@ -6,6 +6,7 @@
@import 'react-tippy/dist/tippy.css';
@import 'react-daterange-picker.css';
@import 'rc-time-picker.css';
@import 'antd/dist/reset.css';
@tailwind base;
@tailwind components;

View file

@ -1,155 +1,47 @@
const colors = require('./app/theme/colors');
const defaultColors = require('tailwindcss/colors')
const defaultColors = require('tailwindcss/colors');
console.log(defaultColors);
module.exports = {
// important: true,
content: [
'./app/**/*.tsx',
'./app/**/*.js',
],
// corePlugins: [
// 'preflight',
// 'container',
// // 'accessibility',
// // 'alignContent',
// 'alignItems',
// 'alignSelf',
// //'appearance',
// // 'backgroundAttachment',
// 'backgroundColor',
// 'backgroundOpacity',
// // 'backgroundPosition',
// // 'backgroundRepeat',
// // 'backgroundSize',
// // 'borderCollapse',
// 'borderColor',
// 'borderOpacity',
// 'borderRadius',
// // 'borderStyle',
// 'borderWidth',
// // 'boxSizing',
// // 'boxShadow',
// // 'clear',
// 'cursor',
// 'display',
// // 'divideColor',
// // 'divideWidth',
// // 'fill',
// 'flex',
// 'flexDirection',
// // 'flexGrow',
// 'flexShrink',
// 'flexWrap',
// // 'float',
// 'gap',
// 'gridAutoFlow',
// 'gridColumn',
// 'gridColumnStart',
// 'gridColumnEnd',
// 'gridRow',
// 'gridRowStart',
// 'gridRowEnd',
// 'gridTemplateColumns',
// 'gridTemplateRows',
// 'fontFamily',
// 'fontSize',
// //'fontSmoothing',
// 'fontStyle',
// 'fontWeight',
// 'height',
// 'inset',
// 'justifyContent',
// 'justifySelf',
// 'letterSpacing',
// 'lineHeight',
// // 'listStylePosition',
// // 'listStyleType',
// 'margin',
// // 'maxHeight',
// // 'maxWidth',
// // 'minHeight',
// // 'minWidth',
// // 'objectFit',
// // 'objectPosition',
// 'opacity',
// // 'order',
// 'outline',
// 'overflow',
// 'padding',
// // 'placeholderColor',
// // 'placeholderOpacity',
// 'pointerEvents',
// 'position',
// // 'resize',
// // 'rotate',
// // 'scale',
// // 'skew',
// // 'space',
// // 'stroke',
// // 'strokeWidth',
// // 'tableLayout',
// 'textAlign',
// // 'textColor',
// // 'textOpacity',
// 'textDecoration',
// 'textTransform',
// // 'transform',
// // 'transitionDuration',
// // 'transitionProperty',
// // 'transitionTimingFunction',
// // 'translate',
// 'userSelect',
// // 'verticalAlign',
// 'visibility',
// 'whitespace',
// 'width',
// 'wordBreak',
// 'zIndex'
// ],
theme: {
colors: {
...defaultColors,
...colors,
},
// borderColor: {
// default: '#DDDDDD',
// "gray-light-shade": colors["gray-light-shade"],
// "blue": colors["active-blue-border"],
// },
extend: {
boxShadow: {
'border-blue': `0 0 0 1px ${colors['active-blue-border']}`,
'border-main': `0 0 0 1px ${colors['main']}`,
'border-gray': '0 0 0 1px #999',
},
keyframes: {
'fade-in': {
'0%': {
opacity: '0',
// transform: 'translateY(-10px)'
},
'100%': {
opacity: '1',
// transform: 'translateY(0)'
},
content: [
'./app/**/*.tsx',
'./app/**/*.js'
],
theme: {
colors: {
...defaultColors,
...colors
},
extend: {
keyframes: {
'fade-in': {
'0%': {
opacity: '0'
// transform: 'translateY(-10px)'
},
'100%': {
opacity: '1'
// transform: 'translateY(0)'
}
}
},
animation: {
'fade-in': 'fade-in 0.2s ease-out'
'fade-in': 'fade-in 0.2s ease-out'
},
colors: {
'disabled-text': 'rgba(0,0,0, 0.38)',
'disabled-text': 'rgba(0,0,0, 0.38)'
},
boxShadow: {
'border-blue': `0 0 0 1px ${colors['active-blue-border']}`,
'border-main': `0 0 0 1px ${colors['main']}`,
'border-gray': '0 0 0 1px #999',
}
},
'border-gray': '0 0 0 1px #999'
},
}
},
variants: {
visibility: ['responsive', 'hover', 'focus', 'group-hover']
},
plugins: [],
}
visibility: ['responsive', 'hover', 'focus', 'group-hover']
},
plugins: [],
corePlugins: {
preflight: false
}
};