Browse Source

feat(ui): Icon Search in Storybook (#30392)

* feat(ui): Icon Search in Storybook

Adding an icon search page to Storybook and missing icon files from the design system.
Vu Luong 3 years ago
parent
commit
7061e04bcb

+ 1 - 1
.storybook/preview.tsx

@@ -138,7 +138,7 @@ addParameters({
         'Core',
         ['Overview', 'Colors', 'Typography'],
         'Assets',
-        ['Logo', 'Icons', 'Platforms'],
+        ['Icons', 'Logo', 'Platforms'],
         'Components',
         [
           'Buttons',

+ 1 - 1
docs-ui/components/code.tsx

@@ -17,7 +17,7 @@ import space from 'sentry/styles/space';
 import {Theme} from 'sentry/utils/theme';
 
 type Props = {
-  theme: Theme;
+  theme?: Theme;
   /**
    * Main code content gets passed as the children prop
    */

File diff suppressed because it is too large
+ 0 - 0
docs-ui/images/icons-link-preview.svg


+ 0 - 21
docs-ui/stories/assets/fileIcon.stories.js

@@ -1,21 +0,0 @@
-import FileIcon from 'sentry/components/fileIcon';
-
-export default {
-  title: 'Assets/Icons/File Icon',
-  component: FileIcon,
-  args: {
-    fileName: 'src/components/testComponent.tsx',
-    size: 'xl',
-  },
-};
-
-export const _FileIcon = ({...args}) => <FileIcon {...args} />;
-
-_FileIcon.storyName = 'File Icon';
-_FileIcon.parameters = {
-  docs: {
-    description: {
-      story: 'Shows a platform icon for given filename - based on extension',
-    },
-  },
-};

+ 0 - 175
docs-ui/stories/assets/iconProps.stories.js

@@ -1,175 +0,0 @@
-import styled from '@emotion/styled';
-
-import {IconAdd, IconArrow, IconBookmark, IconGroup, IconPin} from 'sentry/icons';
-
-export default {
-  title: 'Assets/Icons',
-};
-
-export const IconProps = () => {
-  return (
-    <SwatchWrapper>
-      <ColorSwatches>
-        <Header>Color Prop</Header>
-        <Swatch>
-          <IconBookmark />
-          <LabelWrapper>IconBookmark</LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconBookmark isSolid color="#6C5FC7" />
-          <LabelWrapper>
-            IconBookmark <Highlight>solid color="#6C5FC7"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-      </ColorSwatches>
-      <SizeSwatches>
-        <Header>Size Prop</Header>
-        <Swatch>
-          <IconGroup size="xs" />
-          <LabelWrapper>
-            IconGroup <Highlight>size="xs"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconGroup />
-          <LabelWrapper>IconGroup</LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconGroup size="md" />
-          <LabelWrapper>
-            IconGroup <Highlight>size="md"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconGroup size="lg" />
-          <LabelWrapper>
-            IconGroup <Highlight>size="lg"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconGroup size="xl" />
-          <LabelWrapper>
-            IconGroup <Highlight>size="xl"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-      </SizeSwatches>
-      <DirectionSwatches>
-        <Header>Direction Prop</Header>
-        <Swatch>
-          <IconArrow />
-          <LabelWrapper>IconArrow</LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconArrow direction="left" />
-          <LabelWrapper>
-            IconArrow <Highlight>direction="left"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconArrow direction="down" />
-          <LabelWrapper>
-            IconArrow <Highlight>direction="down"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconArrow direction="right" />
-          <LabelWrapper>
-            IconArrow <Highlight>direction="right"</Highlight>
-          </LabelWrapper>
-        </Swatch>
-      </DirectionSwatches>
-      <CircleSwatches>
-        <Header>Circle Prop</Header>
-        <Swatch>
-          <IconAdd />
-          <LabelWrapper>IconAdd</LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconAdd circle />
-          <LabelWrapper>
-            IconAdd <Highlight>circle</Highlight>
-          </LabelWrapper>
-        </Swatch>
-      </CircleSwatches>
-      <SolidSwatches>
-        <Header>Solid Prop</Header>
-        <Swatch>
-          <IconPin />
-          <LabelWrapper>IconPin</LabelWrapper>
-        </Swatch>
-        <Swatch>
-          <IconPin solid />
-          <LabelWrapper>
-            IconPin <Highlight>solid</Highlight>
-          </LabelWrapper>
-        </Swatch>
-      </SolidSwatches>
-    </SwatchWrapper>
-  );
-};
-IconProps.parameters = {
-  docs: {
-    description: {
-      story: 'Props you can assign to the icon components',
-    },
-  },
-};
-
-const Highlight = styled('span')`
-  color: ${p => p.theme.purple300};
-  font-weight: 600;
-`;
-
-const Header = styled('h5')`
-  margin-bottom: 8px;
-`;
-
-const LabelWrapper = styled('div')`
-  font-size: 14px;
-  margin-left: 16px;
-`;
-
-const SwatchWrapper = styled('div')`
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  grid-template-rows: repeat(5, auto);
-  gap: 16px;
-`;
-
-const Swatches = styled('div')`
-  border: 1px solid ${p => p.theme.border};
-  padding: 24px;
-`;
-
-const ColorSwatches = styled(Swatches)`
-  grid-column: 1/2;
-  grid-row: 1/2;
-`;
-
-const SizeSwatches = styled(Swatches)`
-  grid-column: 1/2;
-  grid-row: 2/6;
-`;
-
-const DirectionSwatches = styled(Swatches)`
-  grid-column: 2/3;
-  grid-row: 1/4;
-`;
-
-const CircleSwatches = styled(Swatches)`
-  grid-column: 2/3;
-`;
-
-const SolidSwatches = styled(Swatches)`
-  grid-column: 2/3;
-`;
-
-const Swatch = styled('div')`
-  display: flex;
-  align-items: center;
-  min-height: 32px;
-
-  svg {
-    min-width: 32px;
-  }
-`;

+ 0 - 55
docs-ui/stories/assets/iconSet.stories.js

@@ -1,55 +0,0 @@
-import styled from '@emotion/styled';
-
-import * as newIconset from 'sentry/icons';
-
-export default {
-  title: 'Assets/Icons/Icon Set',
-};
-
-export const IconSet = () => {
-  return (
-    <SwatchWrapper>
-      <Header>Icon Set</Header>
-      <Swatches>
-        {Object.entries(newIconset).map(([key, Icon]) => (
-          <Swatch key={key}>
-            <Icon />
-            <LabelWrapper>{key}</LabelWrapper>
-          </Swatch>
-        ))}
-      </Swatches>
-    </SwatchWrapper>
-  );
-};
-
-const Header = styled('h5')`
-  margin-bottom: 16px;
-`;
-
-const LabelWrapper = styled('div')`
-  font-size: 14px;
-  margin-left: 16px;
-`;
-
-const SwatchWrapper = styled('div')`
-  border: 1px solid ${p => p.theme.border};
-  padding: 24px;
-`;
-
-const Swatches = styled('div')`
-  display: grid;
-  grid-template-columns: repeat(auto-fill, 160px);
-  gap: 8px;
-`;
-
-const Swatch = styled('div')`
-  display: flex;
-  align-items: center;
-  min-height: 32px;
-
-  svg {
-    min-width: 32px;
-  }
-`;
-
-IconSet.storyName = 'Icon Set';

+ 607 - 0
docs-ui/stories/assets/icons/data.tsx

@@ -0,0 +1,607 @@
+type IconGroupName =
+  | 'action'
+  | 'navigation'
+  | 'content'
+  | 'file'
+  | 'issue'
+  | 'chart'
+  | 'layout'
+  | 'media'
+  | 'device'
+  | 'other'
+  | 'logo';
+
+export type IconPropName = 'size' | 'direction' | 'isCircled' | 'isSolid' | 'type';
+
+type IconProps = {
+  [key in IconPropName]: {
+    type: 'boolean' | 'select';
+    options?: [string, string][];
+    default?: string;
+    /**
+     * Whether to list all variants of this prop in the icon list
+     */
+    enumerate?: boolean;
+  };
+};
+
+type IconGroup = {
+  id: IconGroupName;
+  label: string;
+};
+
+export type IconData = {
+  id: string;
+  /**
+   * List of alternative keywords for better icon search, e.g. the
+   * icon 'checkmark' could have a ['done', 'success'] keyword list
+   */
+  keywords: string[];
+  /**
+   * Groups that the icon belongs to
+   */
+  groups: IconGroupName[];
+  /**
+   * Any additional props besides 'size' and 'color'. This includes
+   * props like 'isCircled' and 'direction'.
+   */
+  additionalProps?: IconPropName[];
+  /**
+   * Limit the set of options available for certain additional props.
+   * For example, {direction: ['left', 'up']} would limit the available
+   * options for the prop 'direction' to just 'left' and 'up'. Useful for
+   * controlling prop enumeration in the icon list.
+   */
+  limitOptions?: Partial<Record<IconPropName, string[][]>>;
+};
+
+export const iconProps: IconProps = {
+  size: {
+    type: 'select',
+    options: [
+      ['xs', 'Extra small'],
+      ['sm', 'Small'],
+      ['md', 'Medium'],
+      ['lg', 'Large'],
+      ['xl', 'Extra large'],
+    ],
+    default: 'sm',
+  },
+  type: {
+    type: 'select',
+    options: [
+      ['line', 'Line'],
+      ['circle', 'Circle'],
+      ['bar', 'Bar'],
+    ],
+    default: 'line',
+    enumerate: true,
+  },
+  direction: {
+    type: 'select',
+    options: [
+      ['left', 'Left'],
+      ['right', 'Right'],
+      ['up', 'Up'],
+      ['down', 'Down'],
+    ],
+    default: 'left',
+    enumerate: true,
+  },
+  isCircled: {type: 'boolean', enumerate: true},
+  isSolid: {type: 'boolean', enumerate: true},
+};
+
+export const iconGroups: IconGroup[] = [
+  {
+    id: 'action',
+    label: 'Action',
+  },
+  {
+    id: 'navigation',
+    label: 'Navigation',
+  },
+  {
+    id: 'content',
+    label: 'Content',
+  },
+  {
+    id: 'layout',
+    label: 'Layout',
+  },
+  {
+    id: 'issue',
+    label: 'Issue',
+  },
+  {
+    id: 'file',
+    label: 'File',
+  },
+  {
+    id: 'media',
+    label: 'Media',
+  },
+  {
+    id: 'chart',
+    label: 'Chart',
+  },
+  {
+    id: 'device',
+    label: 'Device',
+  },
+  {
+    id: 'other',
+    label: 'Other',
+  },
+  {
+    id: 'logo',
+    label: 'Logo',
+  },
+];
+
+export const icons: IconData[] = [
+  {id: 'add', groups: ['action'], keywords: ['plus'], additionalProps: ['isCircled']},
+  {
+    id: 'subtract',
+    groups: ['action'],
+    keywords: ['minus'],
+    additionalProps: ['isCircled'],
+  },
+  {
+    id: 'checkmark',
+    groups: ['action'],
+    keywords: ['done', 'finish', 'success', 'confirm', 'resolve'],
+    additionalProps: ['isCircled'],
+  },
+  {
+    id: 'close',
+    groups: ['action'],
+    keywords: ['cross', 'deny', 'terminate'],
+    additionalProps: ['isCircled'],
+  },
+  {
+    id: 'chevron',
+    groups: ['action', 'navigation'],
+    keywords: [
+      'up',
+      'down',
+      'left',
+      'right',
+      'point',
+      'direct',
+      'move',
+      'expand',
+      'collapse',
+      'arrow',
+    ],
+    additionalProps: ['isCircled', 'direction'],
+  },
+  {
+    id: 'arrow',
+    groups: ['navigation'],
+    keywords: ['up', 'down', 'left', 'right', 'point', 'direct', 'move'],
+    additionalProps: ['direction'],
+  },
+  {id: 'upload', groups: ['action', 'file'], keywords: ['file', 'image', 'up']},
+  {id: 'download', groups: ['action', 'file'], keywords: ['file', 'image', 'down']},
+  {id: 'sync', groups: ['action', 'file'], keywords: ['swap']},
+  {id: 'menu', groups: ['layout'], keywords: ['navigate']},
+  {id: 'list', groups: ['layout'], keywords: ['item']},
+  {id: 'activity', groups: ['layout', 'issue'], keywords: ['list']},
+  {id: 'dashboard', groups: ['layout'], keywords: ['overview', 'group', 'organize']},
+  {id: 'projects', groups: ['content', 'layout'], keywords: ['overview']},
+  {
+    id: 'upgrade',
+    groups: ['action', 'file'],
+    keywords: ['up'],
+  },
+  {
+    id: 'open',
+    groups: ['action', 'file'],
+    keywords: ['link', 'hyperlink', 'external'],
+  },
+  {
+    id: 'return',
+    groups: ['action'],
+    keywords: ['enter'],
+  },
+  {
+    id: 'refresh',
+    groups: ['action', 'navigation'],
+    keywords: ['reload', 'restart'],
+  },
+  {
+    id: 'bookmark',
+    groups: ['action'],
+    keywords: ['favorite', 'star', 'mark'],
+    additionalProps: ['isSolid'],
+  },
+  {
+    id: 'pin',
+    groups: ['action'],
+    keywords: ['stick'],
+    additionalProps: ['isSolid'],
+  },
+  {
+    id: 'star',
+    groups: ['action'],
+    keywords: ['favorite', 'star', 'bookmark'],
+    additionalProps: ['isSolid'],
+  },
+  {
+    id: 'play',
+    groups: ['media'],
+    keywords: ['video', 'audio', 'unpause'],
+  },
+  {
+    id: 'pause',
+    groups: ['media'],
+    keywords: ['video', 'audio', 'stop'],
+  },
+  {
+    id: 'previous',
+    groups: ['media'],
+    keywords: ['video', 'audio', 'back', 'return', 'rewind'],
+  },
+  {
+    id: 'next',
+    groups: ['media'],
+    keywords: ['video', 'audio', 'skip', 'forward'],
+  },
+  {
+    id: 'graph',
+    groups: ['chart'],
+    keywords: ['line', 'plot'],
+    additionalProps: ['type'],
+  },
+  {
+    id: 'stats',
+    groups: ['chart'],
+    keywords: ['bar', 'graph'],
+  },
+  {
+    id: 'file',
+    groups: ['file', 'content'],
+    keywords: ['document'],
+  },
+  {
+    id: 'search',
+    groups: ['action'],
+    keywords: ['find', 'look', 'query'],
+  },
+  {
+    id: 'copy',
+    groups: ['action', 'file', 'content'],
+    keywords: ['duplicate'],
+  },
+  {
+    id: 'delete',
+    groups: ['action', 'content'],
+    keywords: ['trash', 'can', 'dumpster', 'remove', 'erase', 'clear'],
+  },
+  {
+    id: 'docs',
+    groups: ['file'],
+    keywords: ['document'],
+  },
+  {
+    id: 'print',
+    groups: ['action', 'file'],
+    keywords: [],
+  },
+  {
+    id: 'project',
+    groups: ['issue'],
+    keywords: [],
+  },
+  {
+    id: 'code',
+    groups: ['content'],
+    keywords: ['snippet'],
+  },
+  {
+    id: 'markdown',
+    groups: ['content'],
+    keywords: ['code'],
+  },
+  {
+    id: 'terminal',
+    groups: ['device', 'content'],
+    keywords: ['code', 'bash', 'command'],
+  },
+  {
+    id: 'commit',
+    groups: ['content'],
+    keywords: ['git', 'github'],
+  },
+  {
+    id: 'issues',
+    groups: ['content', 'issue'],
+    keywords: ['stack'],
+  },
+  {
+    id: 'releases',
+    groups: ['content', 'issue'],
+    keywords: ['stack', 'versions'],
+  },
+  {
+    id: 'stack',
+    groups: ['layout', 'content'],
+    keywords: ['group', 'combine', 'view'],
+  },
+  {
+    id: 'span',
+    groups: ['content'],
+    keywords: ['performance', 'transaction'],
+  },
+  {
+    id: 'link',
+    groups: ['action', 'content'],
+    keywords: ['hyperlink', 'anchor'],
+  },
+  {
+    id: 'attachment',
+    groups: ['action', 'content'],
+    keywords: ['include', 'clip'],
+  },
+  {
+    id: 'location',
+    groups: ['content'],
+    keywords: ['pin', 'position', 'map'],
+    additionalProps: ['isSolid'],
+  },
+  {
+    id: 'edit',
+    groups: ['action', 'content'],
+    keywords: ['pencil'],
+  },
+  {
+    id: 'filter',
+    groups: ['action', 'content'],
+    keywords: [],
+  },
+  {
+    id: 'show',
+    groups: ['action', 'content'],
+    keywords: ['visible'],
+  },
+  {
+    id: 'lock',
+    groups: ['action'],
+    keywords: ['secure'],
+  },
+  {
+    id: 'grabbable',
+    groups: ['action', 'layout'],
+    keywords: ['move', 'arrange', 'organize', 'rank', 'switch'],
+  },
+  {
+    id: 'ellipsis',
+    groups: ['action', 'layout'],
+    keywords: ['expand', 'open', 'more', 'hidden'],
+  },
+  {
+    id: 'fire',
+    groups: ['issue'],
+    keywords: ['danger', 'severe', 'critical'],
+  },
+  {
+    id: 'megaphone',
+    groups: ['other'],
+    keywords: ['speaker', 'announce'],
+  },
+  {
+    id: 'question',
+    groups: ['layout'],
+    keywords: ['info', 'about', 'information', 'ask', 'faq', 'q&a'],
+  },
+  {
+    id: 'info',
+    groups: ['layout'],
+    keywords: ['more', 'about', 'information', 'ask', 'faq', 'q&a'],
+  },
+  {
+    id: 'warning',
+    groups: ['issue'],
+    keywords: ['alert', 'notification'],
+  },
+  {
+    id: 'not',
+    groups: ['other'],
+    keywords: ['invalid', 'no', 'forbidden'],
+  },
+  {
+    id: 'laptop',
+    groups: ['device'],
+    keywords: ['computer', 'macbook'],
+  },
+  {
+    id: 'mobile',
+    groups: ['device'],
+    keywords: ['phone', 'iphone'],
+  },
+  {
+    id: 'window',
+    groups: ['device'],
+    keywords: ['application'],
+  },
+  {
+    id: 'user',
+    groups: ['content'],
+    keywords: ['person', 'portrait'],
+  },
+  {
+    id: 'group',
+    groups: ['content'],
+    keywords: ['person', 'people'],
+  },
+  {
+    id: 'chat',
+    groups: ['action', 'content'],
+    keywords: ['message', 'bubble'],
+  },
+  {
+    id: 'support',
+    groups: ['content'],
+    keywords: ['microphone', 'help'],
+  },
+  {
+    id: 'clock',
+    groups: ['content'],
+    keywords: ['time', 'watch'],
+  },
+  {
+    id: 'calendar',
+    groups: ['content'],
+    keywords: ['time', 'date'],
+  },
+  {
+    id: 'sliders',
+    groups: ['action'],
+    keywords: ['settings', 'slide', 'adjust'],
+    additionalProps: ['direction'],
+    limitOptions: {
+      direction: [
+        ['left', 'Left'],
+        ['up', 'Up'],
+      ],
+    },
+  },
+  {id: 'switch', groups: ['action'], keywords: ['swap']},
+  {
+    id: 'toggle',
+    groups: ['action'],
+    keywords: ['switch', 'form', 'disable', 'enable'],
+  },
+  {
+    id: 'settings',
+    groups: ['content'],
+    keywords: ['preference'],
+  },
+  {
+    id: 'mail',
+    groups: ['content'],
+    keywords: ['email'],
+  },
+  {
+    id: 'fix',
+    groups: ['action'],
+    keywords: ['wrench', 'resolve'],
+  },
+  {
+    id: 'lab',
+    groups: ['content', 'other'],
+    keywords: ['experiment', 'test'],
+  },
+  {
+    id: 'tag',
+    groups: ['content'],
+    keywords: ['price', 'category', 'group'],
+  },
+  {
+    id: 'broadcast',
+    groups: ['action', 'content'],
+    keywords: ['stream'],
+  },
+  {
+    id: 'telescope',
+    groups: ['other'],
+    keywords: [],
+  },
+  {
+    id: 'moon',
+    groups: ['action'],
+    keywords: ['dark', 'night'],
+  },
+  {
+    id: 'lightning',
+    groups: ['content'],
+    keywords: ['feature', 'new', 'fresh'],
+    additionalProps: ['isSolid'],
+  },
+  {
+    id: 'business',
+    groups: ['content'],
+    keywords: ['feature', 'promotion', 'fresh', 'new'],
+  },
+  {
+    id: 'bell',
+    groups: ['content'],
+    keywords: ['alert', 'notification', 'ring'],
+  },
+  {
+    id: 'siren',
+    groups: ['content'],
+    keywords: ['alert', 'important', 'warning'],
+  },
+  {
+    id: 'anchor',
+    groups: ['other'],
+    keywords: [],
+  },
+  {
+    id: 'circle',
+    groups: ['other'],
+    keywords: ['shape', 'round'],
+  },
+  {
+    id: 'rectangle',
+    groups: ['other'],
+    keywords: ['shape', 'rect', 'diamond'],
+  },
+  {
+    id: 'flag',
+    groups: ['action'],
+    keywords: ['bookmark', 'mark', 'save', 'warning', 'message'],
+  },
+  {
+    id: 'sound',
+    groups: ['content', 'action'],
+    keywords: ['audio'],
+  },
+  {
+    id: 'sentry',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'bitbucket',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'github',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'gitlab',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'google',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'jira',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'trello',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'vsts',
+    groups: ['logo'],
+    keywords: [],
+  },
+  {
+    id: 'generic',
+    groups: ['logo'],
+    keywords: [],
+  },
+];

+ 9 - 0
docs-ui/stories/assets/icons/iconSearch.stories.mdx

@@ -0,0 +1,9 @@
+import SearchPanel from './searchPanel';
+
+<Meta title="Assets/Icons" parameters={{hideToc: true}} />
+
+# Icons
+
+**Pro tip:** in addition to icon name, you can also search by keyword. For example, typing either 'checkmark' or 'success' will return IconCheckmark.
+
+<SearchPanel />

+ 88 - 0
docs-ui/stories/assets/icons/infoBox.tsx

@@ -0,0 +1,88 @@
+import {Dispatch, SetStateAction, useState} from 'react';
+import {Manager, Reference} from 'react-popper';
+import styled from '@emotion/styled';
+
+import space from 'sentry/styles/space';
+
+import IconPopper from './popper';
+import IconSample from './sample';
+import {ExtendedIconData, SelectedIcon} from './searchPanel';
+
+type Props = {
+  icon: ExtendedIconData;
+  selectedIcon: SelectedIcon;
+  setSelectedIcon: Dispatch<SetStateAction<SelectedIcon>>;
+  groupId: string;
+};
+
+const IconInfoBox = ({icon, selectedIcon, setSelectedIcon, groupId}: Props) => {
+  const isSelected = selectedIcon.group === groupId && selectedIcon.icon === icon.id;
+  const [boxRef, setBoxRef] = useState(null);
+
+  return (
+    <Manager>
+      <Reference>
+        {({ref: popperRef}) => (
+          <BoxWrap
+            ref={ref => {
+              setBoxRef(ref);
+              popperRef(ref);
+            }}
+            selected={isSelected}
+            onClick={() =>
+              setSelectedIcon(
+                isSelected ? {group: '', icon: ''} : {group: groupId, icon: icon.id}
+              )
+            }
+          >
+            <IconSample
+              name={icon.name}
+              size="xl"
+              color="gray500"
+              {...icon.defaultProps}
+            />
+            <Name>{icon.name}</Name>
+          </BoxWrap>
+        )}
+      </Reference>
+      {isSelected && (
+        <IconPopper icon={icon} setSelectedIcon={setSelectedIcon} boxRef={boxRef} />
+      )}
+    </Manager>
+  );
+};
+
+export default IconInfoBox;
+
+const BoxWrap = styled('div')<{selected: boolean}>`
+  grid-column-end: span 1;
+  text-align: center;
+  justify-content: center;
+  padding: ${space(2)};
+  border: solid 1px transparent;
+  border-radius: ${p => p.theme.borderRadius};
+  cursor: pointer;
+
+  &:hover {
+    border-color: ${p => p.theme.innerBorder};
+  }
+
+  ${p =>
+    p.selected &&
+    `
+    border-color: ${p.theme.blue200};
+    background: ${p.theme.blue100};
+
+    &:hover {
+      border-color: ${p.theme.blue200};
+    }
+    `}
+`;
+
+const Name = styled('p')`
+  position: relative;
+  margin-top: ${space(1)};
+  margin-bottom: 0;
+  font-size: 0.875rem;
+  text-transform: capitalize;
+`;

+ 202 - 0
docs-ui/stories/assets/icons/popper.tsx

@@ -0,0 +1,202 @@
+import {Dispatch, RefObject, SetStateAction, useEffect, useState} from 'react';
+import {Popper} from 'react-popper';
+import styled from '@emotion/styled';
+import Code from 'docs-ui/components/code';
+
+import SelectField from 'sentry/components/forms/selectField';
+import space from 'sentry/styles/space';
+import BooleanField from 'sentry/views/settings/components/forms/booleanField';
+
+import {iconProps} from './data';
+import IconSample from './sample';
+import {ExtendedIconData, SelectedIcon} from './searchPanel';
+
+type Props = {
+  icon: ExtendedIconData;
+  setSelectedIcon: Dispatch<SetStateAction<SelectedIcon>>;
+  boxRef: RefObject<HTMLDivElement>;
+};
+
+const IconPopper = ({icon, setSelectedIcon, boxRef}: Props) => {
+  /**
+   * Editable icon props
+   */
+  const [size, setSize] = useState(iconProps.size.default);
+  const [direction, setDirection] = useState(
+    icon.defaultProps?.direction ?? iconProps.direction.default
+  );
+  const [type, setType] = useState(icon.defaultProps?.type ?? iconProps.type.default);
+  const [isCircled, setIsCircled] = useState(icon.defaultProps?.isCircled ?? false);
+  const [isSolid, setIsSolid] = useState(icon.defaultProps?.isSolid ?? false);
+
+  /**
+   * Generate and update code sample based on prop states
+   */
+  const getCodeSample = () => {
+    return `<Icon${icon.name} color="gray500" size="${size}"${
+      isCircled ? ' isCircled' : ' '
+    }${isSolid ? ' isSolid' : ' '} />`;
+  };
+  const [codeSample, setCodeSample] = useState(getCodeSample());
+  useEffect(() => {
+    setCodeSample(getCodeSample());
+  }, [size, isCircled, isSolid]);
+
+  /**
+   * Deselect icon box on outside click
+   */
+  const clickAwayHandler = e => {
+    if (e.target !== boxRef && !boxRef?.contains?.(e.target)) {
+      setSelectedIcon({group: '', icon: ''});
+    }
+  };
+  useEffect(() => {
+    document.addEventListener('click', clickAwayHandler);
+    return () => document.removeEventListener('click', clickAwayHandler);
+  }, []);
+
+  return (
+    <Popper
+      placement="bottom"
+      modifiers={{
+        offset: {offset: '0, 10'},
+        flip: {enabled: false},
+      }}
+    >
+      {({ref: popperRef, style, placement}) => {
+        return (
+          <Wrap
+            ref={popperRef}
+            style={style}
+            data-placement={placement}
+            /**
+             * Prevent click event from propagating up to <IconInfoBox />,
+             * otherwise it would trigger setSelectedIcon and deselect the icon box.
+             */
+            onClick={e => e.stopPropagation()}
+          >
+            <SampleWrap>
+              <IconSample
+                name={icon.name}
+                size={size}
+                color="gray500"
+                {...(icon.additionalProps?.includes('type') ? {type} : {})}
+                {...(icon.additionalProps?.includes('direction') ? {direction} : {})}
+                {...(isCircled ? {isCircled} : {})}
+                {...(isSolid ? {isSolid} : {})}
+              />
+            </SampleWrap>
+
+            <PropsWrap>
+              <SelectorWrap>
+                <SelectorLabel>Size</SelectorLabel>
+                <StyledSelectField
+                  name="size"
+                  defaultValue={iconProps.size.default}
+                  choices={iconProps.size.options}
+                  onChange={value => setSize(value as string)}
+                  clearable={false}
+                />
+              </SelectorWrap>
+              {icon.additionalProps?.includes('direction') && (
+                <SelectorWrap>
+                  <SelectorLabel>Direction</SelectorLabel>
+                  <StyledSelectField
+                    name="type"
+                    defaultValue={direction}
+                    choices={iconProps.direction.options}
+                    onChange={value => setDirection(value as string)}
+                    clearable={false}
+                  />
+                </SelectorWrap>
+              )}
+              {icon.additionalProps?.includes('type') && (
+                <SelectorWrap>
+                  <SelectorLabel>Type</SelectorLabel>
+                  <StyledSelectField
+                    name="type"
+                    defaultValue={type}
+                    choices={iconProps.type.options}
+                    onChange={value => setType(value as string)}
+                    clearable={false}
+                  />
+                </SelectorWrap>
+              )}
+              {icon.additionalProps?.includes('isCircled') && (
+                <StyledBooleanField
+                  name="isCircled"
+                  label="Circled"
+                  value={isCircled}
+                  onChange={value => setIsCircled(value)}
+                />
+              )}
+              {icon.additionalProps?.includes('isSolid') && (
+                <StyledBooleanField
+                  name="isSolid"
+                  label="Solid"
+                  value={isSolid}
+                  onChange={value => setIsSolid(value)}
+                />
+              )}
+            </PropsWrap>
+
+            <Code className="language-jsx">{codeSample}</Code>
+          </Wrap>
+        );
+      }}
+    </Popper>
+  );
+};
+
+export default IconPopper;
+
+const Wrap = styled('div')`
+  text-align: left;
+  max-width: 20rem;
+  padding: ${space(2)};
+  padding-bottom: 0;
+  background: ${p => p.theme.background};
+  border: solid 1px ${p => p.theme.border};
+  box-shadow: ${p => p.theme.dropShadowHeavy};
+  border-radius: ${p => p.theme.borderRadius};
+  z-index: ${p => p.theme.zIndex.modal};
+  cursor: initial;
+`;
+
+const SampleWrap = styled('div')`
+  width: 100%;
+  height: 4rem;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin: 0 auto ${space(3)};
+`;
+
+const PropsWrap = styled('div')``;
+
+const SelectorWrap = styled('div')`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  &:not(:first-of-type) {
+    padding-top: ${space(2)};
+  }
+  &:not(:last-of-type) {
+    padding-bottom: ${space(2)};
+    border-bottom: solid 1px ${p => p.theme.innerBorder};
+  }
+`;
+
+const SelectorLabel = styled('p')`
+  margin-bottom: 0;
+`;
+
+const StyledSelectField = styled(SelectField)`
+  width: 50%;
+  padding-left: 10px;
+`;
+
+const StyledBooleanField = styled(BooleanField)`
+  padding-left: 0;
+`;

Some files were not shown because too many files changed in this diff