Render plugin list

Summary:
This diff adds the rough navigation to open pugins, there are some rough egdes still, and tests will be added later, but wanted to keep diffs small, and land the feature early to get some initial dogfooding going on early.

Note that we now also show all disabled plugins to help people with trouble shooting.

Reviewed By: nikoant

Differential Revision: D24418411

fbshipit-source-id: 1402d69efe2e52bc2c81336cfb4f4c9928ea4d80
This commit is contained in:
Michel Weststrate
2020-10-22 09:37:26 -07:00
committed by Facebook GitHub Bot
parent 2c6c7fb46c
commit 8a7323b9f8
7 changed files with 596 additions and 268 deletions

View File

@@ -126,7 +126,7 @@ export abstract class FlipperBasePlugin<
static gatekeeper: string | null = null;
static entry: string | null = null;
static isDefault: boolean;
static details: PluginDetails | undefined;
static details: PluginDetails;
static keyboardActions: KeyboardActions | null;
static screenshot: string | null;
static defaultPersistedState: any;

View File

@@ -34,6 +34,7 @@ import createPaste from '../fb-stubs/createPaste';
import {ReactNode} from 'react';
import React from 'react';
import {KeyboardActions} from '../MenuBar';
import {PluginDetails} from 'flipper-plugin-lib';
type ID = string;
@@ -255,6 +256,21 @@ export default function createTableNativePlugin(id: string, title: string) {
static id = id || '';
static title = title || '';
static details: PluginDetails = {
id,
title,
icon: 'apps',
name: id,
// all hmm...
specVersion: 1,
version: 'auto',
dir: '',
source: '',
main: '',
entry: '',
isDefault: false,
};
static defaultPersistedState: PersistedState = {
rows: [],
datas: {},

View File

@@ -7,27 +7,17 @@
* @format
*/
import React, {useEffect, useRef, useState} from 'react';
import {Alert, Badge, Button, Input, Menu, Tooltip, Typography} from 'antd';
import React from 'react';
import {Alert, Button, Input} from 'antd';
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
import {
SettingOutlined,
RocketOutlined,
MailOutlined,
AppstoreOutlined,
} from '@ant-design/icons';
import {Glyph, Layout, Link, styled} from '../../ui';
import {SettingOutlined, RocketOutlined} from '@ant-design/icons';
import {Layout, Link} from '../../ui';
import {theme} from '../theme';
import {useStore as useReduxStore} from 'react-redux';
import {showEmulatorLauncher} from './LaunchEmulator';
import {AppSelector} from './AppSelector';
import {useDispatch, useStore} from '../../utils/useStore';
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
import {PluginDefinition} from '../../plugin';
import {selectPlugin} from '../../reducers/connections';
const {SubMenu} = Menu;
const {Text} = Typography;
import {useStore} from '../../utils/useStore';
import {PluginList} from './PluginList';
const appTooltip = (
<>
@@ -45,7 +35,7 @@ export function AppInspect() {
const selectedDevice = useStore((state) => state.connections.selectedDevice);
return (
<LeftSidebar>
<Layout.Top scrollable>
<Layout.Top>
<Layout.Container borderBottom>
<SidebarTitle actions={<InfoIcon>{appTooltip}</InfoIcon>}>
App Inspect
@@ -68,226 +58,15 @@ export function AppInspect() {
</Layout.Vertical>
</Layout.Container>
<Layout.Container padv={theme.space.large}>
{selectedDevice ? (
<PluginList />
) : (
<Alert message="No device or app selected" type="info" />
)}
<Layout.ScrollContainer vertical>
{selectedDevice ? (
<PluginList />
) : (
<Alert message="No device or app selected" type="info" />
)}
</Layout.ScrollContainer>
</Layout.Container>
</Layout.Top>
</LeftSidebar>
);
}
function PluginList() {
// const {selectedApp, selectedDevice} = useStore((state) => state.connections);
return (
<Layout.Container>
<SidebarTitle>Plugins</SidebarTitle>
<Layout.Container padv={theme.space.small} padh={theme.space.tiny}>
<PluginMenu
inlineIndent={8}
onClick={() => {}}
defaultOpenKeys={['device']}
mode="inline">
<DevicePlugins key="device" />
<SubMenu
key="sub1"
title={
<Layout.Right center>
<Text strong>Header</Text>
<Badge count={8}></Badge>
</Layout.Right>
}>
<Menu.Item key="1" icon={<AppstoreOutlined />}>
Option 1
</Menu.Item>
<Menu.Item key="2" icon={<AppstoreOutlined />}>
Option 2
</Menu.Item>
<Menu.Item key="3">Option 3</Menu.Item>
<Menu.Item key="4">Option 4</Menu.Item>
</SubMenu>
<SubMenu
key="sub2"
icon={<AppstoreOutlined />}
title="Navigation Two">
<Menu.Item key="5">Option 5</Menu.Item>
<Menu.Item key="6">Option 6</Menu.Item>
<Menu.Item key="7">Option 7</Menu.Item>
<Menu.Item key="8">Option 8</Menu.Item>
</SubMenu>
<SubMenu
key="sub4"
title={
<span>
<SettingOutlined />
<span>Navigation Three</span>
</span>
}>
<Menu.Item key="9">Option 9</Menu.Item>
<Menu.Item key="10">Option 10</Menu.Item>
<Menu.Item key="11">Option 11</Menu.Item>
<Menu.Item key="12">Option 12</Menu.Item>
</SubMenu>
</PluginMenu>
</Layout.Container>
</Layout.Container>
);
}
function DevicePlugins(props: any) {
const dispatch = useDispatch();
const {selectedDevice, selectedPlugin} = useStore(
(state) => state.connections,
);
const devicePlugins = useStore((state) => state.plugins.devicePlugins);
if (selectedDevice?.devicePlugins.length === 0) {
return null;
}
return (
<SubMenu {...props} title={<Text strong>Device</Text>}>
{selectedDevice!.devicePlugins
.map((pluginName) => devicePlugins.get(pluginName)!)
.sort(sortPluginsByName)
.map((plugin) => (
<Plugin
key={plugin.id}
isActive={plugin.id === selectedPlugin}
onClick={() => {
dispatch(
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: null,
deepLinkPayload: null,
selectedDevice,
}),
);
}}
plugin={plugin}
/>
))}
</SubMenu>
);
}
const Plugin: React.FC<{
onClick: () => void;
isActive: boolean;
plugin: PluginDefinition;
app?: string | null | undefined;
helpRef?: any;
provided?: any;
onFavorite?: () => void;
starred?: boolean; // undefined means: not starrable
}> = function (props) {
const {isActive, plugin, onFavorite, starred, ...rest} = props;
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const node = domRef.current;
if (isActive && node) {
const rect = node.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
node.scrollIntoView();
}
}
}, [isActive]);
return (
<Menu.Item
{...rest}
active={isActive}
onClick={props.onClick}
disabled={starred === false}>
<Tooltip
placement="right"
title={
<>
{getPluginTitle(plugin)} ({plugin.version})
{plugin.details?.description ? (
<>
<br />
<br />
{plugin.details?.description}
</>
) : (
''
)}
</>
}
mouseEnterDelay={1}>
<Layout.Horizontal center gap={10} ref={domRef}>
<PluginIconWrapper>
<Glyph size={16} name={plugin.icon || 'apps'} color="white" />
</PluginIconWrapper>
{getPluginTitle(plugin)}
</Layout.Horizontal>
</Tooltip>
{/* {starred !== undefined && (!starred || isActive) && (
<ToggleButton
onClick={onFavorite}
toggled={starred}
tooltip="Click to enable / disable this plugin"
/>
)} */}
</Menu.Item>
);
};
const iconStyle = {
color: theme.white,
background: theme.primaryColor,
borderRadius: theme.borderRadius,
width: 24,
height: 24,
};
// TODO: move this largely to themes/base.less to make it the default?
// Dimensions are hardcoded as they correlate strongly
const PluginMenu = styled(Menu)({
border: 'none',
'.ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title ': {
width: '100%', // reset to remove weird bonus pixel from ANT
},
'.ant-menu-submenu > .ant-menu-submenu-title, .ant-menu-sub.ant-menu-inline > .ant-menu-item': {
borderRadius: theme.borderRadius,
height: '32px',
lineHeight: '24px',
padding: `4px 32px 4px 8px !important`,
'&:hover': {
color: theme.textColorPrimary,
background: theme.backgroundTransparentHover,
},
'&.ant-menu-item-selected::after': {
border: 'none',
},
'&.ant-menu-item-selected': {
color: theme.white,
background: theme.primaryColor,
border: 'none',
},
},
'.ant-menu-submenu-inline > .ant-menu-submenu-title .ant-menu-submenu-arrow': {
right: 8,
},
'.ant-badge-count': {
color: theme.textColorPrimary,
background: theme.backgroundTransparentHover,
fontWeight: 'bold',
padding: `0 10px`,
boxShadow: 'none',
},
'.ant-menu-item .anticon': {
...iconStyle,
lineHeight: '28px', // WUT?
},
});
const PluginIconWrapper = styled.div({
...iconStyle,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
});

