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 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 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
-
-
-
+
+
+
))}
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 = {
-
+