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 ( + + + {title} + + + + + + ); +}; 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 ( + + + + + + + + + + + {!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; +}