View File

@@ -0,0 +1,477 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
import {Glyph, Layout, styled} from '../../ui';
import {theme} from '../theme';
import {useDispatch, useStore} from '../../utils/useStore';
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin';
import {selectPlugin, starPlugin} from '../../reducers/connections';
import Client from '../../Client';
import {State} from '../../reducers';
import BaseDevice from '../../devices/BaseDevice';
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
import {PluginDetails} from 'flipper-plugin-lib';
const {SubMenu} = Menu;
const {Text} = Typography;
export const PluginList = memo(function PluginList() {
const dispatch = useDispatch();
const connections = useStore((state) => state.connections);
const plugins = useStore((state) => state.plugins);
const metroDevice = useMemo<BaseDevice | undefined>(
() =>
connections?.devices?.find(
(device) => device.os === 'Metro' && !device.isArchived,
),
[connections.devices],
);
const client = useMemo(
() => connections.clients.find((c) => c.id === connections.selectedApp),
[connections.clients, connections.selectedApp],
);
const {
devicePlugins,
metroPlugins,
enabledPlugins,
disabledPlugins,
unavailablePlugins,
} = useMemo(
() =>
computePluginLists(
connections.selectedDevice!,
metroDevice,
client,
plugins,
connections.userStarredPlugins,
),
[
connections.selectedDevice,
metroDevice,
plugins,
client,
connections.userStarredPlugins,
],
);
const handleAppPluginClick = useCallback(
(pluginId) => {
dispatch(
selectPlugin({
selectedPlugin: pluginId,
selectedApp: connections.selectedApp,
deepLinkPayload: null,
selectedDevice: connections.selectedDevice,
}),
);
},
[dispatch, connections.selectedDevice, connections.selectedApp],
);
const handleMetroPluginClick = useCallback(
(pluginId) => {
dispatch(
selectPlugin({
selectedPlugin: pluginId,
selectedApp: connections.selectedApp,
deepLinkPayload: null,
selectedDevice: metroDevice,
}),
);
},
[dispatch, metroDevice, connections.selectedApp],
);
const handleStarPlugin = useCallback(
(id: string) => {
dispatch(
starPlugin({
selectedApp: client!.query.app,
plugin: plugins.clientPlugins.get(id)!,
}),
);
},
[client, plugins.clientPlugins, dispatch],
);
return (
<Layout.Container>
<SidebarTitle>Plugins</SidebarTitle>
<Layout.Container padv={theme.space.small} padh={theme.space.tiny}>
<PluginMenu
inlineIndent={8}
onClick={() => {}}
defaultOpenKeys={['device', 'enabled', 'metro']}
mode="inline">
<PluginGroup key="device" title="Device">
{devicePlugins.map((plugin) => (
<PluginEntry
key={plugin.id}
plugin={plugin.details}
active={plugin.id === connections.selectedPlugin}
onClick={handleAppPluginClick}
tooltip={getPluginTooltip(plugin.details)}
/>
))}
</PluginGroup>
<PluginGroup key="metro" title="React Native">
{metroPlugins.map((plugin) => (
<PluginEntry
key={plugin.id}
plugin={plugin.details}
active={plugin.id === connections.selectedPlugin}
onClick={handleMetroPluginClick}
tooltip={getPluginTooltip(plugin.details)}
/>
))}
</PluginGroup>
<PluginGroup key="enabled" title="Enabled">
{enabledPlugins.map((plugin) => (
<PluginEntry
key={plugin.id}
plugin={plugin.details}
active={plugin.id === connections.selectedPlugin}
onClick={handleAppPluginClick}
tooltip={getPluginTooltip(plugin.details)}
actions={
<ActionButton
id={plugin.id}
onClick={handleStarPlugin}
icon={<MinusOutlined size={16} style={{marginRight: 0}} />}
/>
}
/>
))}
</PluginGroup>
<PluginGroup key="disabled" title="Disabled">
{disabledPlugins.map((plugin) => (
<PluginEntry
key={plugin.id}
plugin={plugin.details}
active={plugin.id === connections.selectedPlugin}
tooltip={getPluginTooltip(plugin.details)}
actions={
<ActionButton
id={plugin.id}
onClick={handleStarPlugin}
icon={<PlusOutlined size={16} style={{marginRight: 0}} />}
/>
}
disabled
/>
))}
</PluginGroup>
<PluginGroup key="unavailable" title="Unavailable plugins">
{unavailablePlugins.map(([plugin, reason]) => (
<PluginEntry
key={plugin.id}
plugin={plugin}
tooltip={`${getPluginTitle(plugin)} (${plugin.id}@${
plugin.version
}): ${reason}`}
disabled
actions={<InfoIcon>{reason}</InfoIcon>}
/>
))}
</PluginGroup>
</PluginMenu>
</Layout.Container>
</Layout.Container>
);
});
function getPluginTooltip(details: PluginDetails): string {
return `${getPluginTitle(details)} (${details.id}@${details.version}) ${
details.description ?? ''
}`;
}
function ActionButton({
icon,
onClick,
id,
}: {
id: string;
icon: React.ReactElement;
onClick: (id: string) => void;
}) {
return (
<Button
size="small"
icon={icon}
style={{border: 'none', color: theme.textColorPrimary}}
onClick={() => {
onClick(id);
}}
/>
);
}
const PluginEntry = memo(function PluginEntry({
plugin,
disabled,
tooltip,
onClick,
active,
actions,
...rest
}: {
plugin: {id: string; title: string; icon?: string; version: string};
disabled?: boolean;
active?: boolean;
tooltip: string;
onClick?: (id: string) => void;
actions?: React.ReactElement;
}) {
const [hovering, setHovering] = useState(false);
const handleMouseEnter = useCallback(() => {
setHovering(true);
}, []);
const handleMouseLeave = useCallback(() => {
setHovering(false);
}, []);
const handleClick = useCallback(() => {
onClick?.(plugin.id);
}, [onClick, plugin.id]);
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const node = domRef.current;
if (active && node) {
const rect = node.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
node.scrollIntoView();
}
}
}, [active]);
return (
<Menu.Item
key={plugin.id}
active={active}
disabled={disabled}
onClick={handleClick}
{...rest}>
<Layout.Horizontal
center
gap={10}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<PluginIconWrapper disabled={disabled} ref={domRef}>
<Glyph size={16} name={plugin.icon || 'apps'} color="white" />
</PluginIconWrapper>
<Tooltip placement="right" title={tooltip} mouseEnterDelay={1}>
<Text style={{flex: 1}}>{getPluginTitle(plugin)}</Text>
</Tooltip>
{hovering && actions}
</Layout.Horizontal>
</Menu.Item>
);
});
const PluginGroup = memo(function PluginGroup({
title,
children,
...rest
}: {title: string; children: React.ReactElement[]} & Record<string, any>) {
if (children.length === 0) {
return null;
}
return (
<SubMenu
{...rest}
title={
<Layout.Right center>
<Text strong>{title}</Text>
<Badge
count={children.length}
style={{
marginRight: 20,
}}
/>
</Layout.Right>
}>
{children}
</SubMenu>
);
});
function computePluginLists(
device: BaseDevice,
metroDevice: BaseDevice | undefined,
client: Client | undefined,
plugins: State['plugins'],
userStarredPlugins: State['connections']['userStarredPlugins'],
) {
const devicePlugins: DevicePluginDefinition[] = device.devicePlugins.map(
(name) => plugins.devicePlugins.get(name)!,
);
const metroPlugins: DevicePluginDefinition[] =
metroDevice?.devicePlugins.map(
(name) => plugins.devicePlugins.get(name)!,
) ?? [];
const enabledPlugins: ClientPluginDefinition[] = [];
const disabledPlugins: ClientPluginDefinition[] = [];
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
{
// find all device plugins that aren't part of the current device / metro
const detectedDevicePlugins = new Set([
...device.devicePlugins,
...(metroDevice?.devicePlugins ?? []),
]);
for (const [name, definition] of plugins.devicePlugins.entries()) {
if (!detectedDevicePlugins.has(name)) {
unavailablePlugins.push([
definition.details,
`Device plugin '${getPluginTitle(
definition.details,
)}' is not supported by the current device type.`,
]);
}
}
}
// process all client plugins
if (client) {
const clientPlugins = Array.from(plugins.clientPlugins.values()).sort(
sortPluginsByName,
);
const favoritePlugins = getFavoritePlugins(
device,
client,
clientPlugins,
client && userStarredPlugins[client.query.app],
true,
);
client &&
clientPlugins.forEach((plugin) => {
if (!client.plugins.includes(plugin.id)) {
unavailablePlugins.push([
plugin.details,
`Plugin '${getPluginTitle(
plugin.details,
)}' is not loaded by the client application`,
]);
} else if (favoritePlugins.includes(plugin)) {
enabledPlugins.push(plugin);
} else {
disabledPlugins.push(plugin);
}
});
}
// process problematic plugins
plugins.disabledPlugins.forEach((plugin) => {
unavailablePlugins.push([plugin, 'Plugin is disabled by configuration']);
});
plugins.gatekeepedPlugins.forEach((plugin) => {
unavailablePlugins.push([
plugin,
`This plugin is only available to members of gatekeeper '${plugin.gatekeeper}'`,
]);
});
plugins.failedPlugins.forEach(([plugin, error]) => {
unavailablePlugins.push([
plugin,
`Flipper failed to load this plugin: '${error}'`,
]);
});
devicePlugins.sort(sortPluginsByName);
metroPlugins.sort(sortPluginsByName);
unavailablePlugins.sort(([a], [b]) => {
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
});
return {
devicePlugins,
metroPlugins,
enabledPlugins,
disabledPlugins,
unavailablePlugins,
};
}
// Dimensions are hardcoded as they correlate strongly
const PluginMenu = styled(Menu)({
userSelect: 'none',
border: 'none',
'.ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title ': {
width: '100%', // reset to remove weird bonus pixel from ANT
},
'.ant-menu-submenu > .ant-menu-submenu-title, .ant-menu-sub.ant-menu-inline > .ant-menu-item': {
borderRadius: theme.borderRadius,
height: '32px',
lineHeight: '24px',
padding: `4px 8px !important`,
'&:hover': {
color: theme.textColorPrimary,
background: theme.backgroundTransparentHover,
},
'&.ant-menu-item-selected::after': {
border: 'none',
},
'&.ant-menu-item-selected': {
color: theme.white,
background: theme.primaryColor,
border: 'none',
},
'&.ant-menu-item-selected .ant-typography': {
color: theme.white,
},
},
'.ant-menu-submenu-inline > .ant-menu-submenu-title .ant-menu-submenu-arrow': {
right: 8,
},
'.ant-badge-count': {
color: theme.textColorPrimary,
background: theme.backgroundTransparentHover,
fontWeight: 'bold',
padding: `0 10px`,
boxShadow: 'none',
},
});
const PluginIconWrapper = styled.div<{disabled?: boolean}>(({disabled}) => ({
...iconStyle(!!disabled),
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}));
function iconStyle(disabled: boolean) {
return {
color: theme.white,
background: disabled ? theme.disabledColor : theme.primaryColor,
borderRadius: theme.borderRadius,
width: 24,
height: 24,
};
}

View File

@@ -211,7 +211,10 @@ export function getPersistentPlugins(plugins: PluginsState): Array<string> {
});
}
export function getPluginTitle(pluginClass: PluginDefinition) {
export function getPluginTitle(pluginClass: {
title?: string | null;
id: string;
}) {
return pluginClass.title || pluginClass.id;
}

View File

@@ -28,7 +28,8 @@
12
],
"bird": [
12
12,
16
],
"borders": [
16
@@ -37,13 +38,15 @@
12
],
"brush-paint": [
12
12,
16
],
"bug": [
12
],
"building-city": [
12
12,
16
],
"camcorder": [
12,
@@ -111,7 +114,8 @@
24
],
"dashboard": [
12
12,
16
],
"data-table": [
16
@@ -120,7 +124,8 @@
12
],
"directions": [
12
12,
16
],
"dots-3-circle-outline": [
16
@@ -135,7 +140,8 @@
12
],
"flash-default": [
12
12,
16
],
"info-circle": [
12,
@@ -143,13 +149,15 @@
24
],
"internet": [
12
12,
16
],
"life-event-major": [
16
],
"magic-wand": [
12,
16,
20
],
"magnifying-glass": [
@@ -158,7 +166,8 @@
24
],
"messages": [
12
12,
16
],
"minus-circle": [
12
@@ -172,10 +181,12 @@
32
],
"network": [
12
12,
16
],
"news-feed": [
12
12,
16
],
"pause": [
16
@@ -187,7 +198,8 @@
16
],
"profile": [
12
12,
16
],
"question-circle-outline": [
16
@@ -205,6 +217,7 @@
],
"rocket": [
12,
16,
20
],
"settings": [
@@ -241,7 +254,8 @@
16
],
"thought-bubble": [
12
12,
16
],
"tools": [
12,
@@ -249,7 +263,8 @@
20
],
"translate": [
12
12,
16
],
"trash-outline": [
16
@@ -259,10 +274,12 @@
16
],
"tree": [
12
12,
16
],
"trending": [
12
12,
16
],
"triangle-down": [
12,
@@ -273,13 +290,16 @@
12
],
"underline": [
12
12,
16
],
"washing-machine": [
12
12,
16
],
"watch-tv": [
12
12,
16
],
"gears-two": [
16
@@ -315,25 +335,30 @@
16
],
"messenger-code": [
12
12,
16
],
"book": [
12
],
"list-arrow-up": [
12
12,
16
],
"cat": [
12
12,
16
],
"duplicate": [
12
12,
16
],
"profile-circle-outline": [
16
],
"card-person": [
12
12,
16
],
"pencil-outline": [
16
@@ -366,10 +391,12 @@
16
],
"paper-stack": [
12
12,
16
],
"weather-cold": [
12
12,
16
],
"mobile-cross": [
16
@@ -390,13 +417,15 @@
16
],
"marketplace": [
12
12,
16
],
"workflow": [
12
],
"sankey-diagram": [
12
12,
16
],
"media-stack": [
16
@@ -411,7 +440,8 @@
16
],
"log": [
12
12,
16
],
"triangle-up": [
16,
@@ -434,10 +464,12 @@
16
],
"scuba": [
12
12,
16
],
"line-chart": [
12
12,
16
],
"caution-circle": [
12
@@ -459,5 +491,26 @@
],
"sushi": [
12
],
"arrows-up-down": [
16
],
"style-effects": [
16
],
"stopwatch": [
16
],
"database": [
16
],
"bar-chart": [
16
],
"augmented-reality": [
16
],
"app-flash": [
16
]
}

View File

@@ -11,7 +11,7 @@
// Based on: https://www.figma.com/file/4e6BMdm2SuZ1L7FSuOPQVC/Flipper?node-id=620%3A84614
// Link Text & Icon
@primary-color: #722ED1;
@primary-color: @purple-6;
// Success
@success-color: @green-7;
// Negative