diff --git a/hyperglass/ui/components/card/body.tsx b/hyperglass/ui/components/card/body.tsx
new file mode 100644
index 0000000..6d0cd92
--- /dev/null
+++ b/hyperglass/ui/components/card/body.tsx
@@ -0,0 +1,24 @@
+import { Flex } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { ICardBody } from './types';
+
+export const CardBody = (props: ICardBody) => {
+ const { onClick, ...rest } = props;
+ const bg = useColorValue('white', 'dark.500');
+ const color = useColorValue('dark.500', 'white');
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/card/footer.tsx b/hyperglass/ui/components/card/footer.tsx
new file mode 100644
index 0000000..2c90d2e
--- /dev/null
+++ b/hyperglass/ui/components/card/footer.tsx
@@ -0,0 +1,18 @@
+import { Flex } from '@chakra-ui/react';
+
+import type { ICardFooter } from './types';
+
+export const CardFooter = (props: ICardFooter) => (
+
+);
diff --git a/hyperglass/ui/components/card/header.tsx b/hyperglass/ui/components/card/header.tsx
new file mode 100644
index 0000000..4b89435
--- /dev/null
+++ b/hyperglass/ui/components/card/header.tsx
@@ -0,0 +1,21 @@
+import { Flex, Text } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { ICardHeader } from './types';
+
+export const CardHeader = (props: ICardHeader) => {
+ const { children, ...rest } = props;
+ const bg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
+ return (
+
+ {children}
+
+ );
+};
diff --git a/hyperglass/ui/components/card/index.ts b/hyperglass/ui/components/card/index.ts
new file mode 100644
index 0000000..9f66b54
--- /dev/null
+++ b/hyperglass/ui/components/card/index.ts
@@ -0,0 +1,3 @@
+export * from './body';
+export * from './footer';
+export * from './header';
diff --git a/hyperglass/ui/components/card/types.ts b/hyperglass/ui/components/card/types.ts
new file mode 100644
index 0000000..9c962fb
--- /dev/null
+++ b/hyperglass/ui/components/card/types.ts
@@ -0,0 +1,9 @@
+import type { FlexProps } from '@chakra-ui/react';
+
+export interface ICardBody extends Omit {
+ onClick?: () => boolean;
+}
+
+export interface ICardFooter extends FlexProps {}
+
+export interface ICardHeader extends FlexProps {}
diff --git a/hyperglass/ui/components/footer/button.tsx b/hyperglass/ui/components/footer/button.tsx
new file mode 100644
index 0000000..fecd07d
--- /dev/null
+++ b/hyperglass/ui/components/footer/button.tsx
@@ -0,0 +1,37 @@
+import { Button, Menu, MenuButton, MenuList } from '@chakra-ui/react';
+import { Markdown } from '~/components';
+import { useColorValue, useBreakpointValue } from '~/context';
+import { useOpposingColor } from '~/hooks';
+
+import type { TFooterButton } from './types';
+
+export const FooterButton = (props: TFooterButton) => {
+ const { content, title, side, ...rest } = props;
+ const placement = side === 'left' ? 'top' : side === 'right' ? 'top-start' : undefined;
+ const bg = useColorValue('white', 'gray.900');
+ const color = useOpposingColor(bg);
+ const size = useBreakpointValue({ base: 'xs', lg: 'sm' });
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/footer/colorMode.tsx b/hyperglass/ui/components/footer/colorMode.tsx
new file mode 100644
index 0000000..3626ee0
--- /dev/null
+++ b/hyperglass/ui/components/footer/colorMode.tsx
@@ -0,0 +1,43 @@
+import { forwardRef } from 'react';
+import dynamic from 'next/dynamic';
+import { Button, Icon, Tooltip } from '@chakra-ui/react';
+import { If } from '~/components';
+import { useColorMode, useColorValue, useBreakpointValue } from '~/context';
+import { useOpposingColor } from '~/hooks';
+
+import type { TColorModeToggle } from './types';
+
+const Sun = dynamic(() => import('@meronex/icons/hi').then(i => i.HiSun));
+const Moon = dynamic(() => import('@meronex/icons/hi').then(i => i.HiMoon));
+
+export const ColorModeToggle = forwardRef((props, ref) => {
+ const { size = '1.5rem', ...rest } = props;
+ const { colorMode, toggleColorMode } = useColorMode();
+
+ const bg = useColorValue('primary.500', 'yellow.300');
+ const color = useOpposingColor(bg);
+ const label = useColorValue('Switch to dark mode', 'Switch to light mode');
+ const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
+
+ return (
+
+
+
+ );
+});
diff --git a/hyperglass/ui/components/footer/footer.tsx b/hyperglass/ui/components/footer/footer.tsx
new file mode 100644
index 0000000..950af99
--- /dev/null
+++ b/hyperglass/ui/components/footer/footer.tsx
@@ -0,0 +1,65 @@
+import dynamic from 'next/dynamic';
+import { Button, Flex, Link, Icon, HStack, useToken } from '@chakra-ui/react';
+import { If } from '~/components';
+import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
+import { useStrf } from '~/hooks';
+import { FooterButton } from './button';
+import { ColorModeToggle } from './colorMode';
+
+const CodeIcon = dynamic(() => import('@meronex/icons/fi').then(i => i.FiCode));
+const ExtIcon = dynamic(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
+
+export const Footer = () => {
+ const { web, content, primary_asn } = useConfig();
+
+ const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
+ const footerColor = useColorValue('black', 'white');
+
+ const extUrl = useStrf(web.external_link.url, { primary_asn }) ?? '/';
+
+ const size = useBreakpointValue({ base: useToken('sizes', 4), lg: useToken('sizes', 6) });
+ const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
+
+ const isMobile = useMobile();
+
+ return (
+
+
+
+
+
+
+
+
+ }
+ aria-label={web.external_link.title}>
+ {web.external_link.title}
+
+
+ {!isMobile && }
+
+ }
+ />
+
+
+
+ );
+};
diff --git a/hyperglass/ui/components/footer/index.ts b/hyperglass/ui/components/footer/index.ts
new file mode 100644
index 0000000..a058eae
--- /dev/null
+++ b/hyperglass/ui/components/footer/index.ts
@@ -0,0 +1 @@
+export * from './footer';
diff --git a/hyperglass/ui/components/footer/types.ts b/hyperglass/ui/components/footer/types.ts
new file mode 100644
index 0000000..ddd71bd
--- /dev/null
+++ b/hyperglass/ui/components/footer/types.ts
@@ -0,0 +1,15 @@
+import type { ButtonProps, MenuListProps } from '@chakra-ui/react';
+
+type TFooterSide = 'left' | 'right';
+
+export interface TFooterButton extends Omit {
+ side: TFooterSide;
+ title?: MenuListProps['children'];
+ content: string;
+}
+
+export type TFooterItems = 'help' | 'credit' | 'terms';
+
+export interface TColorModeToggle extends ButtonProps {
+ size?: string;
+}
diff --git a/hyperglass/ui/components/markdown/elements.tsx b/hyperglass/ui/components/markdown/elements.tsx
new file mode 100644
index 0000000..37f7e57
--- /dev/null
+++ b/hyperglass/ui/components/markdown/elements.tsx
@@ -0,0 +1,87 @@
+import {
+ OrderedList,
+ UnorderedList,
+ Code as ChakraCode,
+ Link as ChakraLink,
+ Text as ChakraText,
+ Divider as ChakraDivider,
+ Heading as ChakraHeading,
+ Checkbox as ChakraCheckbox,
+ ListItem as ChakraListItem,
+} from '@chakra-ui/react';
+
+import { TD, TH, Table as ChakraTable } from './table';
+
+import { CodeBlock as CustomCodeBlock, If } from '~/components';
+
+import type {
+ BoxProps,
+ TextProps,
+ CodeProps,
+ LinkProps,
+ HeadingProps,
+ DividerProps,
+} from '@chakra-ui/react';
+import type { TCheckbox, TList, THeading, TCodeBlock, TTableData, TListItem } from './types';
+
+export const Checkbox = (props: TCheckbox) => {
+ const { checked, ...rest } = props;
+ return ;
+};
+
+export const List = (props: TList) => {
+ const { ordered, ...rest } = props;
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export const ListItem = (props: TListItem) => {
+ const { checked, ...rest } = props;
+ return checked ? : ;
+};
+
+export const Heading = (props: THeading) => {
+ const { level, ...rest } = props;
+
+ const levelMap = {
+ 1: { as: 'h1', size: 'lg', fontWeight: 'bold' },
+ 2: { as: 'h2', size: 'lg', fontWeight: 'normal' },
+ 3: { as: 'h3', size: 'lg', fontWeight: 'bold' },
+ 4: { as: 'h4', size: 'md', fontWeight: 'normal' },
+ 5: { as: 'h5', size: 'md', fontWeight: 'bold' },
+ 6: { as: 'h6', size: 'sm', fontWeight: 'bold' },
+ } as { [i: number]: HeadingProps };
+
+ return ;
+};
+
+export const Link = (props: LinkProps) => ;
+
+export const CodeBlock = (props: TCodeBlock) => {props.value};
+
+export const TableData = (props: TTableData) => {
+ const { isHeader, ...rest } = props;
+ return (
+ <>
+
+ |
+
+
+ |
+
+ >
+ );
+};
+
+export const Paragraph = (props: TextProps) => ;
+export const InlineCode = (props: CodeProps) => ;
+export const Divider = (props: DividerProps) => ;
+export const Table = (props: BoxProps) => ;
diff --git a/hyperglass/ui/components/markdown/index.ts b/hyperglass/ui/components/markdown/index.ts
new file mode 100644
index 0000000..99334b5
--- /dev/null
+++ b/hyperglass/ui/components/markdown/index.ts
@@ -0,0 +1 @@
+export * from './markdown';
diff --git a/hyperglass/ui/components/markdown/markdown.tsx b/hyperglass/ui/components/markdown/markdown.tsx
new file mode 100644
index 0000000..c368d32
--- /dev/null
+++ b/hyperglass/ui/components/markdown/markdown.tsx
@@ -0,0 +1,33 @@
+import ReactMarkdown from 'react-markdown';
+import {
+ List,
+ ListItem,
+ Heading,
+ Link,
+ CodeBlock,
+ TableData,
+ Paragraph,
+ InlineCode,
+ Divider,
+ Table,
+} from './elements';
+
+import type { ReactMarkdownProps } from 'react-markdown';
+import type { TMarkdown } from './types';
+
+const renderers = {
+ paragraph: Paragraph,
+ link: Link,
+ heading: Heading,
+ inlineCode: InlineCode,
+ list: List,
+ listItem: ListItem,
+ thematicBreak: Divider,
+ code: CodeBlock,
+ table: Table,
+ tableCell: TableData,
+} as ReactMarkdownProps['renderers'];
+
+export const Markdown = (props: TMarkdown) => (
+
+);
diff --git a/hyperglass/ui/components/markdown/table.tsx b/hyperglass/ui/components/markdown/table.tsx
new file mode 100644
index 0000000..d20d18c
--- /dev/null
+++ b/hyperglass/ui/components/markdown/table.tsx
@@ -0,0 +1,27 @@
+import { Box } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { BoxProps } from '@chakra-ui/react';
+
+export const Table = (props: BoxProps) => (
+
+);
+
+export const TH = (props: BoxProps) => {
+ const bg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
+ return ;
+};
+
+export const TD = (props: BoxProps) => {
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/markdown/types.ts b/hyperglass/ui/components/markdown/types.ts
new file mode 100644
index 0000000..f0b5b71
--- /dev/null
+++ b/hyperglass/ui/components/markdown/types.ts
@@ -0,0 +1,37 @@
+import type {
+ BoxProps,
+ CheckboxProps,
+ HeadingProps,
+ ListProps,
+ ListItemProps,
+} from '@chakra-ui/react';
+
+export interface TMarkdown {
+ content: string;
+}
+
+export interface TCheckbox extends CheckboxProps {
+ checked: boolean;
+}
+
+export interface TListItem {
+ checked: boolean;
+ children?: React.ReactNode;
+}
+
+export interface TList extends ListProps {
+ ordered: boolean;
+ children?: React.ReactNode;
+}
+
+export interface THeading extends HeadingProps {
+ level: 1 | 2 | 3 | 4 | 5 | 6;
+}
+
+export interface TCodeBlock {
+ value: React.ReactNode;
+}
+
+export interface TTableData extends BoxProps {
+ isHeader: boolean;
+}
diff --git a/hyperglass/ui/components/table/body.tsx b/hyperglass/ui/components/table/body.tsx
new file mode 100644
index 0000000..f90e06d
--- /dev/null
+++ b/hyperglass/ui/components/table/body.tsx
@@ -0,0 +1,16 @@
+import { Box } from '@chakra-ui/react';
+
+import type { BoxProps } from '@chakra-ui/react';
+
+export const TableBody = (props: BoxProps) => (
+
+);
diff --git a/hyperglass/ui/components/table/button.tsx b/hyperglass/ui/components/table/button.tsx
new file mode 100644
index 0000000..d86721f
--- /dev/null
+++ b/hyperglass/ui/components/table/button.tsx
@@ -0,0 +1,7 @@
+import { IconButton } from '@chakra-ui/react';
+
+import type { TTableIconButton } from './types';
+
+export const TableIconButton = (props: TTableIconButton) => (
+
+);
diff --git a/hyperglass/ui/components/table/cell.tsx b/hyperglass/ui/components/table/cell.tsx
new file mode 100644
index 0000000..27e68e9
--- /dev/null
+++ b/hyperglass/ui/components/table/cell.tsx
@@ -0,0 +1,28 @@
+import { Box } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { TTableCell } from './types';
+
+export const TableCell = (props: TTableCell) => {
+ const { bordersVertical = [false, 0], align, ...rest } = props;
+ const [doVerticalBorders, index] = bordersVertical;
+ const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100');
+
+ let borderProps = {};
+ if (doVerticalBorders && index !== 0) {
+ borderProps = { borderLeft: '1px solid', borderLeftColor };
+ }
+
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/table/head.tsx b/hyperglass/ui/components/table/head.tsx
new file mode 100644
index 0000000..adee3a8
--- /dev/null
+++ b/hyperglass/ui/components/table/head.tsx
@@ -0,0 +1,9 @@
+import { Box } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { BoxProps } from '@chakra-ui/react';
+
+export const TableHead = (props: BoxProps) => {
+ const bg = useColorValue('blackAlpha.100', 'whiteAlpha.100');
+ return ;
+};
diff --git a/hyperglass/ui/components/table/index.ts b/hyperglass/ui/components/table/index.ts
new file mode 100644
index 0000000..7285324
--- /dev/null
+++ b/hyperglass/ui/components/table/index.ts
@@ -0,0 +1,8 @@
+export * from './body';
+export * from './button';
+export * from './cell';
+export * from './head';
+export * from './main';
+export * from './main';
+export * from './pageSelect';
+export * from './row';
diff --git a/hyperglass/ui/components/table/main.tsx b/hyperglass/ui/components/table/main.tsx
new file mode 100644
index 0000000..d94a4b8
--- /dev/null
+++ b/hyperglass/ui/components/table/main.tsx
@@ -0,0 +1,202 @@
+import dynamic from 'next/dynamic';
+import { Flex, Icon, Text } from '@chakra-ui/react';
+import { usePagination, useSortBy, useTable } from 'react-table';
+import { useMobile } from '~/context';
+import { CardBody, CardFooter, CardHeader, If } from '~/components';
+import { TableMain } from './table';
+import { TableCell } from './cell';
+import { TableHead } from './head';
+import { TableRow } from './row';
+import { TableBody } from './body';
+import { TableIconButton } from './button';
+import { TableSelectShow } from './pageSelect';
+
+import type { TableOptions, PluginHook } from 'react-table';
+import type { TCellRender } from '~/types';
+import type { TTable } from './types';
+
+const ChevronRight = dynamic(() =>
+ import('@meronex/icons/fa').then(i => i.FaChevronRight),
+);
+
+const ChevronLeft = dynamic(() =>
+ import('@meronex/icons/fa').then(i => i.FaChevronLeft),
+);
+
+const ChevronDown = dynamic(() =>
+ import('@meronex/icons/fa').then(i => i.FaChevronDown),
+);
+
+const DoubleChevronRight = dynamic(() =>
+ import('@meronex/icons/fi').then(i => i.FiChevronsRight),
+);
+const DoubleChevronLeft = dynamic(() =>
+ import('@meronex/icons/fi').then(i => i.FiChevronsLeft),
+);
+
+export function Table(props: TTable) {
+ const {
+ data,
+ columns,
+ heading,
+ Cell,
+ rowHighlightBg,
+ striped = false,
+ rowHighlightProp,
+ bordersVertical = false,
+ bordersHorizontal = false,
+ } = props;
+
+ const isMobile = useMobile();
+
+ const defaultColumn = {
+ minWidth: 100,
+ width: 150,
+ maxWidth: 300,
+ };
+
+ let hiddenColumns = [] as string[];
+
+ for (const col of columns) {
+ if (col.hidden) {
+ hiddenColumns.push(col.accessor);
+ }
+ }
+
+ const options = {
+ columns,
+ defaultColumn,
+ data,
+ initialState: { hiddenColumns },
+ } as TableOptions;
+
+ const plugins = [useSortBy, usePagination] as PluginHook[];
+
+ const instance = useTable(options, ...plugins);
+
+ const {
+ page,
+ gotoPage,
+ nextPage,
+ pageCount,
+ prepareRow,
+ canNextPage,
+ pageOptions,
+ setPageSize,
+ headerGroups,
+ previousPage,
+ getTableProps,
+ canPreviousPage,
+ state: { pageIndex, pageSize },
+ } = instance;
+
+ return (
+
+ {heading && {heading}}
+
+
+ {headerGroups.map((headerGroup, i) => (
+
+ {headerGroup.headers.map(column => (
+
+
+ {column.render('Header')}
+
+
+
+
+
+
+
+
+
+ {''}
+
+ ))}
+
+ ))}
+
+
+ {page.map((row, key) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell, i) => {
+ const { column, row, value } = cell as TCellRender;
+ return (
+
+ {typeof Cell !== 'undefined' ? (
+ |
+ ) : (
+ cell.render('Cell')
+ )}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+
+ gotoPage(0)}
+ isDisabled={!canPreviousPage}
+ icon={}
+ />
+ previousPage()}
+ isDisabled={!canPreviousPage}
+ icon={}
+ />
+
+
+
+ Page{' '}
+
+ {pageIndex + 1} of {pageOptions.length}
+ {' '}
+
+ {!isMobile && (
+ {
+ setPageSize(Number(e.target.value));
+ }}
+ />
+ )}
+
+
+ }
+ />
+ }
+ onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
+ />
+
+
+
+ );
+}
diff --git a/hyperglass/ui/components/table/pageSelect.tsx b/hyperglass/ui/components/table/pageSelect.tsx
new file mode 100644
index 0000000..f58f157
--- /dev/null
+++ b/hyperglass/ui/components/table/pageSelect.tsx
@@ -0,0 +1,15 @@
+import { Select } from '@chakra-ui/react';
+import { SelectProps } from '@chakra-ui/react';
+
+export const TableSelectShow = (props: SelectProps) => {
+ const { value, ...rest } = props;
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/table/row.tsx b/hyperglass/ui/components/table/row.tsx
new file mode 100644
index 0000000..20e15db
--- /dev/null
+++ b/hyperglass/ui/components/table/row.tsx
@@ -0,0 +1,51 @@
+import { Box } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+import { useOpposingColor } from '~/hooks';
+
+import type { TTableRow } from './types';
+
+export const TableRow = (props: TTableRow) => {
+ const {
+ index = 0,
+ doStripe = false,
+ highlight = false,
+ highlightBg = 'primary',
+ doHorizontalBorders = false,
+ ...rest
+ } = props;
+
+ const alpha = useColorValue('100', '200');
+ const alphaHover = useColorValue('200', '100');
+ const bgStripe = useColorValue('blackAlpha.50', 'whiteAlpha.50');
+ let hoverBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
+ const rowBorder = useColorValue(
+ { borderTop: '1px', borderTopColor: 'blackAlpha.100' },
+ { borderTop: '1px', borderTopColor: 'whiteAlpha.100' },
+ );
+ let bg;
+
+ if (highlight) {
+ bg = `${String(highlightBg)}.${alpha}`;
+ hoverBg = `${String(highlightBg)}.${alphaHover}`;
+ } else if (doStripe && index % 2 !== 0) {
+ bg = bgStripe;
+ }
+ const defaultBg = useColorValue('white', 'black');
+ const color = useOpposingColor(bg ?? defaultBg);
+ const borderProps = doHorizontalBorders && index !== 0 ? rowBorder : {};
+
+ return (
+ td': { color } }}
+ fontWeight={highlight ? 'bold' : undefined}
+ _hover={{
+ cursor: 'pointer',
+ backgroundColor: highlight ? `${String(highlightBg)}.${alphaHover}` : hoverBg,
+ }}
+ {...borderProps}
+ {...rest}
+ />
+ );
+};
diff --git a/hyperglass/ui/components/table/table.tsx b/hyperglass/ui/components/table/table.tsx
new file mode 100644
index 0000000..27ca08e
--- /dev/null
+++ b/hyperglass/ui/components/table/table.tsx
@@ -0,0 +1,34 @@
+import { Box } from '@chakra-ui/react';
+import { useColorValue } from '~/context';
+
+import type { BoxProps } from '@chakra-ui/react';
+
+export const TableMain = (props: BoxProps) => {
+ const scrollbar = useColorValue('blackAlpha.300', 'whiteAlpha.300');
+ const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
+ const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/table/types.ts b/hyperglass/ui/components/table/types.ts
new file mode 100644
index 0000000..341d566
--- /dev/null
+++ b/hyperglass/ui/components/table/types.ts
@@ -0,0 +1,30 @@
+import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
+
+import type { Theme, TColumn, TCellRender } from '~/types';
+
+export interface TTable {
+ data: TRoute[];
+ striped?: boolean;
+ columns: TColumn[];
+ heading?: React.ReactNode;
+ bordersVertical?: boolean;
+ bordersHorizontal?: boolean;
+ Cell?: React.FC;
+ rowHighlightProp?: keyof IRoute;
+ rowHighlightBg?: Theme.ColorNames;
+}
+
+export interface TTableCell extends Omit {
+ bordersVertical?: [boolean, number];
+ align?: 'left' | 'right' | 'center';
+}
+
+export interface TTableRow extends BoxProps {
+ highlightBg?: Theme.ColorNames;
+ doHorizontalBorders?: boolean;
+ highlight?: boolean;
+ doStripe?: boolean;
+ index: number;
+}
+
+export type TTableIconButton = Omit;
diff --git a/hyperglass/ui/components/util/animated.ts b/hyperglass/ui/components/util/animated.ts
new file mode 100644
index 0000000..99fb7dc
--- /dev/null
+++ b/hyperglass/ui/components/util/animated.ts
@@ -0,0 +1,8 @@
+import { chakra } from '@chakra-ui/react';
+import { motion } from 'framer-motion';
+
+export const AnimatedDiv = motion.custom(chakra.div);
+export const AnimatedForm = motion.custom(chakra.form);
+export const AnimatedH1 = motion.custom(chakra.h1);
+export const AnimatedH3 = motion.custom(chakra.h3);
+export const AnimatedButton = motion.custom(chakra.button);
diff --git a/hyperglass/ui/components/util/if.tsx b/hyperglass/ui/components/util/if.tsx
new file mode 100644
index 0000000..9825a83
--- /dev/null
+++ b/hyperglass/ui/components/util/if.tsx
@@ -0,0 +1,6 @@
+import type { TIf } from './types';
+
+export const If = (props: TIf) => {
+ const { c, render, children, ...rest } = props;
+ return c ? (render ? render(rest) : children) : null;
+};
diff --git a/hyperglass/ui/components/util/index.ts b/hyperglass/ui/components/util/index.ts
new file mode 100644
index 0000000..d7a1522
--- /dev/null
+++ b/hyperglass/ui/components/util/index.ts
@@ -0,0 +1,2 @@
+export * from './animated';
+export * from './if';
diff --git a/hyperglass/ui/components/util/types.ts b/hyperglass/ui/components/util/types.ts
new file mode 100644
index 0000000..dfa0b58
--- /dev/null
+++ b/hyperglass/ui/components/util/types.ts
@@ -0,0 +1,5 @@
+export interface TIf {
+ c: boolean;
+ render?: (rest: any) => JSX.Element;
+ [k: string]: any;
+}