From 9834b9518b42a8a5f180cbdbe5b2a80d0e679b97 Mon Sep 17 00:00:00 2001 From: Max Koon <22125083+k2on@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:06:12 -0500 Subject: [PATCH] feat: add table --- packages/react-native-opentui/index.tsx | 9 +- packages/shared/src/queries.ts | 4 +- packages/ui/package.json | 4 +- packages/ui/src/index.tsx | 30 +++++-- packages/ui/src/table.tsx | 113 ++++++++++++++++++++++++ packages/ui/src/useKeyboard.web.ts | 14 ++- pnpm-lock.yaml | 21 +---- 7 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 packages/ui/src/table.tsx diff --git a/packages/react-native-opentui/index.tsx b/packages/react-native-opentui/index.tsx index baa5db7..0b1376f 100644 --- a/packages/react-native-opentui/index.tsx +++ b/packages/react-native-opentui/index.tsx @@ -8,7 +8,14 @@ export function View({ children, style }: ViewProps) { ? style.backgroundColor : undefined : undefined; - return {children} + const flexDirection = style && + 'flexDirection' in style + ? typeof style.flexDirection == 'string' + ? style.flexDirection + : undefined + : undefined; + + return {children} } diff --git a/packages/shared/src/queries.ts b/packages/shared/src/queries.ts index de0ddf8..53a4dd5 100644 --- a/packages/shared/src/queries.ts +++ b/packages/shared/src/queries.ts @@ -12,9 +12,9 @@ export const queries = { .one(); }), allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => { - isLoggedIn(authData); + // isLoggedIn(authData); return builder.transaction - .where('user_id', '=', authData.user.id) + // .where('user_id', '=', authData.user.id) .orderBy('datetime', 'desc') .limit(50) }), diff --git a/packages/ui/package.json b/packages/ui/package.json index 09b655f..69ceb62 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,8 +7,8 @@ ".": "./src/index.tsx" }, "dependencies": { - "react-native-opentui": "workspace:*", - "@money/shared": "workspace:*" + "@money/shared": "workspace:*", + "react-native-opentui": "workspace:*" }, "packageManager": "pnpm@10.18.2" } diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 3444758..a9bd2ed 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -1,15 +1,29 @@ -import { Text } from "react-native"; -import { List } from "./list"; +import { Table, type Column } from "./table"; import { useQuery } from "@rocicorp/zero/react"; import { queries } from '@money/shared'; -export function Settings() { - const [items] = useQuery(queries.getItems(null)); - return {item.name}} - /> +export type Account = { + name: string; + createdAt: number; +} + +const COLUMNS: Column[] = [ + { name: 'createdAt', label: 'Created At', render: (n) => new Date(n).toDateString() }, + { name: 'amount', label: 'Amount' }, + { name: 'name', label: 'Name' }, +]; + + +export function Settings() { + const [items] = useQuery(queries.allTransactions(null)); + + return ( + + ) } diff --git a/packages/ui/src/table.tsx b/packages/ui/src/table.tsx new file mode 100644 index 0000000..1859ac8 --- /dev/null +++ b/packages/ui/src/table.tsx @@ -0,0 +1,113 @@ +import { createContext, use, useState, type ReactNode } from "react"; +import { View, Text } from "react-native"; +import { useKeyboard } from "./useKeyboard"; + +const HEADER_COLOR = '#7158e2'; +const TABLE_COLORS = [ + '#3c3c3c', + '#4b4b4b' +]; +const SELECTED_COLOR = '#f7b730'; + + +const EXTRA = 5; + +export type ValidRecord = Record; + +const TableContext = createContext<{ data: unknown[], columns: Column[], columnMap: Map }>({ + data: [], + columns: [], + columnMap: new Map(), +}); + +export type Column = { name: string, label: string, render?: (i: number | string) => string }; + + + +function renderCell(row: ValidRecord, column: Column): string { + const cell = row[column.name]; + if (cell == undefined) return 'n/a'; + if (cell == null) return 'null'; + if (column.render) return column.render(cell); + return cell.toString(); +} + + +export interface TableProps { + data: T[]; + columns: Column[]; +}; +export function Table({ data, columns }: TableProps) { + const [idx, setIdx] = useState(0); + const [selectedFrom, setSelectedFrom] = useState(); + + useKeyboard((key) => { + if (key.name == 'j' || key.name == 'down') { + if (key.shift && selectedFrom == undefined) { + setSelectedFrom(idx); + } + setIdx((prev) => Math.min(prev + 1, data.length - 1)); + } else if (key.name == 'k' || key.name == 'up') { + if (key.shift && selectedFrom == undefined) { + setSelectedFrom(idx); + } + setIdx((prev) => Math.max(prev - 1, 0)); + } else if (key.name == 'g' && key.shift) { + setIdx(data.length - 1); + } else if (key.name == 'v') { + setSelectedFrom(idx); + } else if (key.name == 'escape') { + setSelectedFrom(undefined); + } + }, [data, idx]); + + + const columnMap = new Map(columns.map(col => { + return [col.name, Math.max(col.label.length, ...data.map(row => renderCell(row, col).length))] + })); + + + return ( + + + + {columns.map(column => {rpad(column.label, columnMap.get(column.name)! - column.label.length + EXTRA)})} + + {data.map((row, index) => { + const isSelected = index == idx || (selectedFrom != undefined && ((selectedFrom <= index && index <= idx) || (idx <= index && index <= selectedFrom))) + + return ( + + + + ); + })} + + + ); +} + +interface RowProps { + row: T; + index: number; + isSelected: boolean; +} +function TableRow({ row, isSelected }: RowProps) { + const { data, columns, columnMap } = use(TableContext); + + + return + {columns.map(column => { + const rendered = renderCell(row, column); + return {rpad(rendered, columnMap.get(column.name)! - rendered.length + EXTRA)}; + })} + +} + +function rpad(input: string, length: number): string { + return input + Array.from({ length }) + .map(_ => " ") + .join(""); +} + + diff --git a/packages/ui/src/useKeyboard.web.ts b/packages/ui/src/useKeyboard.web.ts index 89bf2d5..d972ef8 100644 --- a/packages/ui/src/useKeyboard.web.ts +++ b/packages/ui/src/useKeyboard.web.ts @@ -3,12 +3,19 @@ import type { KeyboardEvent } from "react"; import type { KeyEvent } from "@opentui/core"; +function convertName(keyName: string): string { + const result = keyName.toLowerCase() + if (result == 'arrowdown') return 'down'; + if (result == 'arrowup') return 'up'; + return result; +} + export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) { useEffect(() => { const handlerWeb = (event: KeyboardEvent) => { // @ts-ignore handler({ - name: event.key.toLowerCase(), + name: convertName(event.key), ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey, @@ -29,7 +36,10 @@ export function useKeyboard(handler: (key: KeyEvent) => void, deps: any[] = []) // @ts-ignore window.addEventListener("keydown", handlerWeb); + return () => { + console.log("REMOVING"); // @ts-ignore - return () => window.removeEventListener("keydown", handlerWeb); +window.removeEventListener("keydown", handlerWeb); + }; }, deps); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90d4728..50383a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8065,7 +8065,6 @@ snapshots: - graphql - supports-color - utf-8-validate - optional: true '@expo/code-signing-certificates@0.0.5': dependencies: @@ -8132,7 +8131,6 @@ snapshots: optionalDependencies: react: 19.2.0 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) - optional: true '@expo/env@2.0.7': dependencies: @@ -8211,7 +8209,7 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - bufferutil - supports-color @@ -8290,7 +8288,7 @@ snapshots: '@expo/json-file': 10.0.7 '@react-native/normalize-colors': 0.81.5 debug: 4.4.3 - expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) resolve-from: 5.0.0 semver: 7.7.3 xml2js: 0.6.0 @@ -8318,7 +8316,6 @@ snapshots: expo-font: 14.0.9(expo@54.0.23)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) - optional: true '@expo/ws-tunnel@1.0.6': {} @@ -10247,7 +10244,6 @@ snapshots: react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) optionalDependencies: '@types/react': 19.1.17 - optional: true '@react-navigation/bottom-tabs@7.8.4(@react-navigation/native@7.1.19(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': dependencies: @@ -11107,7 +11103,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.4 - expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -12091,7 +12087,6 @@ snapshots: react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) transitivePeerDependencies: - supports-color - optional: true expo-constants@18.0.10(expo@54.0.23)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): dependencies: @@ -12110,7 +12105,6 @@ snapshots: react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) transitivePeerDependencies: - supports-color - optional: true expo-crypto@15.0.7(expo@54.0.23): dependencies: @@ -12126,7 +12120,6 @@ snapshots: dependencies: expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) - optional: true expo-font@14.0.9(expo@54.0.23)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: @@ -12141,7 +12134,6 @@ snapshots: fontfaceobserver: 2.3.0 react: 19.2.0 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) - optional: true expo-haptics@15.0.7(expo@54.0.23): dependencies: @@ -12164,7 +12156,6 @@ snapshots: dependencies: expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) react: 19.2.0 - optional: true expo-linking@8.0.8(expo@54.0.23)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: @@ -12206,7 +12197,6 @@ snapshots: invariant: 2.2.4 react: 19.2.0 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0) - optional: true expo-router@6.0.14(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.23)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.3(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: @@ -12422,7 +12412,6 @@ snapshots: - graphql - supports-color - utf-8-validate - optional: true exponential-backoff@3.1.3: {} @@ -14511,7 +14500,6 @@ snapshots: - bufferutil - supports-color - utf-8-validate - optional: true react-reconciler@0.32.0(react@19.1.0): dependencies: @@ -14579,8 +14567,7 @@ snapshots: react@19.1.0: {} - react@19.2.0: - optional: true + react@19.2.0: {} readable-stream@3.6.2: dependencies: