Merge branch 'next' into 'main'
|
@ -3,3 +3,6 @@
|
||||||
**/protos
|
**/protos
|
||||||
**/proto
|
**/proto
|
||||||
**/coverage
|
**/coverage
|
||||||
|
**/storybook-static
|
||||||
|
**/examples
|
||||||
|
**/packages/status-react
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"plugin:jsx-a11y/recommended",
|
"plugin:jsx-a11y/recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
@ -89,6 +90,7 @@
|
||||||
"alwaysTryTypes": true,
|
"alwaysTryTypes": true,
|
||||||
"project": ["tsconfig.base.json", "packages/*/tsconfig.json"]
|
"project": ["tsconfig.base.json", "packages/*/tsconfig.json"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"import/ignore": ["react-native"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
run: yarn typecheck
|
run: yarn typecheck
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint && yarn format:check
|
run: yarn lint && yarn format --check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
|
@ -79,3 +79,28 @@ node_modules/
|
||||||
|
|
||||||
# Vercel
|
# Vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# Tamagui
|
||||||
|
.tamagui
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
storybook-static
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
node_modules/
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
npm-debug.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
*.orig.*
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Tauri
|
||||||
|
**/src-tauri/target/
|
||||||
|
|
||||||
|
# Local
|
||||||
|
**/.data
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
**/dist
|
|
||||||
**/node_modules
|
|
||||||
.parcel-cache
|
|
||||||
.github
|
|
||||||
**/protos
|
|
||||||
**/coverage
|
|
||||||
.next
|
|
||||||
**/.data
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
|
@ -0,0 +1,27 @@
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
// prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
// to make use of `TAURI_DEBUG` and other env variables
|
||||||
|
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||||
|
envPrefix: ['VITE_', 'TAURI_'],
|
||||||
|
build: {
|
||||||
|
// Tauri supports es2021
|
||||||
|
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
|
||||||
|
// don't minify for debug builds
|
||||||
|
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||||
|
// produce sourcemaps for debug builds
|
||||||
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "desktop",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tauri dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rimraf node_modules .turbo"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^1.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
|
"@types/node": "^18.15.2",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "desktop"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.57"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tauri = { version = "1.2", features = ["api-all"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# by default Tauri runs in production mode
|
||||||
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||||
|
# DO NOT remove this
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 974 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 903 B |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"devPath": "http://localhost:5173",
|
||||||
|
"distDir": "../dist"
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "desktop",
|
||||||
|
"version": "0.0.0"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"copyright": "",
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"externalBin": [],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"identifier": "com.tauri.dev",
|
||||||
|
"longDescription": "",
|
||||||
|
"macOS": {
|
||||||
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
|
},
|
||||||
|
"resources": [],
|
||||||
|
"shortDescription": "",
|
||||||
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "desktop",
|
||||||
|
"width": 800,
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"titleBarStyle": "Overlay"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["_vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
// eslint-disable-next-line eslint-comments/disable-enable-pair
|
||||||
|
/* eslint-disable import/namespace */
|
||||||
|
import 'expo-dev-client'
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { useNavigation, useRoute } from '@react-navigation/native'
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
|
import { Heading, IconButton, Paragraph } from '@status-im/components'
|
||||||
|
import { Avatar } from '@status-im/components/src/avatar'
|
||||||
|
import { ArrowLeftIcon, MembersIcon } from '@status-im/icons/20'
|
||||||
|
import { Stack as View, TamaguiProvider } from '@tamagui/core'
|
||||||
|
import { useFonts } from 'expo-font'
|
||||||
|
import { Platform } from 'react-native'
|
||||||
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
|
import { AnimatePresence } from 'tamagui'
|
||||||
|
|
||||||
|
import { NavigationProvider } from './navigation/provider'
|
||||||
|
import { ChannelScreen } from './screens/channel'
|
||||||
|
import { HomeScreen } from './screens/home'
|
||||||
|
import tamaguiConfig from './tamagui.config'
|
||||||
|
|
||||||
|
import type { RouteProp } from '@react-navigation/native'
|
||||||
|
import type { HeaderBackButtonProps } from '@react-navigation/native-stack/lib/typescript/src/types'
|
||||||
|
|
||||||
|
export type RootStackParamList = {
|
||||||
|
Home: undefined
|
||||||
|
Channel: { channelId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<RootStackParamList>()
|
||||||
|
|
||||||
|
const CustomHeaderLeft = (props: HeaderBackButtonProps) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const route = useRoute<RouteProp<RootStackParamList, 'Channel'>>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
icon={<ArrowLeftIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
props.canGoBack && navigation.goBack()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Paragraph weight="semibold" marginLeft={12}>
|
||||||
|
# {route.params.channelId || 'channel'}
|
||||||
|
</Paragraph>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [position, setPosition] = useState(0)
|
||||||
|
|
||||||
|
const [loaded] = useFonts({
|
||||||
|
Inter: require('@tamagui/font-inter/otf/Inter-Medium.otf'),
|
||||||
|
InterBold: require('@tamagui/font-inter/otf/Inter-Bold.otf'),
|
||||||
|
// Tamagui does this for you on web, but you need to do it manually on native. Only for the demo. We should seek a better solution.
|
||||||
|
UbuntuMono: require('./assets/fonts/UbuntuMono.ttf'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const onScroll = event => {
|
||||||
|
if (event.nativeEvent.contentOffset.y > 90) {
|
||||||
|
setPosition(event.nativeEvent.contentOffset.y)
|
||||||
|
} else {
|
||||||
|
setPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMinimizedHeader = useMemo(() => {
|
||||||
|
return position > 90
|
||||||
|
}, [position])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TamaguiProvider config={tamaguiConfig} defaultTheme="light">
|
||||||
|
<NavigationProvider>
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<Stack.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
animation: 'slide_from_right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Home"
|
||||||
|
options={{
|
||||||
|
headerTransparent: true,
|
||||||
|
headerShadowVisible: false,
|
||||||
|
header: () => (
|
||||||
|
<View
|
||||||
|
height={100}
|
||||||
|
animation="fast"
|
||||||
|
backgroundColor={
|
||||||
|
showMinimizedHeader ? '$background' : 'transparent'
|
||||||
|
}
|
||||||
|
padding={16}
|
||||||
|
paddingTop={48}
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showMinimizedHeader && (
|
||||||
|
<View
|
||||||
|
key="header"
|
||||||
|
animation={[
|
||||||
|
'fast',
|
||||||
|
{
|
||||||
|
opacity: {
|
||||||
|
overshootClamping: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
opacity={1}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Heading color="$textPrimary">Rarible</Heading>
|
||||||
|
<Avatar
|
||||||
|
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
|
||||||
|
size={48}
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props => (
|
||||||
|
<HomeScreen
|
||||||
|
{...props}
|
||||||
|
onScroll={onScroll}
|
||||||
|
isMinimized={showMinimizedHeader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack.Screen>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Channel"
|
||||||
|
component={ChannelScreen}
|
||||||
|
options={{
|
||||||
|
headerBlurEffect: 'systemUltraThinMaterialLight',
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: Platform.select({
|
||||||
|
ios: 'transparent',
|
||||||
|
default: 'white',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
headerTransparent: true,
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerTitle: '',
|
||||||
|
headerLeft(props) {
|
||||||
|
return <CustomHeaderLeft {...props} />
|
||||||
|
},
|
||||||
|
headerRight() {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<MembersIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
// noop
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</NavigationProvider>
|
||||||
|
</TamaguiProvider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "mobile",
|
||||||
|
"slug": "status-poc",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"fallbackToCacheTimeout": 0
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": ["**/*"],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true,
|
||||||
|
"bundleIdentifier": "com.marcelines.statuspoc"
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"package": "com.marcelines.statuspoc"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"eas": {
|
||||||
|
"projectId": "e214cc8a-4e7e-4850-8a41-e280ca3a4469"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"owner": "marcelines"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true)
|
||||||
|
return {
|
||||||
|
presets: [['babel-preset-expo', { jsxRuntime: 'automatic' }]],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-plugin-module-resolver'),
|
||||||
|
{
|
||||||
|
root: ['../..'],
|
||||||
|
alias: {
|
||||||
|
// define aliases to shorten the import paths
|
||||||
|
|
||||||
|
'@status-im/components': '../../packages/components',
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.jsx', '.tsx', '.ios.js', '.android.js'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// if you want reanimated support
|
||||||
|
// 'react-native-reanimated/plugin',
|
||||||
|
...(process.env.EAS_BUILD_PLATFORM === 'android'
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
[
|
||||||
|
'@tamagui/babel-plugin',
|
||||||
|
{
|
||||||
|
components: ['@status-im/components'],
|
||||||
|
config: './tamagui.config.ts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'transform-inline-environment-variables',
|
||||||
|
{
|
||||||
|
include: 'TAMAGUI_TARGET',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 3.3.2"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {},
|
||||||
|
"simulator": {
|
||||||
|
"ios": {
|
||||||
|
"simulator": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { registerRootComponent } from 'expo'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
// the environment is set up appropriately
|
||||||
|
registerRootComponent(App)
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||||
|
/**
|
||||||
|
* @type {import('expo/metro-config')}
|
||||||
|
*/
|
||||||
|
const { getDefaultConfig } = require('@expo/metro-config')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const projectRoot = __dirname
|
||||||
|
const workspaceRoot = path.resolve(__dirname, '../..')
|
||||||
|
|
||||||
|
const config = getDefaultConfig(projectRoot)
|
||||||
|
|
||||||
|
config.watchFolders = [workspaceRoot]
|
||||||
|
config.resolver.nodeModulesPaths = [
|
||||||
|
path.resolve(projectRoot, 'node_modules'),
|
||||||
|
path.resolve(workspaceRoot, 'node_modules'),
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = config
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { NavigationContainer } from '@react-navigation/native'
|
||||||
|
|
||||||
|
export function NavigationProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return <NavigationContainer>{children}</NavigationContainer>
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "mobile",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "expo start -c",
|
||||||
|
"ios": "TAMAGUI_TARGET=native yarn expo run:ios",
|
||||||
|
"android": "TAMAGUI_TARGET=native yarn expo run:android",
|
||||||
|
"start": "expo start --dev-client",
|
||||||
|
"lint": "eslint screens",
|
||||||
|
"#typecheck": "tsc",
|
||||||
|
"clean": "rimraf node_modules .turbo"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.9",
|
||||||
|
"@react-navigation/native": "^6.1.2",
|
||||||
|
"@react-navigation/native-stack": "^6.9.8",
|
||||||
|
"@status-im/components": "*",
|
||||||
|
"expo": "~47.0.12",
|
||||||
|
"expo-constants": "^14.0.2",
|
||||||
|
"expo-dev-client": "^2.0.1",
|
||||||
|
"expo-linear-gradient": "^12.0.1",
|
||||||
|
"expo-splash-screen": "~0.17.5",
|
||||||
|
"expo-status-bar": "^1.4.2",
|
||||||
|
"expo-updates": "^0.15.6",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-native": "0.70.5",
|
||||||
|
"react-native-safe-area-context": "4.4.1",
|
||||||
|
"react-native-screens": "~3.18.0",
|
||||||
|
"react-native-svg": "^13.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.9",
|
||||||
|
"@expo/metro-config": "^0.3.21",
|
||||||
|
"@tamagui/babel-plugin": "1.7.7",
|
||||||
|
"@types/react-native": "~0.70.6",
|
||||||
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
import { Composer, Messages } from '@status-im/components'
|
||||||
|
import { Stack, useTheme } from '@tamagui/core'
|
||||||
|
import { StatusBar } from 'expo-status-bar'
|
||||||
|
import {
|
||||||
|
Keyboard,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
} from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { ScrollView } from 'tamagui'
|
||||||
|
|
||||||
|
import type { RootStackParamList } from '../App'
|
||||||
|
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
type ChannelScreenProps = NativeStackScreenProps<RootStackParamList, 'Channel'>
|
||||||
|
|
||||||
|
export const ChannelScreen = ({ route }: ChannelScreenProps) => {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// We need to get the channel name from the route params
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const _channelName = route.params.channelId
|
||||||
|
const scrollRef = useRef(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={{ height: '100%', flex: 1, backgroundColor: theme.background.val }}
|
||||||
|
>
|
||||||
|
<StatusBar style={'dark'} animated />
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollRef}
|
||||||
|
paddingHorizontal={12}
|
||||||
|
width="100%"
|
||||||
|
onContentSizeChange={() => scrollRef.current?.scrollToEnd()}
|
||||||
|
>
|
||||||
|
<Stack pt={insets.top + 60} pb={insets.bottom}>
|
||||||
|
<Messages />
|
||||||
|
</Stack>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
|
<Stack>
|
||||||
|
<Composer />
|
||||||
|
</Stack>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// eslint-disable-next-line eslint-comments/disable-enable-pair
|
||||||
|
/* eslint-disable import/namespace */
|
||||||
|
|
||||||
|
import { Sidebar } from '@status-im/components'
|
||||||
|
import { Stack } from '@tamagui/core'
|
||||||
|
import { StatusBar } from 'expo-status-bar'
|
||||||
|
import { ScrollView } from 'tamagui'
|
||||||
|
|
||||||
|
import type { RootStackParamList } from '../App'
|
||||||
|
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||||
|
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
|
||||||
|
|
||||||
|
type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'> & {
|
||||||
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
|
||||||
|
isMinimized?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HomeScreen = ({
|
||||||
|
navigation,
|
||||||
|
onScroll,
|
||||||
|
isMinimized,
|
||||||
|
}: HomeScreenProps) => {
|
||||||
|
const onChannelPress = (id: string) => {
|
||||||
|
navigation.navigate('Channel', { channelId: id })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack flex={1} backgroundColor="$background">
|
||||||
|
<StatusBar style={isMinimized ? 'dark' : 'light'} animated />
|
||||||
|
<ScrollView onScroll={onScroll} scrollEventThrottle={16} flex={1}>
|
||||||
|
<Stack pb={40}>
|
||||||
|
<Sidebar
|
||||||
|
name="Rarible"
|
||||||
|
description="Multichain community-centric NFT marketplace. Create, buy and sell your NFTs."
|
||||||
|
membersCount={123}
|
||||||
|
onChannelPress={onChannelPress}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ScrollView>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { config } from '@status-im/components'
|
||||||
|
|
||||||
|
export default config
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {},
|
||||||
|
"extends": "expo/tsconfig.base"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<title>Status</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "TAMAGUI_TARGET='web' vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@status-im/components": "*",
|
||||||
|
"@status-im/icons": "*",
|
||||||
|
"@tamagui/core": "1.7.7",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-native-web": "^0.18.12",
|
||||||
|
"use-resize-observer": "^9.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tamagui/vite-plugin": "1.7.7",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,148 @@
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnchorActions,
|
||||||
|
CHANNEL_GROUPS,
|
||||||
|
Composer,
|
||||||
|
Messages,
|
||||||
|
SidebarCommunity,
|
||||||
|
SidebarMembers,
|
||||||
|
Topbar,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppState,
|
||||||
|
} from '@status-im/components'
|
||||||
|
import useResizeObserver from 'use-resize-observer'
|
||||||
|
|
||||||
|
import { useScrollPosition } from './hooks/use-scroll-position'
|
||||||
|
|
||||||
|
const COMMUNITY = {
|
||||||
|
name: 'Rarible',
|
||||||
|
description:
|
||||||
|
'Multichain community-centric NFT marketplace. Create, buy and sell your NFTs.',
|
||||||
|
membersCount: 123,
|
||||||
|
imageSrc:
|
||||||
|
'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2264&q=80',
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProperty = (property: string, value: number) => {
|
||||||
|
document.documentElement.style.setProperty(property, `${value}px`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [loading /*, setLoading*/] = useState(false)
|
||||||
|
const [showMembers, setShowMembers] = useState(false)
|
||||||
|
|
||||||
|
// TODO: Use it to simulate loading
|
||||||
|
// useEffect(() => {
|
||||||
|
// setLoading(true)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setLoading(false)
|
||||||
|
// }, 2000)
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
const appState = useAppState()
|
||||||
|
const appDispatch = useAppDispatch()
|
||||||
|
|
||||||
|
// TODO: This should change based on the URL
|
||||||
|
const selectedChannel = useMemo(() => {
|
||||||
|
for (const { channels } of CHANNEL_GROUPS) {
|
||||||
|
for (const channel of channels) {
|
||||||
|
if (channel.id === appState.channelId) {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [appState.channelId])
|
||||||
|
|
||||||
|
const topbarRef = useRef<HTMLDivElement>(null)
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const composerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useResizeObserver<HTMLDivElement>({
|
||||||
|
ref: topbarRef,
|
||||||
|
onResize({ height }) {
|
||||||
|
updateProperty('--topbar-height', height!)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useResizeObserver<HTMLDivElement>({
|
||||||
|
ref: composerRef,
|
||||||
|
onResize({ height }) {
|
||||||
|
updateProperty('--composer-height', height!)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition({
|
||||||
|
ref: contentRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
contentRef.current!.scrollTop = contentRef.current!.scrollHeight
|
||||||
|
}, [selectedChannel])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="app">
|
||||||
|
<div id="sidebar-community" style={{ zIndex: 200 }}>
|
||||||
|
<SidebarCommunity
|
||||||
|
community={COMMUNITY}
|
||||||
|
selectedChannelId={appState.channelId}
|
||||||
|
onChannelPress={channelId =>
|
||||||
|
appDispatch({ type: 'set-channel', channelId })
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<div id="topbar" ref={topbarRef}>
|
||||||
|
<Topbar
|
||||||
|
blur={scrollPosition !== 'top'}
|
||||||
|
channel={selectedChannel!}
|
||||||
|
showMembers={showMembers}
|
||||||
|
onMembersPress={() => setShowMembers(show => !show)}
|
||||||
|
pinnedMessages={[
|
||||||
|
{
|
||||||
|
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
|
||||||
|
reactions: {},
|
||||||
|
pinned: true,
|
||||||
|
id: '1234-1234',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam.',
|
||||||
|
reactions: {},
|
||||||
|
pinned: true,
|
||||||
|
id: '4321-4321',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content" ref={contentRef}>
|
||||||
|
<div id="messages">
|
||||||
|
<Messages loading={loading} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading === false && (
|
||||||
|
<div id="composer" ref={composerRef}>
|
||||||
|
{scrollPosition !== 'bottom' && (
|
||||||
|
<div id="anchor-actions">
|
||||||
|
<AnchorActions />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Composer blur={scrollPosition !== 'bottom'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{showMembers && (
|
||||||
|
<div id="sidebar-members">
|
||||||
|
<SidebarMembers />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type Position = 'top' | 'middle' | 'bottom'
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
ref: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScrollPosition(options: Options) {
|
||||||
|
const { ref } = options
|
||||||
|
|
||||||
|
const [position, setPosition] = useState<Position>('bottom')
|
||||||
|
const positionRef = useRef(position)
|
||||||
|
|
||||||
|
// Using ref for storing position because don't want to recreate the event listener
|
||||||
|
positionRef.current = position
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current!
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = node
|
||||||
|
|
||||||
|
if (scrollTop === 0) {
|
||||||
|
setPosition('top')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollTop + clientHeight === scrollHeight) {
|
||||||
|
setPosition('bottom')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionRef.current !== 'middle') {
|
||||||
|
setPosition('middle')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
node.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [ref])
|
||||||
|
|
||||||
|
return position
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import '../styles/reset.css'
|
||||||
|
import '../styles/app.css'
|
||||||
|
import '@tamagui/core/reset.css'
|
||||||
|
import '@tamagui/font-inter/css/400.css'
|
||||||
|
import '@tamagui/font-inter/css/700.css'
|
||||||
|
|
||||||
|
import { StrictMode } from 'react'
|
||||||
|
|
||||||
|
import { Provider, ToastContainer } from '@status-im/components'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
import App from './app'
|
||||||
|
|
||||||
|
const root = document.getElementById('root') as HTMLElement
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Provider>
|
||||||
|
<App />
|
||||||
|
<ToastContainer />
|
||||||
|
</Provider>
|
||||||
|
</StrictMode>
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,93 @@
|
||||||
|
:root {
|
||||||
|
--topbar-height: 56px;
|
||||||
|
--composer-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::selection {
|
||||||
|
color: #fff;
|
||||||
|
background: hsla(229, 71%, 57%, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
isolation: isolate;
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 352px 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-community {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-members {
|
||||||
|
width: 352px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: var(--topbar-height);
|
||||||
|
padding-bottom: var(--composer-height);
|
||||||
|
height: 100vh;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#anchor-actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
transform: translateY(calc(-100% - 12px));
|
||||||
|
}
|
||||||
|
|
||||||
|
#composer {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
#app {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
1. Use a more-intuitive box-sizing model.
|
||||||
|
*/
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
2. Remove default margin
|
||||||
|
*/
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
3. Allow percentage-based heights in the application
|
||||||
|
*/
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior-y: none; /* not working on Safari */
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Typographic tweaks!
|
||||||
|
4. Add accessible line-height
|
||||||
|
5. Improve text rendering
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
6. Improve media defaults
|
||||||
|
*/
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
7. Remove built-in form typography styles
|
||||||
|
*/
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
8. Avoid text overflows
|
||||||
|
*/
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
9. Create a root stacking context
|
||||||
|
*/
|
||||||
|
#root,
|
||||||
|
#__next {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Temporary testing purposes of keyboard navigation */
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid crimson;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { config } from '@status-im/components'
|
||||||
|
|
||||||
|
export default config
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"module": "esnext",
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { tamaguiPlugin } from '@tamagui/vite-plugin'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import path from 'path'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
import type { PluginOption } from 'vite'
|
||||||
|
|
||||||
|
process.env.TAMAGUI_TARGET = 'web'
|
||||||
|
process.env.TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD = '1'
|
||||||
|
|
||||||
|
const tamaguiConfig = {
|
||||||
|
components: ['@status-im/components'],
|
||||||
|
config: './tamagui.config.ts',
|
||||||
|
// useReactNativeWebLite: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see: https://vitejs.dev/config
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
// mainFields: ['module', 'jsnext:main', 'jsnext'],
|
||||||
|
alias: {
|
||||||
|
'@status-im/components/hooks': path.resolve(
|
||||||
|
'../../packages/components/hooks'
|
||||||
|
),
|
||||||
|
'@status-im/components': path.resolve('../../packages/components/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
TAMAGUI_TARGET: JSON.stringify('web'),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tamaguiPlugin(tamaguiConfig) as PluginOption,
|
||||||
|
// tamaguiExtractPlugin(tamaguiConfig)
|
||||||
|
],
|
||||||
|
})
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 3.4.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal",
|
||||||
|
"ios": {
|
||||||
|
"resourceClass": "m1-medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal",
|
||||||
|
"ios": {
|
||||||
|
"resourceClass": "m1-medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"ios": {
|
||||||
|
"resourceClass": "m1-medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,13 +12,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@status-im/react": "^0.1.1",
|
"@status-im/react": "^0.1.1",
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
"react": "^16.8.0 || ^17.0.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^16.8.0 || ^17.0.0",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^16.8.0 || ^17.0.0",
|
"@types/react-dom": "^18.0.11",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.9.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
|
|
|
@ -10,14 +10,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@status-im/react": "^0.1.1",
|
"@status-im/react": "^0.1.1",
|
||||||
"react": "^17.0.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^3.1.7"
|
"vite": "^3.1.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
70
package.json
|
@ -1,45 +1,57 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": {
|
||||||
"packages/*",
|
"packages": [
|
||||||
"examples/*"
|
"packages/status-js",
|
||||||
],
|
"packages/components",
|
||||||
|
"packages/icons",
|
||||||
|
"apps/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"test": "turbo run test --filter=@status-im/* -- --run",
|
"test": "turbo run test --filter=@status-im/* -- --run",
|
||||||
"dev": "turbo run dev --parallel --filter=@status-im/*",
|
"dev": "turbo run dev --filter=@status-im/* --parallel",
|
||||||
"build": "turbo run build --filter=@status-im/*",
|
"build": "turbo run build --filter=@status-im/*",
|
||||||
"typecheck": "turbo run typecheck --filter=@status-im/*",
|
|
||||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/.eslint-cache .",
|
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/.eslint-cache .",
|
||||||
"format": "prettier --cache --write .",
|
"typecheck": "turbo run typecheck",
|
||||||
"format:check": "prettier --check .",
|
"format": "prettier --ignore-path .gitignore --cache --write .",
|
||||||
"clean": "turbo run clean && rm -rf node_modules"
|
"clean": "turbo run clean && rimraf node_modules",
|
||||||
|
"web": "yarn workspace web dev",
|
||||||
|
"mobile": "yarn workspace mobile dev",
|
||||||
|
"desktop": "yarn workspace desktop dev",
|
||||||
|
"storybook": "yarn workspace @status-im/components storybook"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "18.0.28",
|
||||||
|
"@types/react-dom": "18.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.23.0",
|
"@changesets/cli": "^2.23.0",
|
||||||
"@tsconfig/strictest": "^1.0.1",
|
"@tsconfig/strictest": "^1.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
"@typescript-eslint/parser": "^5.12.0",
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
"eslint": "^8.9.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.7.0",
|
||||||
"eslint-import-resolver-node": "^0.3.6",
|
"eslint-import-resolver-node": "^0.3.7",
|
||||||
"eslint-import-resolver-typescript": "^3.5.0",
|
"eslint-import-resolver-typescript": "^3.5.3",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-react": "^7.27.0",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^12.3.4",
|
"lint-staged": "^13.2.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.4",
|
||||||
"turbo": "^1.3.1",
|
"rimraf": "^4.4.0",
|
||||||
"typescript": "^4.5.5",
|
"turbo": "^1.8.3",
|
||||||
"vite": "^2.9.12",
|
"typescript": "^4.9.5",
|
||||||
"vite-node": "^0.16.0",
|
"vite": "^4.1.4",
|
||||||
"vitest": "^0.16.0"
|
"vite-node": "^0.29.2",
|
||||||
|
"vitest": "^0.29.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,jsx}": [
|
"*.{ts,tsx,js,jsx}": [
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
THUMBS_DB
|
||||||
|
node_modules/
|
||||||
|
types/
|
|
@ -0,0 +1,12 @@
|
||||||
|
/* Animation for skeleton placeholder */
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { StorybookConfig } from '@storybook/types'
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'storybook-addon-designs',
|
||||||
|
'storybook-dark-mode',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script>
|
||||||
|
window.global = window
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Provider, ToastContainer } from '../src'
|
||||||
|
import { Parameters, Decorator } from '@storybook/react'
|
||||||
|
|
||||||
|
import './reset.css'
|
||||||
|
import './components.css'
|
||||||
|
|
||||||
|
export const parameters: Parameters = {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const withThemeProvider: Decorator = (Story, _context) => {
|
||||||
|
return (
|
||||||
|
<Provider>
|
||||||
|
<Story />
|
||||||
|
<ToastContainer />
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export const decorators = [withThemeProvider]
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
1. Use a more-intuitive box-sizing model.
|
||||||
|
*/
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
2. Remove default margin
|
||||||
|
*/
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
3. Allow percentage-based heights in the application
|
||||||
|
*/
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior-y: none; /* not working on Safari */
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Typographic tweaks!
|
||||||
|
4. Add accessible line-height
|
||||||
|
5. Improve text rendering
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
6. Improve media defaults
|
||||||
|
*/
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
7. Remove built-in form typography styles
|
||||||
|
*/
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
8. Avoid text overflows
|
||||||
|
*/
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
9. Create a root stacking context
|
||||||
|
*/
|
||||||
|
#root,
|
||||||
|
#__next {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
10. Remove user selection on buttons
|
||||||
|
*/
|
||||||
|
button {
|
||||||
|
user-select: none;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { useImageUpload } from './use-image-uploader'
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
interface UseImageUploadReturn {
|
||||||
|
imagesData: string[]
|
||||||
|
handleImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
handleImageRemove: (index: number) => void
|
||||||
|
imageUploaderInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
isDisabled: boolean
|
||||||
|
}
|
||||||
|
const ALLOWED_EXTENSIONS = /(\.jpg|\.jpeg|\.png)$/i
|
||||||
|
const IMAGES_LIMIT = 6
|
||||||
|
|
||||||
|
const useImageUpload = (): UseImageUploadReturn => {
|
||||||
|
const [imagesData, setImagesData] = useState<string[]>([])
|
||||||
|
const imageUploaderInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleImageUpload = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const files = event.target.files
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredFiles = [...files].filter(file =>
|
||||||
|
ALLOWED_EXTENSIONS.test(file.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Show alert if some files have unsupported formats
|
||||||
|
if (files.length > filteredFiles.length) {
|
||||||
|
return alert(
|
||||||
|
`Some files have unsupported formats. Only .jpg, .jpeg and .png formats are supported.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length > IMAGES_LIMIT || imagesData.length > IMAGES_LIMIT) {
|
||||||
|
return alert(
|
||||||
|
`You can upload only ${IMAGES_LIMIT} images. Please remove some files and try again.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newImagesData: string[] = await Promise.all(
|
||||||
|
filteredFiles.map(async file => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
resolve(reader.result as string)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
setImagesData(prevState => [...prevState, ...newImagesData])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageRemove = (index: number) => {
|
||||||
|
// Reset input value to trigger onChange event
|
||||||
|
imageUploaderInputRef.current!.value = ''
|
||||||
|
setImagesData(prevState => prevState.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imagesData,
|
||||||
|
handleImageUpload,
|
||||||
|
handleImageRemove,
|
||||||
|
imageUploaderInputRef,
|
||||||
|
isDisabled: imagesData.length >= IMAGES_LIMIT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useImageUpload }
|
|
@ -0,0 +1,71 @@
|
||||||
|
{
|
||||||
|
"name": "@status-im/components",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"sideEffects": [
|
||||||
|
"*.css"
|
||||||
|
],
|
||||||
|
"private": true,
|
||||||
|
"#module": "./src/index.tsx",
|
||||||
|
"types": "./src/index.tsx",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"#types": "./dist/types/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"types",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "TAMAGUI_TARGET='web' vite build --watch --mode development",
|
||||||
|
"build": "TAMAGUI_TARGET='web' vite build",
|
||||||
|
"postbuild": "yarn build:types",
|
||||||
|
"build:types": "tsc --noEmit false --emitDeclarationOnly || true",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"typecheck": "tsc",
|
||||||
|
"storybook": "TAMAGUI_TARGET='web' storybook dev -p 3001",
|
||||||
|
"storybook:build": "TAMAGUI_TARGET='web' storybook build",
|
||||||
|
"clean": "rimraf node_modules dist .turbo storybook-static"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-web": "^0.18.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
|
"@radix-ui/react-toast": "^1.1.3",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
|
"@status-im/icons": "*",
|
||||||
|
"@tamagui/animations-css": "1.7.7",
|
||||||
|
"@tamagui/animations-react-native": "1.7.7",
|
||||||
|
"@tamagui/core": "1.7.7",
|
||||||
|
"@tamagui/font-inter": "1.7.7",
|
||||||
|
"@tamagui/react-native-media-driver": "1.7.7",
|
||||||
|
"@tamagui/shorthands": "1.7.7",
|
||||||
|
"@tamagui/theme-base": "1.7.7",
|
||||||
|
"expo-blur": "^12.2.2",
|
||||||
|
"expo-linear-gradient": "^12.1.2",
|
||||||
|
"tamagui": "1.7.7",
|
||||||
|
"zustand": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-essentials": "7.0.0-beta.21",
|
||||||
|
"@storybook/addon-interactions": "7.0.0-beta.21",
|
||||||
|
"@storybook/addon-links": "7.0.0-beta.21",
|
||||||
|
"@storybook/blocks": "7.0.0-beta.21",
|
||||||
|
"@storybook/react": "7.0.0-beta.21",
|
||||||
|
"@storybook/react-vite": "7.0.0-beta.21",
|
||||||
|
"@storybook/testing-library": "^0.0.13",
|
||||||
|
"@tamagui/vite-plugin": "1.7.7",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-native-svg": "^13.8.0",
|
||||||
|
"react-native-web": "^0.18.12",
|
||||||
|
"storybook": "7.0.0-beta.21",
|
||||||
|
"storybook-addon-designs": "7.0.0-beta.2",
|
||||||
|
"storybook-dark-mode": "^2.1.1",
|
||||||
|
"vite": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
|
import { DynamicButton } from '../dynamic-button'
|
||||||
|
|
||||||
|
// type Props = {}
|
||||||
|
|
||||||
|
const AnchorActions = () => {
|
||||||
|
return (
|
||||||
|
<Stack flexDirection="row" space={8}>
|
||||||
|
<DynamicButton type="mention" count={1} />
|
||||||
|
<DynamicButton type="notification" count={0} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AnchorActions }
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createAnimations } from '@tamagui/animations-react-native'
|
||||||
|
|
||||||
|
export const animations = createAnimations({
|
||||||
|
fast: {
|
||||||
|
damping: 20,
|
||||||
|
mass: 1.2,
|
||||||
|
stiffness: 250,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
damping: 10,
|
||||||
|
mass: 0.9,
|
||||||
|
stiffness: 100,
|
||||||
|
},
|
||||||
|
slow: {
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 60,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { createAnimations } from '@tamagui/animations-css'
|
||||||
|
|
||||||
|
export const animations = createAnimations({
|
||||||
|
fast: 'ease-in 150ms',
|
||||||
|
medium: 'ease-in 300ms',
|
||||||
|
slow: 'ease-in 450ms',
|
||||||
|
})
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
|
import { Author } from './author'
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Author> = {
|
||||||
|
component: Author,
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
name: 'Alisher Yakupov',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=3155%3A49848&t=87Ziud3PyYYSvsRg-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render: args => (
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author {...args} />
|
||||||
|
<Author {...args} status="verified" />
|
||||||
|
<Author {...args} status="untrustworthy" />
|
||||||
|
<Author {...args} status="contact" />
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Author>
|
||||||
|
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
args: {},
|
||||||
|
render: args => (
|
||||||
|
<Stack gap={20}>
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author {...args} />
|
||||||
|
<Author {...args} status="verified" />
|
||||||
|
<Author {...args} status="untrustworthy" />
|
||||||
|
<Author {...args} status="contact" />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author {...args} address="zQ3...9d4Gs0" />
|
||||||
|
<Author {...args} status="verified" address="zQ3...9d4Gs0" />
|
||||||
|
<Author {...args} status="untrustworthy" address="zQ3...9d4Gs0" />
|
||||||
|
<Author {...args} status="contact" address="zQ3...9d4Gs0" />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author {...args} address="zQ3...9d4Gs0" time="09:30" />
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="verified"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="untrustworthy"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="contact"
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="verified"
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="untrustworthy"
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="contact"
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
size={15}
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="verified"
|
||||||
|
size={15}
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="untrustworthy"
|
||||||
|
size={15}
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
<Author
|
||||||
|
{...args}
|
||||||
|
status="contact"
|
||||||
|
size={15}
|
||||||
|
nickname="alisher.eth"
|
||||||
|
address="zQ3...9d4Gs0"
|
||||||
|
time="09:30"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
ContactIcon,
|
||||||
|
UntrustworthyIcon,
|
||||||
|
VerifiedIcon,
|
||||||
|
} from '@status-im/icons/12'
|
||||||
|
import { XStack } from 'tamagui'
|
||||||
|
|
||||||
|
import { Text } from '../text'
|
||||||
|
|
||||||
|
import type { TextProps } from '../text'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
size?: Extract<TextProps['size'], 13 | 15>
|
||||||
|
nickname?: string
|
||||||
|
status?: 'verified' | 'untrustworthy' | 'contact'
|
||||||
|
address?: string
|
||||||
|
time?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Author = (props: Props) => {
|
||||||
|
const { name, size = 13, nickname, status, address, time } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XStack space={8} alignItems="center">
|
||||||
|
<XStack gap={4} alignItems="center">
|
||||||
|
<Text size={size} weight="semibold">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{nickname && (
|
||||||
|
<Text size={11} color="$neutral-60">
|
||||||
|
· {nickname}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{status === 'contact' && <ContactIcon />}
|
||||||
|
{status === 'verified' && <VerifiedIcon />}
|
||||||
|
{status === 'untrustworthy' && <UntrustworthyIcon />}
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
{(address || time) && (
|
||||||
|
<XStack gap={4} alignItems="center">
|
||||||
|
{address && (
|
||||||
|
<Text size={11} color="$neutral-50" type="monospace">
|
||||||
|
{address}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{time && (
|
||||||
|
<Text size={11} color="$neutral-50">
|
||||||
|
· {time}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</XStack>
|
||||||
|
)}
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Author }
|
||||||
|
export type { Props as AuthorProps }
|
|
@ -0,0 +1 @@
|
||||||
|
export { Author } from './author'
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Stack } from '@tamagui/core'
|
||||||
|
|
||||||
|
import { Avatar } from './avatar'
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
|
const meta: Meta<typeof Avatar> = {
|
||||||
|
component: Avatar,
|
||||||
|
argTypes: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Avatar>
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
src: 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&h=500&q=80',
|
||||||
|
},
|
||||||
|
render: args => (
|
||||||
|
<Stack space flexDirection="row">
|
||||||
|
<Stack space>
|
||||||
|
<Avatar {...args} size={80} />
|
||||||
|
<Avatar {...args} size={56} />
|
||||||
|
<Avatar {...args} size={48} />
|
||||||
|
<Avatar {...args} size={32} />
|
||||||
|
<Avatar {...args} size={28} />
|
||||||
|
<Avatar {...args} size={24} />
|
||||||
|
<Avatar {...args} size={20} />
|
||||||
|
<Avatar {...args} size={16} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack space>
|
||||||
|
<Avatar {...args} size={80} indicator="online" />
|
||||||
|
<Avatar {...args} size={56} indicator="online" />
|
||||||
|
<Avatar {...args} size={48} indicator="online" />
|
||||||
|
<Avatar {...args} size={32} indicator="online" />
|
||||||
|
<Avatar {...args} size={28} indicator="online" />
|
||||||
|
<Avatar {...args} size={24} indicator="online" />
|
||||||
|
<Avatar {...args} size={20} indicator="online" />
|
||||||
|
<Avatar {...args} size={16} indicator="online" />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Rounded: Story = {
|
||||||
|
args: {
|
||||||
|
src: 'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&h=500&q=80',
|
||||||
|
shape: 'rounded',
|
||||||
|
},
|
||||||
|
render: args => (
|
||||||
|
<Stack space>
|
||||||
|
<Avatar {...args} size={80} />
|
||||||
|
<Avatar {...args} size={56} />
|
||||||
|
<Avatar {...args} size={48} />
|
||||||
|
<Avatar {...args} size={32} />
|
||||||
|
<Avatar {...args} size={28} />
|
||||||
|
<Avatar {...args} size={24} />
|
||||||
|
<Avatar {...args} size={20} />
|
||||||
|
<Avatar {...args} size={16} />
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
|
@ -0,0 +1,196 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Stack, styled, Text, Unspaced } from '@tamagui/core'
|
||||||
|
|
||||||
|
import { Image } from '../image'
|
||||||
|
|
||||||
|
import type { GetStyledVariants } from '@tamagui/core'
|
||||||
|
|
||||||
|
type Variants = GetStyledVariants<typeof Base>
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
src: string
|
||||||
|
size: 80 | 56 | 48 | 32 | 28 | 24 | 20 | 16
|
||||||
|
shape?: Variants['shape']
|
||||||
|
outline?: Variants['outline']
|
||||||
|
indicator?: GetStyledVariants<typeof Indicator>['state']
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error'
|
||||||
|
|
||||||
|
const Avatar = (props: Props) => {
|
||||||
|
const {
|
||||||
|
src,
|
||||||
|
size,
|
||||||
|
shape = 'circle',
|
||||||
|
outline = false,
|
||||||
|
indicator = 'none',
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<ImageLoadingStatus>('idle')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus('idle')
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base size={size} shape={shape} outline={outline}>
|
||||||
|
{indicator !== 'none' && (
|
||||||
|
<Unspaced>
|
||||||
|
<Indicator size={size} state={indicator} />
|
||||||
|
</Unspaced>
|
||||||
|
)}
|
||||||
|
<Shape shape={shape}>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
width="full"
|
||||||
|
aspectRatio={1}
|
||||||
|
onLoad={() => setStatus('loaded')}
|
||||||
|
onError={() => setStatus('error')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<Fallback
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
PP
|
||||||
|
</Fallback>
|
||||||
|
)}
|
||||||
|
</Shape>
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar }
|
||||||
|
export type { Props as AvatarProps }
|
||||||
|
|
||||||
|
const Base = styled(Stack, {
|
||||||
|
name: 'Avatar',
|
||||||
|
|
||||||
|
position: 'relative',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
// defined in Avatar props
|
||||||
|
size: {
|
||||||
|
'...': (size: number) => {
|
||||||
|
return {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
shape: {
|
||||||
|
circle: {
|
||||||
|
borderRadius: 80, // big enough to cover all sizes
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
true: {
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '$white-100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
const Shape = styled(Stack, {
|
||||||
|
name: 'AvatarShape',
|
||||||
|
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: '$white-100',
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
shape: {
|
||||||
|
circle: {
|
||||||
|
borderRadius: 80, // big enough to cover all sizes
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Indicator = styled(Stack, {
|
||||||
|
name: 'AvatarIndicator',
|
||||||
|
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 2,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '$white-100',
|
||||||
|
borderRadius: 10,
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
80: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
bottom: 4,
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
56: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
bottom: 2,
|
||||||
|
right: 2,
|
||||||
|
},
|
||||||
|
48: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
32: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
},
|
||||||
|
28: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
right: -2,
|
||||||
|
bottom: -2,
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
16: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
state: {
|
||||||
|
none: {},
|
||||||
|
online: {
|
||||||
|
backgroundColor: '$success-50',
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
backgroundColor: '$neutral-40',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
const Fallback = styled(Text, {
|
||||||
|
name: 'AvatarFallback',
|
||||||
|
})
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { LockedIcon, UnlockedIcon } from '@status-im/icons/12'
|
||||||
|
import { type ColorTokens, Stack, styled, Text } from '@tamagui/core'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
emoji: string
|
||||||
|
color?: ColorTokens
|
||||||
|
background?: ColorTokens
|
||||||
|
size: 32 | 24 | 20
|
||||||
|
lock?: 'locked' | 'unlocked' | 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiSizes: Record<Props['size'], number> = {
|
||||||
|
32: 14,
|
||||||
|
24: 13,
|
||||||
|
20: 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=399-20709&t=kX5LC5OYFnSF8BiZ-11
|
||||||
|
const ChannelAvatar = (props: Props) => {
|
||||||
|
const { emoji, background = '$blue-50-opa-20', size, lock = 'none' } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base size={size} backgroundColor={background}>
|
||||||
|
{lock !== 'none' && (
|
||||||
|
<LockBase variant={size}>
|
||||||
|
{lock === 'locked' ? <LockedIcon /> : <UnlockedIcon />}
|
||||||
|
</LockBase>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text fontSize={emojiSizes[size]}>{emoji}</Text>
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChannelAvatar }
|
||||||
|
export type { Props as ChannelAvatarProps }
|
||||||
|
|
||||||
|
const Base = styled(Stack, {
|
||||||
|
position: 'relative',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
'...': (size: number) => {
|
||||||
|
return {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const LockBase = styled(Stack, {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
backgroundColor: '$white-100',
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: 16,
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
32: {
|
||||||
|
right: -4,
|
||||||
|
bottom: -4,
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
right: -4,
|
||||||
|
bottom: -4,
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
right: -6,
|
||||||
|
bottom: -6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { cloneElement } from 'react'
|
||||||
|
|
||||||
|
import { type ColorTokens, Stack, styled } from '@tamagui/core'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactElement
|
||||||
|
backgroundColor?: ColorTokens
|
||||||
|
color?: ColorTokens
|
||||||
|
size?: 20 | 32 | 48
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconAvatar = (props: Props) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
color = '$blue-50',
|
||||||
|
backgroundColor = '$blue-50-opa-20',
|
||||||
|
size = 32,
|
||||||
|
} = props
|
||||||
|
return (
|
||||||
|
<Base backgroundColor={backgroundColor} size={size}>
|
||||||
|
{cloneElement(children, { color })}
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Base = styled(Stack, {
|
||||||
|
borderRadius: 80,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
'...': (size: number) => {
|
||||||
|
return {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { IconAvatar }
|
||||||
|
export type { Props as IconAvatarProps }
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './avatar'
|
||||||
|
export * from './channel-avatar'
|
||||||
|
export * from './icon-avatar'
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { AlertIcon, PinIcon, RecentIcon } from '@status-im/icons/20'
|
||||||
|
import { Stack } from '@tamagui/core'
|
||||||
|
|
||||||
|
import { Banner } from './banner'
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Banner> = {
|
||||||
|
component: Banner,
|
||||||
|
argTypes: {
|
||||||
|
children: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Banner>
|
||||||
|
|
||||||
|
export const Full: Story = {
|
||||||
|
args: {
|
||||||
|
icon: <PinIcon />,
|
||||||
|
children: 'Banner message',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoIcon: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Banner message',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoCount: Story = {
|
||||||
|
args: {
|
||||||
|
icon: <PinIcon />,
|
||||||
|
children: 'Banner message',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkStateConnecting: Story = {
|
||||||
|
args: {
|
||||||
|
backgroundColor: '$neutral-80-opa-5',
|
||||||
|
icon: <RecentIcon />,
|
||||||
|
children: 'Connecting...',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkStateError: Story = {
|
||||||
|
args: {
|
||||||
|
backgroundColor: '$danger-50-opa-20',
|
||||||
|
icon: <AlertIcon />,
|
||||||
|
children: 'Network is down',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
args: {},
|
||||||
|
render: () => (
|
||||||
|
<Stack space>
|
||||||
|
<Banner icon={<PinIcon />} count={5}>
|
||||||
|
Banner message
|
||||||
|
</Banner>
|
||||||
|
<Banner count={5}>Banner message</Banner>
|
||||||
|
<Banner backgroundColor="$neutral-80-opa-5" icon={<RecentIcon />}>
|
||||||
|
Connecting...
|
||||||
|
</Banner>
|
||||||
|
<Banner backgroundColor="$danger-50-opa-20" icon={<AlertIcon />}>
|
||||||
|
Network is down
|
||||||
|
</Banner>
|
||||||
|
<Banner icon={<PinIcon />}>Banner message</Banner>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Stack, styled } from '@tamagui/core'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
|
||||||
|
import { Counter } from '../counter'
|
||||||
|
import { Text } from '../text'
|
||||||
|
|
||||||
|
import type { ColorTokens } from '@tamagui/core'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode
|
||||||
|
icon?: React.ReactNode
|
||||||
|
count?: number
|
||||||
|
backgroundColor?: ColorTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
const Banner = (props: Props) => {
|
||||||
|
const {
|
||||||
|
icon = null,
|
||||||
|
children,
|
||||||
|
count,
|
||||||
|
backgroundColor = '$primary-50-opa-20',
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base backgroundColor={backgroundColor}>
|
||||||
|
<Content>
|
||||||
|
{icon}
|
||||||
|
<Stack flexGrow={1} flexShrink={1}>
|
||||||
|
<Text size={13} color="$textPrimary" truncate>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Content>
|
||||||
|
{count ? <Counter value={count} /> : null}
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Banner }
|
||||||
|
export type { Props as BannerProps }
|
||||||
|
|
||||||
|
const Base = styled(View, {
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
maxHeight: '40px',
|
||||||
|
gap: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const Content = styled(View, {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '90%', // truncate does not work without this ¯\_(ツ)_/¯
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
export { Banner, type BannerProps } from './banner'
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { action } from '@storybook/addon-actions'
|
||||||
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
|
import { Button } from './button'
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
|
const meta: Meta<typeof Button> = {
|
||||||
|
component: Button,
|
||||||
|
args: {
|
||||||
|
onPress: action('press'),
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
disabled: {
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
Story => (
|
||||||
|
<Stack alignItems="flex-start">
|
||||||
|
<Story />
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Button>
|
||||||
|
|
||||||
|
const icon = (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 15.7C11.3398 15.7 12.5717 15.2377 13.5448 14.464L9.88475 10.804L6.43975 14.4516C7.41525 15.2328 8.65306 15.7 10 15.7ZM5.52328 13.5287L8.96514 9.88437L5.53601 6.45524C4.76227 7.42833 4.3 8.66018 4.3 10C4.3 11.3325 4.7572 12.5581 5.52328 13.5287ZM9.85811 8.93887L6.45525 5.536C7.42834 4.76227 8.66018 4.3 10 4.3C11.2156 4.3 12.3423 4.68051 13.2675 5.32892L9.85811 8.93887ZM10.7777 9.85848L14.241 6.19151C15.1481 7.20095 15.7 8.53602 15.7 10C15.7 11.3398 15.2377 12.5717 14.464 13.5448L10.7777 9.85848ZM10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Click me',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary32: Story = {
|
||||||
|
name: 'Primary / 32',
|
||||||
|
args: {
|
||||||
|
size: 32,
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary24: Story = {
|
||||||
|
name: 'Primary / 24',
|
||||||
|
args: {
|
||||||
|
size: 24,
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryIconBefore: Story = {
|
||||||
|
name: 'Primary icon before',
|
||||||
|
args: {
|
||||||
|
children: 'Click me',
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryIconAfter: Story = {
|
||||||
|
name: 'Primary/Icon after',
|
||||||
|
args: {
|
||||||
|
children: 'Click me',
|
||||||
|
iconAfter: icon,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const PrimaryIconOnly: Story = {
|
||||||
|
name: 'Primary/Icon only',
|
||||||
|
args: {
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryIconOnlyCirlce: Story = {
|
||||||
|
name: 'Primary/Icon only/Circle',
|
||||||
|
args: {
|
||||||
|
icon,
|
||||||
|
shape: 'circle',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Success: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'positive',
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Outline: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'outline',
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ghost: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'ghost',
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Danger: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'danger',
|
||||||
|
children: 'Click me',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { cloneElement, forwardRef } from 'react'
|
||||||
|
|
||||||
|
import { Stack, styled } from '@tamagui/core'
|
||||||
|
|
||||||
|
import { Text } from '../text'
|
||||||
|
|
||||||
|
import type { TextProps } from '../text'
|
||||||
|
import type { GetVariants, MapVariant, PressableProps } from '../types'
|
||||||
|
import type { StackProps } from '@tamagui/core'
|
||||||
|
import type { Ref } from 'react'
|
||||||
|
|
||||||
|
type Variants = GetVariants<typeof Base>
|
||||||
|
|
||||||
|
type Props = PressableProps & {
|
||||||
|
variant?: Variants['variant']
|
||||||
|
size?: Variants['size']
|
||||||
|
shape?: 'default' | 'circle'
|
||||||
|
children?: string
|
||||||
|
icon?: React.ReactElement
|
||||||
|
iconAfter?: React.ReactElement
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColors: MapVariant<typeof Base, 'variant'> = {
|
||||||
|
primary: '$white-100',
|
||||||
|
positive: '$white-100',
|
||||||
|
grey: '$neutral-100',
|
||||||
|
darkGrey: '$neutral-100',
|
||||||
|
outline: '$neutral-100',
|
||||||
|
ghost: '$neutral-100',
|
||||||
|
danger: '$white-100',
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSizes: Record<NonNullable<Props['size']>, TextProps['size']> = {
|
||||||
|
'40': 15,
|
||||||
|
'32': 15,
|
||||||
|
'24': 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
|
const {
|
||||||
|
variant = 'primary',
|
||||||
|
shape = 'default',
|
||||||
|
size = 40,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
iconAfter,
|
||||||
|
...buttonProps
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// TODO: provider aria-label if button has only icon
|
||||||
|
const iconOnly = !children && Boolean(icon)
|
||||||
|
|
||||||
|
const textColor = textColors[variant]
|
||||||
|
const textSize = textSizes[size]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base
|
||||||
|
{...(buttonProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
radius={shape === 'circle' ? 'full' : size}
|
||||||
|
size={size}
|
||||||
|
iconOnly={iconOnly}
|
||||||
|
>
|
||||||
|
{icon ? cloneElement(icon, { color: textColor }) : null}
|
||||||
|
<Text weight="medium" color={textColor} size={textSize}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
{iconAfter ? cloneElement(iconAfter, { color: textColor }) : null}
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Button = forwardRef(Button)
|
||||||
|
|
||||||
|
export { _Button as Button }
|
||||||
|
export type { Props as ButtonProps }
|
||||||
|
|
||||||
|
const Base = styled(Stack, {
|
||||||
|
tag: 'button',
|
||||||
|
name: 'Button',
|
||||||
|
accessibilityRole: 'button',
|
||||||
|
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
animation: 'fast',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
primary: {
|
||||||
|
backgroundColor: '$primary-50',
|
||||||
|
hoverStyle: { backgroundColor: '$primary-60' },
|
||||||
|
// TODO: update background color
|
||||||
|
pressStyle: { backgroundColor: '$primary-50' },
|
||||||
|
},
|
||||||
|
positive: {
|
||||||
|
backgroundColor: '$success-50',
|
||||||
|
hoverStyle: { backgroundColor: '$success-60' },
|
||||||
|
// TODO: update background color
|
||||||
|
pressStyle: { backgroundColor: '$success-50' },
|
||||||
|
},
|
||||||
|
grey: {
|
||||||
|
backgroundColor: '$neutral-10',
|
||||||
|
hoverStyle: { backgroundColor: '$neutral-20' },
|
||||||
|
pressStyle: { backgroundColor: '$neutral-30' },
|
||||||
|
},
|
||||||
|
darkGrey: {
|
||||||
|
backgroundColor: '$neutral-20',
|
||||||
|
hoverStyle: { backgroundColor: '$neutral-30' },
|
||||||
|
pressStyle: { backgroundColor: '$neutral-40' },
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '$neutral-30',
|
||||||
|
hoverStyle: { borderColor: '$neutral-40' },
|
||||||
|
pressStyle: { borderColor: '$neutral-50' },
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
hoverStyle: { backgroundColor: '$neutral-10' },
|
||||||
|
pressStyle: { backgroundColor: '$neutral-20' },
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
backgroundColor: '$danger',
|
||||||
|
hoverStyle: { backgroundColor: '$danger-60' },
|
||||||
|
// TODO: update background color
|
||||||
|
pressStyle: { backgroundColor: '$danger' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
true: {
|
||||||
|
opacity: 0.3,
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
size: {
|
||||||
|
40: {
|
||||||
|
height: 40,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
32: {
|
||||||
|
height: 32,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
height: 24,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
full: {
|
||||||
|
borderRadius: 40,
|
||||||
|
},
|
||||||
|
40: {
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
32: {
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
iconOnly: {
|
||||||
|
true: {
|
||||||
|
gap: 0,
|
||||||
|
padding: 0,
|
||||||
|
aspectRatio: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
export { Button, type ButtonProps } from './button'
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Stack } from '@tamagui/core'
|
||||||
|
|
||||||
|
import { Channel } from './channel'
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
|
const meta: Meta<typeof Channel> = {
|
||||||
|
component: Channel,
|
||||||
|
args: {
|
||||||
|
emoji: '🍑',
|
||||||
|
children: 'channel',
|
||||||
|
},
|
||||||
|
|
||||||
|
argTypes: {},
|
||||||
|
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=411-18564&t=kX5LC5OYFnSF8BiZ-11',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render: args => (
|
||||||
|
<Stack space flexDirection="row">
|
||||||
|
<Stack space width={336}>
|
||||||
|
<Channel {...args} type="default" />
|
||||||
|
<Channel {...args} type="default" selected />
|
||||||
|
|
||||||
|
<Channel {...args} type="notification" />
|
||||||
|
<Channel {...args} type="notification" selected />
|
||||||
|
|
||||||
|
<Channel {...args} type="mention" mentionCount={10} />
|
||||||
|
<Channel {...args} type="mention" mentionCount={10} selected />
|
||||||
|
|
||||||
|
<Channel {...args} type="muted" />
|
||||||
|
<Channel {...args} type="muted" selected />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Channel>
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
lock: 'none',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Locked: Story = {
|
||||||
|
args: {
|
||||||
|
lock: 'locked',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Unlocked: Story = {
|
||||||
|
args: {
|
||||||
|
lock: 'unlocked',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { MutedIcon, NotificationIcon, OptionsIcon } from '@status-im/icons/20'
|
||||||
|
import { Stack, styled } from 'tamagui'
|
||||||
|
|
||||||
|
import { ChannelAvatar } from '../avatar'
|
||||||
|
import { Counter } from '../counter'
|
||||||
|
import { DropdownMenu } from '../dropdown-menu'
|
||||||
|
import { Text } from '../text'
|
||||||
|
|
||||||
|
import type { ChannelAvatarProps } from '../avatar'
|
||||||
|
import type { ColorTokens } from 'tamagui'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: string
|
||||||
|
selected: boolean
|
||||||
|
emoji: ChannelAvatarProps['emoji']
|
||||||
|
lock?: ChannelAvatarProps['lock']
|
||||||
|
mentionCount?: number
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'default' | 'notification' | 'muted'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'mention'
|
||||||
|
mentionCount: number
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const textColors: Record<Props['type'], ColorTokens> = {
|
||||||
|
default: '$neutral-50',
|
||||||
|
notification: '$neutral-100',
|
||||||
|
mention: '$neutral-100',
|
||||||
|
muted: '$neutral-40',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Channel = (props: Props) => {
|
||||||
|
const { type, children, selected, emoji, lock } = props
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
const active = hovered || menuOpen
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (active) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu onOpenChange={setMenuOpen}>
|
||||||
|
<Stack tag="button" width={20} height={20}>
|
||||||
|
<OptionsIcon color="$neutral-50" />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* TODO: Find all options */}
|
||||||
|
<DropdownMenu.Content align="start">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
icon={<MutedIcon />}
|
||||||
|
label="Mute channel"
|
||||||
|
onSelect={() => {
|
||||||
|
console.log('Mute channel')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
icon={<MutedIcon />}
|
||||||
|
label="Mark as read"
|
||||||
|
onSelect={() => {
|
||||||
|
console.log('Mark as read')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'default':
|
||||||
|
return null
|
||||||
|
case 'mention': {
|
||||||
|
const { mentionCount } = props
|
||||||
|
return <Counter value={mentionCount} />
|
||||||
|
}
|
||||||
|
case 'notification':
|
||||||
|
return <NotificationIcon color="$neutral-40" />
|
||||||
|
case 'muted':
|
||||||
|
return <MutedIcon color="$neutral-40" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColor: ColorTokens =
|
||||||
|
selected || active ? '$neutral-100' : textColors[type]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base
|
||||||
|
onHoverIn={() => setHovered(true)}
|
||||||
|
onHoverOut={() => setHovered(false)}
|
||||||
|
state={active ? 'active' : selected ? 'selected' : undefined}
|
||||||
|
>
|
||||||
|
<Stack flexDirection="row" gap={8} alignItems="center">
|
||||||
|
<ChannelAvatar emoji={emoji} size={24} lock={lock} />
|
||||||
|
<Text size={15} weight="medium" color={textColor}>
|
||||||
|
# {children}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{renderContent()}
|
||||||
|
</Base>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Channel }
|
||||||
|
export type { Props as ChannelProps }
|
||||||
|
|
||||||
|
const Base = styled(Stack, {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 12,
|
||||||
|
userSelect: 'none',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
state: {
|
||||||
|
active: {
|
||||||
|
backgroundColor: '$primary-50-opa-5',
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
backgroundColor: '$primary-50-opa-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
export { Channel, type ChannelProps } from './channel'
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { CHANNEL_GROUPS } from './mock-data'
|
||||||
|
export { SidebarCommunity } from './sidebar-community'
|
||||||
|
export { SidebarMembers } from './sidebar-members'
|
||||||
|
export { Topbar } from './topbar'
|
|
@ -0,0 +1,199 @@
|
||||||
|
export interface ChannelType {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
emoji: string
|
||||||
|
channelStatus?: 'default' | 'notification' | 'muted'
|
||||||
|
mentionCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelGroupType {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
unreadCount?: number
|
||||||
|
channels: ChannelType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojis = ['👋', '🔥', '🦄', '🍑', '🤫', '🫣', '🏀', '🤝']
|
||||||
|
|
||||||
|
const randomEmoji = () => emojis[Math.floor(Math.random() * emojis.length)]
|
||||||
|
|
||||||
|
// MOCK DATA
|
||||||
|
export const CHANNEL_GROUPS: ChannelGroupType[] = [
|
||||||
|
{
|
||||||
|
id: 'welcome',
|
||||||
|
title: 'Welcome',
|
||||||
|
unreadCount: 3,
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'welcome',
|
||||||
|
title: 'welcome',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'general-welcome',
|
||||||
|
title: 'general',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'random',
|
||||||
|
title: 'random',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'onboarding',
|
||||||
|
title: 'onboarding',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
mentionCount: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'community',
|
||||||
|
title: 'Community',
|
||||||
|
unreadCount: 5,
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'announcements',
|
||||||
|
title: 'announcements',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jobs',
|
||||||
|
title: 'jobs',
|
||||||
|
mentionCount: 3,
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events',
|
||||||
|
title: 'events',
|
||||||
|
mentionCount: 2,
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meetups',
|
||||||
|
title: 'meetups',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'design',
|
||||||
|
title: 'Design',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'design',
|
||||||
|
title: 'design',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ux',
|
||||||
|
title: 'ux',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ui',
|
||||||
|
title: 'ui',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'figma',
|
||||||
|
title: 'figma',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'General',
|
||||||
|
title: 'General',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'general',
|
||||||
|
title: 'general',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'people-ops',
|
||||||
|
title: 'people-ops',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Frontend',
|
||||||
|
title: 'Frontend',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'react',
|
||||||
|
title: 'react',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
channelStatus: 'notification',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vue',
|
||||||
|
title: 'vue',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'angular',
|
||||||
|
title: 'angular',
|
||||||
|
channelStatus: 'muted',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svelte',
|
||||||
|
title: 'svelte',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Backend',
|
||||||
|
title: 'Backend',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: 'node',
|
||||||
|
title: 'node',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'python',
|
||||||
|
title: 'python',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ruby',
|
||||||
|
title: 'ruby',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'php',
|
||||||
|
title: 'php',
|
||||||
|
channelStatus: 'muted',
|
||||||
|
emoji: randomEmoji(),
|
||||||
|
description: 'Share random funny stuff with the community. Play nice.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
|
@ -0,0 +1,74 @@
|
||||||
|
import * as Accordion from '@radix-ui/react-accordion'
|
||||||
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
|
import { Channel } from '../../../channel'
|
||||||
|
import { DividerLabel } from '../../../dividers'
|
||||||
|
|
||||||
|
import type { ChannelType } from '../../mock-data'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string
|
||||||
|
channels: ChannelType[]
|
||||||
|
unreadCount?: number
|
||||||
|
selectedChannelId?: string
|
||||||
|
expanded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelGroup = (props: Props) => {
|
||||||
|
const { name, channels, selectedChannelId, expanded } = props
|
||||||
|
|
||||||
|
const totalMentionsCount = channels.reduce(
|
||||||
|
(acc, channel) => acc + (channel.mentionCount || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion.Item value={name}>
|
||||||
|
<Stack>
|
||||||
|
<Accordion.Trigger>
|
||||||
|
<DividerLabel
|
||||||
|
type="expandable"
|
||||||
|
expanded={expanded}
|
||||||
|
label={name}
|
||||||
|
counterType="default"
|
||||||
|
count={
|
||||||
|
totalMentionsCount > 0 && expanded === false
|
||||||
|
? totalMentionsCount
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Accordion.Trigger>
|
||||||
|
|
||||||
|
<Stack paddingHorizontal={8} paddingBottom={expanded ? 8 : 0}>
|
||||||
|
{channels.map(channel => {
|
||||||
|
const {
|
||||||
|
emoji,
|
||||||
|
title,
|
||||||
|
//This will work differently with the live data
|
||||||
|
channelStatus: type = 'default',
|
||||||
|
mentionCount = 0,
|
||||||
|
} = channel
|
||||||
|
|
||||||
|
const selected = selectedChannelId === channel.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion.Content key={channel.title}>
|
||||||
|
<Channel
|
||||||
|
emoji={emoji}
|
||||||
|
selected={!!selected}
|
||||||
|
{...(mentionCount > 0
|
||||||
|
? { type: 'mention', mentionCount }
|
||||||
|
: { type })}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Channel>
|
||||||
|
</Accordion.Content>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Accordion.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChannelGroup }
|
|
@ -0,0 +1 @@
|
||||||
|
export { SidebarCommunity } from './sidebar-community'
|