Merge pull request #416 from marcelines/feat/table-filters
[website]: Table filters
This commit is contained in:
commit
2b6ef64fdd
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
])}
|
||||
>
|
||||
<FloatingDesktop visible={visible} />
|
||||
|
|
|
@ -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 (
|
||||
<div className="overflow-hidden rounded-2xl border border-neutral-10">
|
||||
<div className="border-b border-neutral-10 bg-neutral-5 p-3">
|
||||
<Text size={15} weight="medium">
|
||||
784 Open
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-neutral-10">
|
||||
{issues.map(issue => (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`https://github.com/status-im/status-react/issues/${issue.id}`}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="medium">
|
||||
{issue.title}
|
||||
</Text>
|
||||
<Text size={13} color="$neutral-50">
|
||||
#9667 • Opened 2 days ago by slaedjenic
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-1">
|
||||
<Tag size={24} label="E:Syncing" color="$orange-50" />
|
||||
<Tag size={24} label="E:Wallet" color="$green-50" />
|
||||
<Tag size={24} label="Feature" color="$pink-50" />
|
||||
<Tag size={24} label="Web" color="$purple-50" />
|
||||
</div>
|
||||
|
||||
<Tag size={24} label="9435" />
|
||||
|
||||
<Avatar
|
||||
type="user"
|
||||
size={24}
|
||||
name="jkbktl"
|
||||
src="https://images.unsplash.com/photo-1552058544-f2b08422138a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1299&q=80"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<Button size={40} variant="outline">
|
||||
Show more 10
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<div
|
||||
className="rounded-full"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: `color-mix(in srgb, ${color} ${opacity}%, transparent)`,
|
||||
border: `1px solid ${color}`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { ColorCircle }
|
|
@ -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 <ColorCircle color={props.color} />
|
||||
}
|
||||
|
||||
if (!props.avatar) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (isAvatar(props.avatar)) {
|
||||
return <Avatar src={props.avatar} size={16} name={props.name} type="user" />
|
||||
}
|
||||
|
||||
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<number[]>([])
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const currentBreakpoint = useCurrentBreakpoint()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}>
|
||||
<Button
|
||||
size={32}
|
||||
variant="outline"
|
||||
iconAfter={
|
||||
<div
|
||||
className={`transition-transform ${
|
||||
isOpen ? 'rotate-180' : 'rotate-0'
|
||||
}`}
|
||||
>
|
||||
<DropdownIcon size={20} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={10}
|
||||
align={currentBreakpoint === '2xl' ? 'end' : 'start'}
|
||||
>
|
||||
<div className="p-2 px-1">
|
||||
<Input
|
||||
placeholder={placeholder || 'Search'}
|
||||
icon={<SearchIcon size={20} />}
|
||||
size={32}
|
||||
value={filterText}
|
||||
onChangeText={setFilterText}
|
||||
/>
|
||||
</div>
|
||||
{filteredData.map(filtered => {
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={filtered.id}
|
||||
icon={<RenderIcon {...filtered} />}
|
||||
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 && (
|
||||
<div className="flex flex-col items-center justify-center p-2 py-1">
|
||||
<Image
|
||||
className="pb-3 invert"
|
||||
alt="No results"
|
||||
src={'/assets/filters/empty.png'}
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className="pb-[2px]">
|
||||
<Text size={15} weight="semibold">
|
||||
No options found
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Text size={13}>
|
||||
We didn't find results that match your search
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { DropdownFilter }
|
||||
export type { Props as DropdownFilterProps }
|
|
@ -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<number>()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<div className="relative">
|
||||
<IconButton icon={<SortIcon size={20} />} variant="outline" />
|
||||
</div>
|
||||
<DropdownMenu.Content sideOffset={10} align="end">
|
||||
<div className="p-2">
|
||||
<Text size={13} color="$neutral-80">
|
||||
Sort by
|
||||
</Text>
|
||||
</div>
|
||||
{data.map(option => {
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
key={option.id}
|
||||
label={option.name}
|
||||
onSelect={() => {
|
||||
setSelectedValue(option.id)
|
||||
}}
|
||||
selected={selectedValue === option.id}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{data.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center p-2 py-1">
|
||||
<Image
|
||||
className="pb-3 invert"
|
||||
alt="No results"
|
||||
src={'/assets/filters/empty.png'}
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className="pb-[2px]">
|
||||
<Text size={15} weight="semibold">
|
||||
No options found
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Text size={13}>We didn't find any results</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { DropdownSort }
|
||||
export type { Props as DropdownSortProps }
|
|
@ -0,0 +1,3 @@
|
|||
export { DropdownFilter } from './dropdown-filter'
|
||||
export { DropdownSort } from './dropdown-sort'
|
||||
export { Tabs } from './tabs'
|
|
@ -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 (
|
||||
<div className="flex">
|
||||
<div className="flex items-center pr-4">
|
||||
<button
|
||||
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||
isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||
}`}
|
||||
onClick={() => setActiveTab('open')}
|
||||
>
|
||||
<OpenIcon size={20} color={isOpen ? '$neutral-100' : '$neutral-50'} />
|
||||
<span className="pl-1 text-[15px]">784 Open</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pr-3">
|
||||
<button
|
||||
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||
!isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||
}`}
|
||||
onClick={() => setActiveTab('closed')}
|
||||
>
|
||||
<DoneIcon
|
||||
size={20}
|
||||
color={!isOpen ? '$neutral-100' : '$neutral-50'}
|
||||
/>
|
||||
<span className="pl-1 text-[15px]">1012 Closed</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs }
|
|
@ -0,0 +1 @@
|
|||
export { TableIssues } from './table-issues'
|
|
@ -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: <ProfileIcon size={16} />,
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div className="overflow-hidden rounded-2xl border border-neutral-10">
|
||||
<div className="flex border-b border-neutral-10 bg-neutral-5 p-3">
|
||||
<div className="flex w-full flex-col 2xl:flex-row 2xl:justify-between">
|
||||
<Tabs />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center 2xl:justify-end">
|
||||
<div className="flex w-full justify-between pt-4 2xl:justify-end 2xl:pt-0">
|
||||
<div className="flex gap-2">
|
||||
<div className="transition-all">
|
||||
<Input
|
||||
placeholder="Find Author"
|
||||
icon={<SearchIcon size={20} />}
|
||||
size={32}
|
||||
value={issuesSearchText}
|
||||
onChangeText={setIssuesSearchText}
|
||||
variant="retractable"
|
||||
direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DropdownFilter
|
||||
data={authors}
|
||||
label="Author"
|
||||
placeholder="Find author"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={epics}
|
||||
label="Epics"
|
||||
placeholder="Find epic"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={labels}
|
||||
label="Labels"
|
||||
placeholder="Find label"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={assignees}
|
||||
label="Assignee"
|
||||
placeholder="Find assignee"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={repositories}
|
||||
label="Repos"
|
||||
placeholder="Find repo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pl-2">
|
||||
<DropdownSort data={sortOptions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-neutral-10">
|
||||
{issues.map(issue => (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`https://github.com/status-im/status-react/issues/${issue.id}`}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="medium">
|
||||
{issue.title}
|
||||
</Text>
|
||||
<Text size={13} color="$neutral-50">
|
||||
#9667 • Opened 2 days ago by slaedjenic
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-1">
|
||||
<Tag size={24} label="E:Syncing" color="$orange-50" />
|
||||
<Tag size={24} label="E:Wallet" color="$green-50" />
|
||||
<Tag size={24} label="Feature" color="$pink-50" />
|
||||
<Tag size={24} label="Web" color="$purple-50" />
|
||||
</div>
|
||||
|
||||
<Tag size={24} label="9435" />
|
||||
|
||||
<Avatar
|
||||
type="user"
|
||||
size={24}
|
||||
name="jkbktl"
|
||||
src="https://images.unsplash.com/photo-1552058544-f2b08422138a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1299&q=80"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<Button size={40} variant="outline">
|
||||
Show more 10
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { TableIssues }
|
|
@ -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<Breakpoint>('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
|
||||
}
|
|
@ -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 = () => {
|
|||
<div className="border-b border-neutral-10 px-5 py-3">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="px-10 py-6">
|
||||
<div className="border-b border-neutral-10 px-10 py-6">
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="border-b border-neutral-10 px-10 py-6">
|
||||
<div role="separator" className="-mx-6 my-6 h-px bg-neutral-10" />
|
||||
|
||||
<TableIssues />
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 = () => {
|
|||
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{repos.map(repo => (
|
||||
<Link
|
||||
key={repo.name}
|
||||
href={`https://github.com/status-im/${repo.name}`}
|
||||
className="flex h-[124px] flex-col rounded-2xl border border-neutral-10 px-4 py-3 transition-colors duration-200 hover:border-neutral-40"
|
||||
>
|
||||
<Text size={15} weight="semibold">
|
||||
{repo.name}
|
||||
</Text>
|
||||
<Text size={15} color="$neutral-50">
|
||||
{repo.description}
|
||||
</Text>
|
||||
<Shadow key={repo.name}>
|
||||
<Link
|
||||
href={`https://github.com/status-im/${repo.name}`}
|
||||
className="flex h-[124px] flex-col justify-between rounded-2xl border border-neutral-10 px-4 py-3 transition-colors duration-200 hover:border-neutral-40"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="semibold">
|
||||
{repo.name}
|
||||
</Text>
|
||||
<Text size={15} color="$neutral-50">
|
||||
{repo.description}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Text size={13} weight="medium" color="$neutral-50">
|
||||
Public
|
||||
</Text>
|
||||
<Text size={13} weight="medium" color="$neutral-50">
|
||||
42 Issues
|
||||
</Text>
|
||||
<Text size={13} weight="medium" color="$neutral-50">
|
||||
32
|
||||
</Text>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex gap-3 pt-4">
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<UnlockedIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
Public
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<OpenIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
42 issues
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<OpenIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
32
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 = () => {
|
|||
<div className="border-b border-neutral-10 px-5 py-3">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="px-10 py-6">
|
||||
<div className="border-b border-neutral-10 px-10 py-6">
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="border-b border-neutral-10 px-10 py-6">
|
||||
<div role="separator" className="-mx-6 my-6 h-px bg-neutral-10" />
|
||||
|
||||
<TableIssues />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import { Checkbox } from './checkbox'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
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<typeof Checkbox>
|
||||
|
||||
const CheckBoxWithHookFilled = () => {
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
id="checkbox"
|
||||
selected={checked}
|
||||
onCheckedChange={() => setChecked(!checked)}
|
||||
variant="filled"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckBoxWithHookOutlined = () => {
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
id="checkbox"
|
||||
selected={checked}
|
||||
onCheckedChange={() => setChecked(!checked)}
|
||||
variant="outline"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const Filled: Story = {
|
||||
render: () => {
|
||||
return <CheckBoxWithHookFilled />
|
||||
},
|
||||
}
|
||||
|
||||
export const Outlined: Story = {
|
||||
render: () => {
|
||||
return <CheckBoxWithHookOutlined />
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -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<typeof Base>
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
id: string
|
||||
size?: 32 | 20
|
||||
variant?: Variants['variant']
|
||||
}
|
||||
|
||||
const iconColor: Record<Variants['variant'], ColorTokens> = {
|
||||
filled: '$neutral-50',
|
||||
outline: '$white-100',
|
||||
}
|
||||
|
||||
const iconSize: Record<Variants['size'], IconProps['size']> = {
|
||||
32: 20,
|
||||
20: 16,
|
||||
}
|
||||
|
||||
const Checkbox = (props: Props) => {
|
||||
const { id, selected, onCheckedChange, size = 20, variant = 'filled' } = props
|
||||
|
||||
return (
|
||||
<Base
|
||||
id={id}
|
||||
selected={selected ? variant : undefined}
|
||||
size={size}
|
||||
onCheckedChange={onCheckedChange}
|
||||
variant={variant}
|
||||
checked={selected}
|
||||
>
|
||||
<Indicator>
|
||||
<CheckIcon size={iconSize[size]} color={iconColor[variant]} />
|
||||
</Indicator>
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
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%',
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export { Checkbox } from './checkbox'
|
|
@ -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 (
|
||||
<ItemBase onSelect={onSelect}>
|
||||
{cloneElement(icon, { color: iconColor })}
|
||||
<Text size={15} weight="medium" color={textColor}>
|
||||
{label}
|
||||
</Text>
|
||||
<Stack flexDirection="row" gap={8} alignItems="center">
|
||||
{icon && cloneElement(icon, { color: iconColor })}
|
||||
<Text size={15} weight="medium" color={textColor}>
|
||||
{label}
|
||||
</Text>
|
||||
</Stack>
|
||||
{selected && <CheckIcon size={20} color={iconColor} />}
|
||||
</ItemBase>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<ItemBaseCheckbox {...props} ref={forwardedRef} onSelect={handleSelect}>
|
||||
<Stack flexDirection="row" gap={8} alignItems="center">
|
||||
{icon && cloneElement(icon)}
|
||||
<Text size={15} weight="medium" color="$neutral-100">
|
||||
{label}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Checkbox id={label} selected={checked} variant="outline" />
|
||||
</ItemBaseCheckbox>
|
||||
)
|
||||
})
|
||||
|
||||
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<Props, 'children'>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<typeof Input>
|
|||
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 (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Please type something..."
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
icon={<SearchIcon size={20} />}
|
||||
onClear={() => setValue('')}
|
||||
label="Search"
|
||||
endLabel={`${value.length}/100`}
|
||||
size={40}
|
||||
button={{
|
||||
label: 'Confirm',
|
||||
onPress: () => alert('Confirmed!'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const InputSearchMinimzed = () => {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Please type something..."
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
icon={<SearchIcon size={20} />}
|
||||
onClear={() => setValue('')}
|
||||
size={32}
|
||||
direction="rtl"
|
||||
variant="retractable"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Minimized: Story = {
|
||||
render: () => <InputSearchMinimzed />,
|
||||
}
|
||||
|
||||
export const CompleteExample: Story = {
|
||||
render: () => <InputSearch />,
|
||||
}
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
placeholder: 'Type something...',
|
||||
error: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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<typeof InputFrame> & {
|
||||
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<TextInput>) => {
|
||||
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<TextInput>(null)
|
||||
|
||||
return (
|
||||
<Stack flexDirection={direction === 'ltr' ? 'row' : 'row-reverse'}>
|
||||
{Boolean(label || endLabel) && (
|
||||
<Stack flexDirection="row" justifyContent="space-between" pb={8}>
|
||||
{label && (
|
||||
<Text size={13} color="$neutral-50" weight="medium">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{endLabel && (
|
||||
<Text size={13} color="$neutral-50">
|
||||
{endLabel}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<InputContainer
|
||||
size={size}
|
||||
error={error}
|
||||
minimized={isMinimized}
|
||||
onPress={event => {
|
||||
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 ? (
|
||||
<Stack flexShrink={0}>{cloneElement(icon, { color })}</Stack>
|
||||
) : null}
|
||||
<InputBase
|
||||
value={value}
|
||||
placeholder={isMinimized && !value ? '' : placeholder}
|
||||
flex={1}
|
||||
ref={composeRefs(ref, inputRef)}
|
||||
onBlur={() => {
|
||||
if (!value && isRetractable && !isMinimized) {
|
||||
setIsMinimized(true)
|
||||
}
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
<Stack flexDirection="row" alignItems="center">
|
||||
{Boolean(onClear) && !!value && (
|
||||
<Stack
|
||||
role="button"
|
||||
accessibilityRole="button"
|
||||
pr={4}
|
||||
onPress={onClear}
|
||||
cursor="pointer"
|
||||
hoverStyle={{ opacity: 0.6 }}
|
||||
animation="fast"
|
||||
>
|
||||
<ClearIcon size={20} />
|
||||
</Stack>
|
||||
)}
|
||||
{button && (
|
||||
<Button onPress={button.onPress} variant="outline" size={24}>
|
||||
{button.label}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</InputContainer>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
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<typeof InputFrame>
|
||||
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,
|
||||
})
|
||||
|
|
|
@ -42,7 +42,7 @@ export const Default: Story = {
|
|||
<Tag label="New tag" size={24} disabled selected />
|
||||
<Tag label="New tag" size={24} color="#FF7D46" />
|
||||
|
||||
<Tag label="New tag #7140FD" size={24} color="#BA34F5" />
|
||||
<Tag label="New tag #BA34F5" size={24} color="#BA34F5" />
|
||||
<Tag label="New tag #7140FD" size={24} color="#7140FD" icon="🧙♂️" />
|
||||
|
||||
<Tag
|
||||
|
|
98
yarn.lock
98
yarn.lock
|
@ -4167,6 +4167,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/primitive@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
|
||||
integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-accordion@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz#fa1ab1b5c6a29aa75aefaf306a9e72fe3a482dbc"
|
||||
|
@ -4191,6 +4198,21 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
|
||||
integrity sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
|
||||
"@radix-ui/react-collapsible@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz#0583470c7caa8cd1ab6f606416288d19b3baf777"
|
||||
|
@ -4224,6 +4246,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
||||
|
@ -4231,6 +4260,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
|
||||
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-dialog@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.3.tgz#a715bf30f35fcd80476c0a07fcc073c1968e6d3e"
|
||||
|
@ -4412,6 +4448,15 @@
|
|||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-presence@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
|
||||
integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz#54e22f49ca59ba88d8143090276d50b93f8a7053"
|
||||
|
@ -4420,6 +4465,14 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
||||
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3"
|
||||
|
@ -4444,6 +4497,14 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
|
||||
"@radix-ui/react-slot@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
|
||||
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
|
||||
"@radix-ui/react-tabs@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.3.tgz#8b4158160a7c6633c893c74641e929d2708e709a"
|
||||
|
@ -4511,6 +4572,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
|
||||
integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-controllable-state@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
|
||||
|
@ -4519,6 +4587,14 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-controllable-state@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
|
||||
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
|
||||
"@radix-ui/react-use-escape-keydown@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz#09ab6455ab240b4f0a61faf06d4e5132c4d639f6"
|
||||
|
@ -4542,6 +4618,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-layout-effect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
|
||||
integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.0.tgz#e48a69c3a7d8078a967084038df66d0d181c56ac"
|
||||
|
@ -4549,6 +4632,13 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
|
||||
integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0"
|
||||
|
@ -4572,6 +4662,14 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-size@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
|
||||
integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-visually-hidden@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.2.tgz#29b117a59ef09a984bdad12cb98d81e8350be450"
|
||||
|
|
Loading…
Reference in New Issue