Merge branch 'next' into 'main'

This commit is contained in:
Felicio Mununga 2023-04-06 15:23:56 +02:00
commit 94e98a5e8b
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
880 changed files with 40031 additions and 2126 deletions

View File

@ -3,3 +3,6 @@
**/protos **/protos
**/proto **/proto
**/coverage **/coverage
**/storybook-static
**/examples
**/packages/status-react

View File

@ -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"]
} }
} }

View File

@ -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

25
.gitignore vendored
View File

@ -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

View File

@ -1,8 +0,0 @@
**/dist
**/node_modules
.parcel-cache
.github
**/protos
**/coverage
.next
**/.data

3
apps/desktop/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

7
apps/desktop/README.md Normal file
View File

@ -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)

View File

@ -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,
},
})

27
apps/desktop/package.json Normal file
View File

@ -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"
}
}

3563
apps/desktop/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"]

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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");
}

View File

@ -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"
}
]
}
}

View File

@ -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" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["_vite.config.ts"]
}

180
apps/mobile/App.tsx Normal file
View File

@ -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>
)
}

39
apps/mobile/app.json Normal file
View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

BIN
apps/mobile/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -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',
},
],
],
}
}

23
apps/mobile/eas.json Normal file
View File

@ -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": {}
}
}

8
apps/mobile/index.js Normal file
View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,9 @@
import { NavigationContainer } from '@react-navigation/native'
export function NavigationProvider({
children,
}: {
children: React.ReactNode
}) {
return <NavigationContainer>{children}</NavigationContainer>
}

43
apps/mobile/package.json Normal file
View File

@ -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"
}
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -0,0 +1,3 @@
import { config } from '@status-im/components'
export default config

View File

@ -0,0 +1,4 @@
{
"compilerOptions": {},
"extends": "expo/tsconfig.base"
}

24
apps/web/.gitignore vendored Normal file
View File

@ -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?

17
apps/web/index.html Normal file
View File

@ -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>

29
apps/web/package.json Normal file
View File

@ -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"
}
}

BIN
apps/web/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

148
apps/web/src/app.tsx Normal file
View File

@ -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

View File

@ -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
}

23
apps/web/src/main.tsx Normal file
View File

@ -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>
)

1
apps/web/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

93
apps/web/styles/app.css Normal file
View File

@ -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%;
}
}

100
apps/web/styles/reset.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1,3 @@
import { config } from '@status-im/components'
export default config

10
apps/web/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"jsx": "react-jsx"
},
"include": ["src"]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

36
apps/web/vite.config.ts Normal file
View File

@ -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)
],
})

28
eas.json Normal file
View File

@ -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": {}
}
}

View File

@ -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"

View File

@ -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": {

View File

@ -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}": [

5
packages/components/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
dist/
.DS_Store
THUMBS_DB
node_modules/
types/

View File

@ -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%;
}
}

View File

@ -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

View File

@ -0,0 +1,3 @@
<script>
window.global = window
</script>

View File

@ -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]

View File

@ -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;
}

View File

@ -0,0 +1 @@
export { useImageUpload } from './use-image-uploader'

View File

@ -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 }

View File

@ -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"
}
}

View File

@ -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 }

View File

@ -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,
},
})

View File

@ -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',
})

View File

@ -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

View File

@ -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 }

View File

@ -0,0 +1 @@
export { Author } from './author'

View File

@ -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

View File

@ -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',
})

View File

@ -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,
},
},
},
})

View File

@ -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 }

View File

@ -0,0 +1,3 @@
export * from './avatar'
export * from './channel-avatar'
export * from './icon-avatar'

View File

@ -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

View File

@ -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 ¯\_(ツ)_/¯
})

View File

@ -0,0 +1 @@
export { Banner, type BannerProps } from './banner'

View File

@ -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

View File

@ -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,
})

View File

@ -0,0 +1 @@
export { Button, type ButtonProps } from './button'

View File

@ -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

View File

@ -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',
},
},
},
})

View File

@ -0,0 +1 @@
export { Channel, type ChannelProps } from './channel'

View File

@ -0,0 +1,4 @@
export { CHANNEL_GROUPS } from './mock-data'
export { SidebarCommunity } from './sidebar-community'
export { SidebarMembers } from './sidebar-members'
export { Topbar } from './topbar'

View File

@ -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.',
},
],
},
]

View File

@ -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 }

View File

@ -0,0 +1 @@
export { SidebarCommunity } from './sidebar-community'

Some files were not shown because too many files have changed in this diff Show More