feat: add tui app

This commit is contained in:
Max Koon
2025-11-14 13:26:15 -05:00
parent 058f2bb94f
commit 5b14b4e7a4
22 changed files with 2395 additions and 128 deletions

View File

@@ -0,0 +1,33 @@
import * as React from "react";
import type { ViewProps, TextProps } from "react-native";
export function View({ children, style }: ViewProps) {
const bg = style &&
'backgroundColor' in style
? typeof style.backgroundColor == 'string'
? style.backgroundColor
: undefined
: undefined;
return <box backgroundColor={bg}>{children}</box>
}
export function Text({ style, children }: TextProps) {
const fg = style &&
'color' in style
? typeof style.color == 'string'
? style.color
: undefined
: undefined;
return <text fg={fg || "black"}>{children}</text>
}
export const Platform = {
OS: "tui",
};
export default {
View,
Text,
}

View File

@@ -0,0 +1,11 @@
{
"name": "react-native-opentui",
"version": "1.0.0",
"main": "index.tsx",
"exports": {
".": "./index.tsx"
},
"peerDependencies": {
"react": "*"
}
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true
}
}

View File

@@ -32,9 +32,9 @@ export const queries = {
.orderBy('name', 'asc');
}),
getItems: syncedQueryWithContext('getItems', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData);
// isLoggedIn(authData);
return builder.plaidAccessTokens
.where('userId', '=', authData.user.id)
// .where('userId', '=', authData.user.id)
.orderBy('createdAt', 'desc');
})
};

14
packages/ui/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "@money/ui",
"private": true,
"version": "1.0.0",
"description": "",
"exports": {
".": "./src/index.tsx"
},
"dependencies": {
"react-native-opentui": "workspace:*",
"@money/shared": "workspace:*"
},
"packageManager": "pnpm@10.18.2"
}

15
packages/ui/src/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Text } from "react-native";
import { List } from "./list";
import { useQuery } from "@rocicorp/zero/react";
import { queries } from '@money/shared';
export function Settings() {
const [items] = useQuery(queries.getItems(null));
return <List
items={items}
renderItem={({ item, isSelected }) => <Text style={{ color: isSelected ? 'white' : 'black' }}>{item.name}</Text>}
/>
}

30
packages/ui/src/list.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { useState, type ReactNode } from "react";
import { View, Text } from "react-native";
import { useKeyboard } from "./useKeyboard";
export type ListProps<T> = {
items: T[],
renderItem: (props: { item: T, isSelected: boolean }) => ReactNode;
};
export function List<T>({ items, renderItem }: ListProps<T>) {
const [idx, setIdx] = useState(0);
useKeyboard((key) => {
if (key.name == 'j') {
setIdx((prevIdx) => prevIdx + 1 < items.length ? prevIdx + 1 : items.length - 1);
} else if (key.name == 'k') {
setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
} else if (key.name == 'g' && key.shift) {
setIdx(items.length - 1);
}
}, [items]);
return (
<View>
{items.map((item, index) => <View style={{ backgroundColor: index == idx ? 'black' : undefined }}>
{renderItem({ item, isSelected: index == idx })}
</View>)}
</View>
);
}

View File

@@ -0,0 +1,5 @@
import { useKeyboard as useOpentuiKeyboard } from "@opentui/react";
export function useKeyboard(handler: Parameters<typeof useOpentuiKeyboard>[0], _deps: any[] = []) {
return useOpentuiKeyboard(handler);
}

View File

@@ -0,0 +1,35 @@
import { useEffect } from "react";
import type { KeyboardEvent } from "react";
import type { KeyEvent } from "@opentui/core";
export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) {
useEffect(() => {
const handlerWeb = (event: KeyboardEvent) => {
// @ts-ignore
handler({
name: event.key.toLowerCase(),
ctrl: event.ctrlKey,
meta: event.metaKey,
shift: event.shiftKey,
option: event.metaKey,
sequence: '',
number: false,
raw: '',
eventType: 'press',
source: "raw",
code: event.code,
super: false,
hyper: false,
capsLock: false,
numLock: false,
baseCode: event.keyCode,
});
};
// @ts-ignore
window.addEventListener("keydown", handlerWeb);
// @ts-ignore
return () => window.removeEventListener("keydown", handlerWeb);
}, deps);
}

28
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}