Merge branch 'next' into 'main'
@ -3,3 +3,6 @@
|
||||
**/protos
|
||||
**/proto
|
||||
**/coverage
|
||||
**/storybook-static
|
||||
**/examples
|
||||
**/packages/status-react
|
||||
|
@ -32,6 +32,7 @@
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [
|
||||
@ -89,6 +90,7 @@
|
||||
"alwaysTryTypes": true,
|
||||
"project": ["tsconfig.base.json", "packages/*/tsconfig.json"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"import/ignore": ["react-native"]
|
||||
}
|
||||
}
|
||||
|
54
.github/workflows/ci.yml
vendored
@ -2,43 +2,43 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
timeout-minutes: 15
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Build and Test
|
||||
timeout-minutes: 15
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint && yarn format:check
|
||||
- name: Lint
|
||||
run: yarn lint && yarn format --check
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
25
.gitignore
vendored
@ -79,3 +79,28 @@ node_modules/
|
||||
|
||||
# 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
|
3
apps/desktop/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
7
apps/desktop/README.md
Normal 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)
|
27
apps/desktop/_vite.config.ts
Normal 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
@ -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
27
apps/desktop/src-tauri/Cargo.toml
Normal 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"]
|
3
apps/desktop/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
BIN
apps/desktop/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/desktop/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
apps/desktop/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 974 B |
BIN
apps/desktop/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
apps/desktop/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
apps/desktop/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
apps/desktop/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
apps/desktop/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 903 B |
BIN
apps/desktop/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/desktop/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/desktop/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
apps/desktop/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
apps/desktop/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
apps/desktop/src-tauri/icons/icon.icns
Normal file
BIN
apps/desktop/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
17
apps/desktop/src-tauri/src/main.rs
Normal 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");
|
||||
}
|
65
apps/desktop/src-tauri/tauri.conf.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
21
apps/desktop/tsconfig.json
Normal 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" }]
|
||||
}
|
9
apps/desktop/tsconfig.node.json
Normal 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
@ -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
@ -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"
|
||||
}
|
||||
}
|
BIN
apps/mobile/assets/adaptive-icon.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
apps/mobile/assets/favicon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/mobile/assets/fonts/UbuntuMono.ttf
Normal file
BIN
apps/mobile/assets/icon.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
apps/mobile/assets/splash.png
Normal file
After Width: | Height: | Size: 24 KiB |
39
apps/mobile/babel.config.js
Normal 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
@ -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
@ -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)
|
21
apps/mobile/metro.config.js
Normal 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
|
9
apps/mobile/navigation/provider.tsx
Normal 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
@ -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"
|
||||
}
|
||||
}
|
53
apps/mobile/screens/channel.tsx
Normal 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>
|
||||
)
|
||||
}
|
42
apps/mobile/screens/home.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
apps/mobile/tamagui.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { config } from '@status-im/components'
|
||||
|
||||
export default config
|
4
apps/mobile/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
24
apps/web/.gitignore
vendored
Normal 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
@ -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
@ -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
After Width: | Height: | Size: 1.2 KiB |
148
apps/web/src/app.tsx
Normal 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
|
49
apps/web/src/hooks/use-scroll-position.tsx
Normal 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
@ -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
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
93
apps/web/styles/app.css
Normal 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
@ -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;
|
||||
}
|
3
apps/web/tamagui.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { config } from '@status-im/components'
|
||||
|
||||
export default config
|
10
apps/web/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
9
apps/web/tsconfig.node.json
Normal 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
@ -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
@ -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": {
|
||||
"@status-im/react": "^0.1.1",
|
||||
"next": "12.3.1",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
|
@ -10,14 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@status-im/react": "^0.1.1",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^3.1.7"
|
||||
},
|
||||
"engines": {
|
||||
|
70
package.json
@ -1,45 +1,57 @@
|
||||
{
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
],
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/status-js",
|
||||
"packages/components",
|
||||
"packages/icons",
|
||||
"apps/*"
|
||||
]
|
||||
},
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"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/*",
|
||||
"typecheck": "turbo run typecheck --filter=@status-im/*",
|
||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/.eslint-cache .",
|
||||
"format": "prettier --cache --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"clean": "turbo run clean && rm -rf node_modules"
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "prettier --ignore-path .gitignore --cache --write .",
|
||||
"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": {
|
||||
"@changesets/cli": "^2.23.0",
|
||||
"@tsconfig/strictest": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-import-resolver-typescript": "^3.5.0",
|
||||
"@tsconfig/strictest": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-import-resolver-node": "^0.3.7",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.27.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.4",
|
||||
"prettier": "^2.7.1",
|
||||
"turbo": "^1.3.1",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.9.12",
|
||||
"vite-node": "^0.16.0",
|
||||
"vitest": "^0.16.0"
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.0",
|
||||
"prettier": "^2.8.4",
|
||||
"rimraf": "^4.4.0",
|
||||
"turbo": "^1.8.3",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.4",
|
||||
"vite-node": "^0.29.2",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
|
5
packages/components/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
dist/
|
||||
.DS_Store
|
||||
THUMBS_DB
|
||||
node_modules/
|
||||
types/
|
12
packages/components/.storybook/components.css
Normal 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%;
|
||||
}
|
||||
}
|
21
packages/components/.storybook/main.ts
Normal 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
|
3
packages/components/.storybook/preview-head.html
Normal file
@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window
|
||||
</script>
|
26
packages/components/.storybook/preview.tsx
Normal 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]
|
101
packages/components/.storybook/reset.css
Normal 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;
|
||||
}
|
1
packages/components/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { useImageUpload } from './use-image-uploader'
|
73
packages/components/hooks/use-image-uploader.ts
Normal 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 }
|
71
packages/components/package.json
Normal 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"
|
||||
}
|
||||
}
|
16
packages/components/src/anchor-actions/index.tsx
Normal 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 }
|
18
packages/components/src/animations.native.ts
Normal 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,
|
||||
},
|
||||
})
|
7
packages/components/src/animations.ts
Normal 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',
|
||||
})
|
140
packages/components/src/author/author.stories.tsx
Normal 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
|
60
packages/components/src/author/author.tsx
Normal 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 }
|
1
packages/components/src/author/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Author } from './author'
|
66
packages/components/src/avatar/avatar.stories.tsx
Normal 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
|
196
packages/components/src/avatar/avatar.tsx
Normal 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',
|
||||
})
|
81
packages/components/src/avatar/channel-avatar.tsx
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
44
packages/components/src/avatar/icon-avatar.tsx
Normal 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 }
|
3
packages/components/src/avatar/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './avatar'
|
||||
export * from './channel-avatar'
|
||||
export * from './icon-avatar'
|
76
packages/components/src/banner/banner.stories.tsx
Normal 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
|
56
packages/components/src/banner/banner.tsx
Normal 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 ¯\_(ツ)_/¯
|
||||
})
|
1
packages/components/src/banner/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Banner, type BannerProps } from './banner'
|
135
packages/components/src/button/button.stories.tsx
Normal 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
|
186
packages/components/src/button/button.tsx
Normal 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,
|
||||
})
|
1
packages/components/src/button/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Button, type ButtonProps } from './button'
|
64
packages/components/src/channel/channel.stories.tsx
Normal 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
|
130
packages/components/src/channel/channel.tsx
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
1
packages/components/src/channel/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Channel, type ChannelProps } from './channel'
|
4
packages/components/src/community/index.tsx
Normal 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'
|
199
packages/components/src/community/mock-data.tsx
Normal 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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
@ -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'
|