diff --git a/apps/website/public/assets/filters/empty.png b/apps/website/public/assets/filters/empty.png new file mode 100644 index 00000000..1f7de5aa Binary files /dev/null and b/apps/website/public/assets/filters/empty.png differ diff --git a/apps/website/src/components/index.tsx b/apps/website/src/components/index.tsx index 096e0f20..8c6fc30e 100644 --- a/apps/website/src/components/index.tsx +++ b/apps/website/src/components/index.tsx @@ -1,2 +1,4 @@ export { Breadcrumbs } from './breadcrumbs/breadcrumbs' +export { EpicOverview } from './epic-overview' export { SideBar } from './side-bar/side-bar' +export { TableIssues } from './table-issues/table-issues' diff --git a/apps/website/src/components/navigation/floating-menu.tsx b/apps/website/src/components/navigation/floating-menu.tsx index dea7c091..5fd4a8b4 100644 --- a/apps/website/src/components/navigation/floating-menu.tsx +++ b/apps/website/src/components/navigation/floating-menu.tsx @@ -55,7 +55,7 @@ const FloatingMenu = (): JSX.Element => { }} className={cx([ 'fixed left-1/2 top-1 z-10 flex -translate-x-1/2 flex-col items-center justify-between p-2 pb-0 lg:hidden', - 'bg-blur-neutral-80/80 border-neutral-80/5 rounded-2xl border backdrop-blur-md', + 'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md', ' w-[calc(100%-24px)]', ' opacity-0 transition-opacity data-[visible=true]:opacity-100', 'z-10', @@ -69,8 +69,8 @@ const FloatingMenu = (): JSX.Element => { }} className={cx([ 'fixed left-1/2 top-5 z-10 w-max min-w-[746px] -translate-x-1/2 overflow-hidden', - 'bg-blur-neutral-80/80 border-neutral-80/5 rounded-2xl border backdrop-blur-md', - 'md-lg:block hidden', + 'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md', + 'hidden md-lg:block', ])} > diff --git a/apps/website/src/components/table-issues.tsx b/apps/website/src/components/table-issues.tsx deleted file mode 100644 index 5fedd1cc..00000000 --- a/apps/website/src/components/table-issues.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Avatar, Button, Tag, Text } from '@status-im/components' -import Link from 'next/link' - -const issues = [ - { - id: 5154, - title: 'Add support for encrypted communities', - status: 'Open', - }, - { - id: 5155, - title: 'Add support for encrypted communities', - status: 'Open', - }, - { - id: 4, - title: 'Add support for encrypted communities', - status: 'Open', - }, - { - id: 4324, - title: 'Add support for encrypted communities', - status: 'Open', - }, - { - id: 876, - title: 'Add support for encrypted communities', - status: 'Open', - }, -] - -export const TableIssues = () => { - return ( -
-
- - 784 Open - -
- -
- {issues.map(issue => ( - -
- - {issue.title} - - - #9667 • Opened 2 days ago by slaedjenic - -
- -
-
- - - - -
- - - - -
- - ))} -
- -
- -
-
- ) -} diff --git a/apps/website/src/components/table-issues/filters/components/color-circle.tsx b/apps/website/src/components/table-issues/filters/components/color-circle.tsx new file mode 100644 index 00000000..53494b87 --- /dev/null +++ b/apps/website/src/components/table-issues/filters/components/color-circle.tsx @@ -0,0 +1,48 @@ +import { tokens } from '@status-im/components/src/tokens' + +import type { ColorTokens } from '@tamagui/core' + +// TypeGuard for ColorTokens +function isColorTokens( + value: `#${string}` | ColorTokens +): value is ColorTokens { + return typeof value === 'string' && value.startsWith('$') +} + +const ColorCircle = ({ + color: colorFromProps, + opacity = 40, + size = 16, +}: { + color: ColorTokens | `#${string}` + opacity?: number + size?: number +}) => { + if (!colorFromProps) { + return null + } + + let color: ColorTokens | string = colorFromProps + + if (isColorTokens(colorFromProps)) { + const colorToken = colorFromProps.replace( + '$', + '' + ) as keyof typeof tokens.color + color = tokens.color[colorToken]?.val || colorFromProps + } + + return ( +
+ ) +} + +export { ColorCircle } diff --git a/apps/website/src/components/table-issues/filters/dropdown-filter.tsx b/apps/website/src/components/table-issues/filters/dropdown-filter.tsx new file mode 100644 index 00000000..74243274 --- /dev/null +++ b/apps/website/src/components/table-issues/filters/dropdown-filter.tsx @@ -0,0 +1,140 @@ +import { cloneElement, useState } from 'react' + +import { Avatar, Button, Input, Text } from '@status-im/components' +import { DropdownMenu } from '@status-im/components/src/dropdown-menu' +import { DropdownIcon, SearchIcon } from '@status-im/icons' +import Image from 'next/image' + +import { useCurrentBreakpoint } from '@/hooks/use-current-breakpoint' + +import { ColorCircle } from './components/color-circle' + +import type { ColorTokens } from '@tamagui/core' + +type Data = { + id: number + name: string + avatar?: string | React.ReactElement + color?: ColorTokens | `#${string}` +} + +type Props = { + data: Data[] + label: string + placeholder?: string +} + +const isAvatar = (value: unknown): value is string => { + return typeof value === 'string' && value !== null +} + +const RenderIcon = (props: Data) => { + if (props.color) { + return + } + + if (!props.avatar) { + return <> + } + + if (isAvatar(props.avatar)) { + return + } + + return cloneElement(props.avatar) || <> +} + +const DropdownFilter = (props: Props) => { + const { data, label, placeholder } = props + + const [filterText, setFilterText] = useState('') + + // TODO - this will be improved by having a debounced search and use memoization + const filteredData = data.filter(label => + label.name.toLowerCase().includes(filterText.toLowerCase()) + ) + + const [selectedValues, setSelectedValues] = useState([]) + const [isOpen, setIsOpen] = useState(false) + + const currentBreakpoint = useCurrentBreakpoint() + + return ( +
+ setIsOpen(!isOpen)}> +
+ } + > + {label} + + +
+ } + size={32} + value={filterText} + onChangeText={setFilterText} + /> +
+ {filteredData.map(filtered => { + return ( + } + label={filtered.name} + checked={selectedValues.includes(filtered.id)} + onSelect={() => { + if (selectedValues.includes(filtered.id)) { + setSelectedValues( + selectedValues.filter(id => id !== filtered.id) + ) + } else { + setSelectedValues([...selectedValues, filtered.id]) + } + }} + /> + ) + })} + {filteredData.length === 0 && ( +
+ No results +
+ + No options found + +
+
+ + We didn't find results that match your search + +
+
+ )} +
+ +
+ ) +} + +export { DropdownFilter } +export type { Props as DropdownFilterProps } diff --git a/apps/website/src/components/table-issues/filters/dropdown-sort.tsx b/apps/website/src/components/table-issues/filters/dropdown-sort.tsx new file mode 100644 index 00000000..cd23e274 --- /dev/null +++ b/apps/website/src/components/table-issues/filters/dropdown-sort.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react' + +import { IconButton, Text } from '@status-im/components' +import { DropdownMenu } from '@status-im/components/src/dropdown-menu' +import { SortIcon } from '@status-im/icons' +import Image from 'next/image' + +type Data = { + id: number + name: string +} + +type Props = { + data: Data[] +} + +const DropdownSort = (props: Props) => { + const { data } = props + + const [selectedValue, setSelectedValue] = useState() + + return ( +
+ +
+ } variant="outline" /> +
+ +
+ + Sort by + +
+ {data.map(option => { + return ( + { + setSelectedValue(option.id) + }} + selected={selectedValue === option.id} + /> + ) + })} + {data.length === 0 && ( +
+ No results +
+ + No options found + +
+
+ We didn't find any results +
+
+ )} +
+
+
+ ) +} + +export { DropdownSort } +export type { Props as DropdownSortProps } diff --git a/apps/website/src/components/table-issues/filters/index.ts b/apps/website/src/components/table-issues/filters/index.ts new file mode 100644 index 00000000..28bd6dc5 --- /dev/null +++ b/apps/website/src/components/table-issues/filters/index.ts @@ -0,0 +1,3 @@ +export { DropdownFilter } from './dropdown-filter' +export { DropdownSort } from './dropdown-sort' +export { Tabs } from './tabs' diff --git a/apps/website/src/components/table-issues/filters/tabs.tsx b/apps/website/src/components/table-issues/filters/tabs.tsx new file mode 100644 index 00000000..ed821ef9 --- /dev/null +++ b/apps/website/src/components/table-issues/filters/tabs.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react' + +import { DoneIcon, OpenIcon } from '@status-im/icons' + +const Tabs = (): JSX.Element => { + const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open') + + const isOpen = activeTab === 'open' + + return ( +
+
+ +
+
+ +
+
+ ) +} + +export { Tabs } diff --git a/apps/website/src/components/table-issues/index.tsx b/apps/website/src/components/table-issues/index.tsx new file mode 100644 index 00000000..2a1b462d --- /dev/null +++ b/apps/website/src/components/table-issues/index.tsx @@ -0,0 +1 @@ +export { TableIssues } from './table-issues' diff --git a/apps/website/src/components/table-issues/table-issues.tsx b/apps/website/src/components/table-issues/table-issues.tsx new file mode 100644 index 00000000..b99c5046 --- /dev/null +++ b/apps/website/src/components/table-issues/table-issues.tsx @@ -0,0 +1,324 @@ +import { useState } from 'react' + +import { Avatar, Button, Input, Tag, Text } from '@status-im/components' +import { ProfileIcon, SearchIcon } from '@status-im/icons' +import Link from 'next/link' + +import { useCurrentBreakpoint } from '@/hooks/use-current-breakpoint' + +import { DropdownFilter, DropdownSort, Tabs } from './filters' + +import type { DropdownFilterProps } from './filters/dropdown-filter' +import type { DropdownSortProps } from './filters/dropdown-sort' + +const issues = [ + { + id: 5154, + title: 'Add support for encrypted communities', + status: 'open', + }, + { + id: 5155, + title: 'Add support for encrypted communities', + status: 'open', + }, + { + id: 4, + title: 'Add support for encrypted communities', + status: 'open', + }, + { + id: 4324, + title: 'Add support for encrypted communities', + status: 'open', + }, + { + id: 134, + title: 'Add support for encrypted communities', + status: 'closed', + }, + { + id: 999, + title: 'Add support for encrypted communities', + status: 'closed', + }, + { + id: 873, + title: 'Add support for encrypted communities', + status: 'open', + }, + { + id: 123, + title: 'Add support for encrypted communities', + status: 'open', + }, +] + +const authors: DropdownFilterProps['data'] = [ + { + id: 4, + name: 'marcelines', + avatar: 'https://avatars.githubusercontent.com/u/29401404?v=4', + }, + { + id: 5, + name: 'prichodko', + avatar: 'https://avatars.githubusercontent.com/u/14926950?v=4', + }, + { + id: 6, + name: 'felicio', + avatar: 'https://avatars.githubusercontent.com/u/13265126?v=4', + }, + { + id: 7, + name: 'jkbktl', + avatar: 'https://avatars.githubusercontent.com/u/520927?v=4', + }, +] + +const epics: DropdownFilterProps['data'] = [ + { + id: 4, + name: 'E:ActivityCenter', + color: '$orange-60', + }, + { + id: 5, + name: 'E:Keycard', + color: '$purple-60', + }, + { + id: 6, + name: 'E:Wallet', + color: '$pink-60', + }, + { + id: 7, + name: 'E:Chat', + color: '$beige-60', + }, +] + +const labels: DropdownFilterProps['data'] = [ + { + id: 4, + name: 'Mobile', + color: '$blue-60', + }, + { + id: 5, + name: 'Frontend', + color: '$brown-50', + }, + { + id: 6, + name: 'Backend', + color: '$red-60', + }, + { + id: 7, + name: 'Desktop', + color: '$green-60', + }, +] + +const assignees: DropdownFilterProps['data'] = [ + { + id: 1, + name: 'Unassigned', + avatar: , + }, + { + id: 4, + name: 'marcelines', + avatar: 'https://avatars.githubusercontent.com/u/29401404?v=4', + }, + { + id: 5, + name: 'prichodko', + avatar: 'https://avatars.githubusercontent.com/u/14926950?v=4', + }, + { + id: 6, + name: 'felicio', + avatar: 'https://avatars.githubusercontent.com/u/13265126?v=4', + }, + { + id: 7, + name: 'jkbktl', + avatar: 'https://avatars.githubusercontent.com/u/520927?v=4', + }, +] + +const repositories: DropdownFilterProps['data'] = [ + { + id: 1, + name: 'status-mobile', + }, + { + id: 2, + name: 'status-desktop', + }, + { + id: 3, + name: 'status-web', + }, + { + id: 4, + name: 'status-go', + }, + { + id: 5, + name: 'nwaku', + }, + { + id: 6, + name: 'go-waku', + }, + { + id: 7, + name: 'js-waku', + }, + { + id: 8, + name: 'nimbus-eth2', + }, + { + id: 9, + name: 'help.status.im', + }, +] + +const sortOptions: DropdownSortProps['data'] = [ + { + id: 1, + name: 'Default', + }, + { + id: 2, + name: 'Alphabetical', + }, + { + id: 3, + name: 'Creation date', + }, + { + id: 4, + name: 'Updated', + }, + { + id: 5, + name: 'Completion', + }, +] + +const TableIssues = () => { + const [issuesSearchText, setIssuesSearchText] = useState('') + + const currentBreakpoint = useCurrentBreakpoint() + + return ( +
+
+
+ +
+
+
+
+
+ } + size={32} + value={issuesSearchText} + onChangeText={setIssuesSearchText} + variant="retractable" + direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'} + /> +
+ + + + + + +
+ +
+ +
+
+
+
+
+
+ +
+ {issues.map(issue => ( + +
+ + {issue.title} + + + #9667 • Opened 2 days ago by slaedjenic + +
+ +
+
+ + + + +
+ + + + +
+ + ))} +
+ +
+ +
+
+ ) +} + +export { TableIssues } diff --git a/apps/website/src/hooks/use-current-breakpoint.ts b/apps/website/src/hooks/use-current-breakpoint.ts new file mode 100644 index 00000000..ec5d3316 --- /dev/null +++ b/apps/website/src/hooks/use-current-breakpoint.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' + +import defaultTheme from 'tailwindcss/defaultTheme' + +// If we had custom breakpoints, we could use this to get the current breakpoint but we will use the default breakpoints for now +// import resolveConfig from 'tailwindcss/resolveConfig' +// import tailwindConfig from '../../tailwind.config' + +// const fullConfig = resolveConfig(tailwindConfig) + +type Breakpoint = keyof (typeof defaultTheme)['screens'] + +export function useCurrentBreakpoint(): Breakpoint { + const [breakpoint, setBreakpoint] = useState('sm') + + useEffect(() => { + const handleResize = () => { + const screenWidth = window.innerWidth + const breakpoints = Object.entries(defaultTheme.screens) as [ + Breakpoint, + string + ][] + + for (let i = breakpoints.length - 1; i >= 0; i--) { + const [breakpoint, minWidth] = breakpoints[i] + + const convertedMinWidth = parseInt(minWidth, 10) + + if (screenWidth >= convertedMinWidth) { + setBreakpoint(breakpoint) + return + } + } + } + + handleResize() + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return breakpoint +} diff --git a/apps/website/src/pages/insights/epics/[epic].tsx b/apps/website/src/pages/insights/epics/[epic].tsx index 4642e461..5c438e40 100644 --- a/apps/website/src/pages/insights/epics/[epic].tsx +++ b/apps/website/src/pages/insights/epics/[epic].tsx @@ -1,6 +1,4 @@ -import { Breadcrumbs } from '@/components' -import { EpicOverview } from '@/components/epic-overview' -import { TableIssues } from '@/components/table-issues' +import { Breadcrumbs, EpicOverview, TableIssues } from '@/components' import { InsightsLayout } from '@/layouts/insights-layout' import type { Page } from 'next' @@ -11,13 +9,14 @@ const EpicsDetailPage: Page = () => {
-
+
- +
+
diff --git a/apps/website/src/pages/insights/orphans.tsx b/apps/website/src/pages/insights/orphans.tsx index 191bb06f..6f0b79c8 100644 --- a/apps/website/src/pages/insights/orphans.tsx +++ b/apps/website/src/pages/insights/orphans.tsx @@ -1,6 +1,6 @@ import { Text } from '@status-im/components' -import { TableIssues } from '@/components/table-issues' +import { TableIssues } from '@/components' import { InsightsLayout } from '@/layouts/insights-layout' import type { Page } from 'next' diff --git a/apps/website/src/pages/insights/repos.tsx b/apps/website/src/pages/insights/repos.tsx index 35e2ff91..4f164371 100644 --- a/apps/website/src/pages/insights/repos.tsx +++ b/apps/website/src/pages/insights/repos.tsx @@ -1,4 +1,5 @@ -import { Text } from '@status-im/components' +import { Shadow, Text } from '@status-im/components' +import { OpenIcon, UnlockedIcon } from '@status-im/icons' import { Link } from '@/components/link' import { InsightsLayout } from '@/layouts/insights-layout' @@ -30,6 +31,24 @@ const repos = [ issues: 10, stars: 5, }, + { + name: 'nim-waku', + description: 'a free (libre) open source, mobile OS for Ethereum.', + issues: 10, + stars: 5, + }, + { + name: 'go-waku', + description: 'a free (libre) open source, mobile OS for Ethereum.', + issues: 10, + stars: 5, + }, + { + name: 'js-waku', + description: 'a free (libre) open source, mobile OS for Ethereum.', + issues: 10, + stars: 5, + }, { name: 'nimbus-eth2', description: 'a free (libre) open source, mobile OS for Ethereum.', @@ -55,30 +74,48 @@ const ReposPage: Page = () => {
{repos.map(repo => ( - - - {repo.name} - - - {repo.description} - + + +
+ + {repo.name} + + + {repo.description} + +
-
- - Public - - - 42 Issues - - - 32 - -
- +
+
+
+ +
+ + Public + +
+
+
+ +
+ + 42 issues + +
+
+
+ +
+ + 32 + +
+
+ +
))}
diff --git a/apps/website/src/pages/insights/workstreams/[workstream].tsx b/apps/website/src/pages/insights/workstreams/[workstream].tsx index da983be3..bb04061b 100644 --- a/apps/website/src/pages/insights/workstreams/[workstream].tsx +++ b/apps/website/src/pages/insights/workstreams/[workstream].tsx @@ -1,6 +1,4 @@ -import { Breadcrumbs } from '@/components' -import { EpicOverview } from '@/components/epic-overview' -import { TableIssues } from '@/components/table-issues' +import { Breadcrumbs, EpicOverview, TableIssues } from '@/components' import { InsightsLayout } from '@/layouts/insights-layout' import type { Page } from 'next' @@ -11,13 +9,14 @@ const WorkstreamDetailPage: Page = () => {
-
+
- +
+
diff --git a/packages/components/package.json b/packages/components/package.json index f7910c4e..329444bc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.1.1", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.3", "@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-popover": "^1.0.5", diff --git a/packages/components/src/checkbox/checkbox.stories.tsx b/packages/components/src/checkbox/checkbox.stories.tsx new file mode 100644 index 00000000..4e8a23e6 --- /dev/null +++ b/packages/components/src/checkbox/checkbox.stories.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' + +import { Checkbox } from './checkbox' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + component: Checkbox, + argTypes: {}, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=180-9685&t=tDEqIV09qddTZgXF-4', + }, + }, +} + +type Story = StoryObj + +const CheckBoxWithHookFilled = () => { + const [checked, setChecked] = useState(false) + + return ( + setChecked(!checked)} + variant="filled" + /> + ) +} + +const CheckBoxWithHookOutlined = () => { + const [checked, setChecked] = useState(false) + + return ( + setChecked(!checked)} + variant="outline" + /> + ) +} + +export const Filled: Story = { + render: () => { + return + }, +} + +export const Outlined: Story = { + render: () => { + return + }, +} + +export default meta diff --git a/packages/components/src/checkbox/checkbox.tsx b/packages/components/src/checkbox/checkbox.tsx new file mode 100644 index 00000000..4abe9d93 --- /dev/null +++ b/packages/components/src/checkbox/checkbox.tsx @@ -0,0 +1,124 @@ +import { Indicator as _Indicator, Root } from '@radix-ui/react-checkbox' +import { CheckIcon } from '@status-im/icons' +import { styled } from '@tamagui/core' + +import type { GetVariants } from '../types' +import type { IconProps } from '@status-im/icons' +import type { ColorTokens } from '@tamagui/core' + +type Variants = GetVariants + +interface Props { + selected?: boolean + onCheckedChange?: (checked: boolean) => void + id: string + size?: 32 | 20 + variant?: Variants['variant'] +} + +const iconColor: Record = { + filled: '$neutral-50', + outline: '$white-100', +} + +const iconSize: Record = { + 32: 20, + 20: 16, +} + +const Checkbox = (props: Props) => { + const { id, selected, onCheckedChange, size = 20, variant = 'filled' } = props + + return ( + + + + + + ) +} + +export { Checkbox } +export type { Props as CheckboxProps } + +const Base = styled(Root, { + name: 'Checkbox', + tag: 'span', + accessibilityRole: 'checkbox', + + backgroundColor: 'transparent', + borderRadius: '$2', + + cursor: 'pointer', + animation: 'fast', + + height: 32, + width: 32, + borderWidth: 1, + + variants: { + size: { + 32: { + height: 32, + width: 32, + }, + 20: { + height: 20, + width: 20, + }, + }, + variant: { + filled: { + backgroundColor: '$neutral-20', + + hoverStyle: { backgroundColor: '$neutral-30' }, + pressStyle: { backgroundColor: '$neutral-30' }, + }, + outline: { + borderColor: '$neutral-20', + + hoverStyle: { borderColor: '$neutral-30' }, + pressStyle: { borderColor: '$neutral-30' }, + }, + }, + selected: { + filled: { + hoverStyle: { backgroundColor: '$primary-60' }, + pressStyle: { backgroundColor: '$primary-60' }, + }, + outline: { + backgroundColor: '$primary-50', + borderColor: '$primary-50', + + hoverStyle: { + backgroundColor: '$primary-60', + }, + pressStyle: { + backgroundColor: '$primary-60', + }, + }, + }, + + disabled: { + true: { + opacity: 0.3, + cursor: 'default', + }, + }, + } as const, +}) + +const Indicator = styled(_Indicator, { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + width: '100%', +}) diff --git a/packages/components/src/checkbox/index.tsx b/packages/components/src/checkbox/index.tsx new file mode 100644 index 00000000..11c849ff --- /dev/null +++ b/packages/components/src/checkbox/index.tsx @@ -0,0 +1 @@ +export { Checkbox } from './checkbox' diff --git a/packages/components/src/dropdown-menu/dropdown-menu.tsx b/packages/components/src/dropdown-menu/dropdown-menu.tsx index 53e5b511..a60cacd7 100644 --- a/packages/components/src/dropdown-menu/dropdown-menu.tsx +++ b/packages/components/src/dropdown-menu/dropdown-menu.tsx @@ -1,6 +1,7 @@ -import { cloneElement } from 'react' +import { cloneElement, forwardRef } from 'react' import { + CheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, @@ -8,8 +9,10 @@ import { Root, Trigger, } from '@radix-ui/react-dropdown-menu' -import { styled } from '@tamagui/core' +import { CheckIcon } from '@status-im/icons' +import { Stack, styled } from '@tamagui/core' +import { Checkbox } from '../checkbox' import { Text } from '../text' interface Props { @@ -32,28 +35,64 @@ const DropdownMenu = (props: Props) => { } interface DropdownMenuItemProps { - icon: React.ReactElement + icon?: React.ReactElement label: string onSelect: () => void + selected?: boolean danger?: boolean } const MenuItem = (props: DropdownMenuItemProps) => { - const { icon, label, onSelect, danger } = props + const { icon, label, onSelect, danger, selected } = props const iconColor = danger ? '$danger-50' : '$neutral-50' const textColor = danger ? '$danger-50' : '$neutral-100' return ( - {cloneElement(icon, { color: iconColor })} - - {label} - + + {icon && cloneElement(icon, { color: iconColor })} + + {label} + + + {selected && } ) } +interface DropdownMenuCheckboxItemProps { + icon?: React.ReactElement + label: string + onSelect: () => void + checked?: boolean + danger?: boolean +} + +const DropdownMenuCheckboxItem = forwardRef< + HTMLDivElement, + DropdownMenuCheckboxItemProps +>(function _DropdownMenuCheckboxItem(props, forwardedRef) { + const { checked, label, icon, onSelect } = props + + const handleSelect = (event: Event) => { + event.preventDefault() + onSelect() + } + + return ( + + + {icon && cloneElement(icon)} + + {label} + + + + + ) +}) + const Content = styled(DropdownMenuContent, { name: 'DropdownMenuContent', acceptsClassName: true, @@ -74,6 +113,32 @@ const ItemBase = styled(DropdownMenuItem, { display: 'flex', alignItems: 'center', + justifyContent: 'space-between', + + height: 32, + paddingHorizontal: 8, + borderRadius: '$10', + cursor: 'pointer', + userSelect: 'none', + gap: 8, + + hoverStyle: { + backgroundColor: '$neutral-5', + }, + + pressStyle: { + backgroundColor: '$neutral-10', + }, +}) + +const ItemBaseCheckbox = styled(CheckboxItem, { + name: 'DropdownMenuCheckboxItem', + acceptsClassName: true, + + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + height: 32, paddingHorizontal: 8, borderRadius: '$10', @@ -104,6 +169,7 @@ const Separator = styled(DropdownMenuSeparator, { DropdownMenu.Content = Content DropdownMenu.Item = MenuItem DropdownMenu.Separator = Separator +DropdownMenu.CheckboxItem = DropdownMenuCheckboxItem export { DropdownMenu } export type DropdownMenuProps = Omit diff --git a/packages/components/src/icon-button/icon-button.tsx b/packages/components/src/icon-button/icon-button.tsx index bd58de10..994c2ab9 100644 --- a/packages/components/src/icon-button/icon-button.tsx +++ b/packages/components/src/icon-button/icon-button.tsx @@ -1,6 +1,6 @@ import { cloneElement, forwardRef } from 'react' -import { Stack, styled } from 'tamagui' +import { Stack, styled } from '@tamagui/core' import { usePressableColors } from '../hooks/use-pressable-colors' @@ -93,8 +93,8 @@ const Base = styled(Stack, { outline: { backgroundColor: 'transparent', - borderColor: '$neutral-20', - hoverStyle: { borderColor: '$neutral-30' }, + borderColor: '$neutral-30', + hoverStyle: { borderColor: '$neutral-40' }, pressStyle: { borderColor: '$neutral-20', backgroundColor: '$neutral-10', diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx index ef945ee6..3e5e557d 100644 --- a/packages/components/src/index.tsx +++ b/packages/components/src/index.tsx @@ -2,11 +2,13 @@ export * from './anchor-actions' export * from './avatar' export * from './button' export * from './calendar' +export * from './checkbox' export * from './community' export * from './composer' export * from './context-tag' export * from './counter' export * from './dividers' +export * from './dropdown-menu' export * from './dynamic-button' export * from './gap-messages' export * from './icon-button' diff --git a/packages/components/src/input/input.stories.tsx b/packages/components/src/input/input.stories.tsx index 10ccabfb..8781318c 100644 --- a/packages/components/src/input/input.stories.tsx +++ b/packages/components/src/input/input.stories.tsx @@ -1,3 +1,7 @@ +import { useEffect, useState } from 'react' + +import { SearchIcon } from '@status-im/icons' + import { Input } from './input' import type { Meta, StoryObj } from '@storybook/react' @@ -14,7 +18,70 @@ type Story = StoryObj export const Primary: Story = { args: { placeholder: 'Type something...', - // children: 'Click me', + }, +} + +const InputSearch = () => { + const [value, setValue] = useState('') + + // limit input to 100 characters just for demo purposes + useEffect(() => { + if (value.length > 100) { + setValue(value.slice(0, 100)) + } + }, [value]) + + return ( + <> + } + onClear={() => setValue('')} + label="Search" + endLabel={`${value.length}/100`} + size={40} + button={{ + label: 'Confirm', + onPress: () => alert('Confirmed!'), + }} + /> + + ) +} + +const InputSearchMinimzed = () => { + const [value, setValue] = useState('') + + return ( + <> + } + onClear={() => setValue('')} + size={32} + direction="rtl" + variant="retractable" + /> + + ) +} + +export const Minimized: Story = { + render: () => , +} + +export const CompleteExample: Story = { + render: () => , +} + +export const WithError: Story = { + args: { + placeholder: 'Type something...', + error: true, }, } diff --git a/packages/components/src/input/input.tsx b/packages/components/src/input/input.tsx index 54034d72..101eac9e 100644 --- a/packages/components/src/input/input.tsx +++ b/packages/components/src/input/input.tsx @@ -1,62 +1,229 @@ -import { setupReactNative, styled } from '@tamagui/core' +import { cloneElement, forwardRef, useRef, useState } from 'react' + +import { composeRefs } from '@radix-ui/react-compose-refs' +import { ClearIcon } from '@status-im/icons' +import { setupReactNative, Stack, styled } from '@tamagui/core' import { focusableInputHOC } from '@tamagui/focusable' import { TextInput } from 'react-native' +import { Button } from '../button' +import { Text } from '../text' + import type { GetProps } from '@tamagui/core' +import type { Ref } from 'react' setupReactNative({ TextInput, }) -export const InputFrame = styled( +type Props = GetProps & { + button?: { + label: string + onPress: () => void + } + endLabel?: string + icon?: React.ReactElement + label?: string + onClear?: () => void + variant?: 'default' | 'retractable' + size?: 40 | 32 + error?: boolean + direction?: 'ltr' | 'rtl' +} + +const _Input = (props: Props, ref: Ref) => { + const { + button, + color = '$neutral-50', + endLabel, + error, + icon, + label, + onClear, + size = 40, + placeholder, + value, + direction = 'ltr', + variant = 'default', + ...rest + } = props + + const [isMinimized, setIsMinimized] = useState(variant === 'retractable') + + const isRetractable = variant === 'retractable' + + const inputRef = useRef(null) + + return ( + + {Boolean(label || endLabel) && ( + + {label && ( + + {label} + + )} + {endLabel && ( + + {endLabel} + + )} + + )} + { + event.stopPropagation() + event.preventDefault() + + if (isRetractable && isMinimized) { + setIsMinimized(false) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ref is not inferred correctly here + inputRef?.current?.focus() + } + }} + > + {icon ? ( + {cloneElement(icon, { color })} + ) : null} + { + if (!value && isRetractable && !isMinimized) { + setIsMinimized(true) + } + }} + {...rest} + /> + + {Boolean(onClear) && !!value && ( + + + + )} + {button && ( + + )} + + + + ) +} + +const Input = forwardRef(_Input) + +export { Input } +export type { Props as InputProps } + +const InputFrame = styled( TextInput, { tag: 'input', name: 'Input', - borderWidth: 1, outlineWidth: 0, - borderColor: 'rgba(0, 200, 0, 1)', + borderColor: '$neutral-20', - paddingHorizontal: 30, - color: 'hsla(218, 51%, 7%, 1)', + color: '$neutral-100', placeholderTextColor: '$placeHolderColor', backgroundColor: 'transparent', - height: 32, - borderRadius: '$12', - - animation: 'fast', - // this fixes a flex bug where it overflows container minWidth: 0, - hoverStyle: { - borderColor: '$beigeHover', - }, - - focusStyle: { - borderColor: '$blueHover', - }, - variants: { blurred: { true: { placeholderTextColor: '$placeHolderColorBlurred', }, }, - }, - - defaultVariants: { - blurred: '$false', - }, + } as const, }, { isInput: true, } ) -export type InputProps = GetProps +const InputBase = focusableInputHOC(InputFrame) -export const Input = focusableInputHOC(InputFrame) +const InputContainer = styled(Stack, { + name: 'InputContainer', + tag: 'div', + flexDirection: 'row', + alignItems: 'center', + gap: 8, + + borderWidth: 1, + borderColor: '$neutral-30', + + paddingHorizontal: 12, + + animation: 'fast', + width: '100%', + + hoverStyle: { + borderColor: '$neutral-40', + }, + + focusStyle: { + borderColor: '$neutral-40', + }, + + pressStyle: { + borderColor: '$neutral-40', + }, + + variants: { + size: { + 40: { + height: 40, + paddingHorizontal: 16, + borderRadius: '$12', + }, + 32: { + height: 32, + paddingHorizontal: 8, + borderRadius: '$10', + }, + }, + minimized: { + true: { + width: 32, + paddingHorizontal: 0, + paddingLeft: 5, + + cursor: 'pointer', + }, + }, + error: { + true: { + borderColor: '$danger-50-opa-40', + }, + }, + + disabled: { + true: { + opacity: 0.3, + cursor: 'default', + }, + }, + } as const, +}) diff --git a/packages/components/src/tag/tag.stories.tsx b/packages/components/src/tag/tag.stories.tsx index 1bd16fcd..6ddc331a 100644 --- a/packages/components/src/tag/tag.stories.tsx +++ b/packages/components/src/tag/tag.stories.tsx @@ -42,7 +42,7 @@ export const Default: Story = { - +