From 4439e0e027f1b2ec25a8c1f76e3ed8f851c64066 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Mon, 12 Jan 2026 23:47:19 +0000 Subject: [PATCH] feat(companion): orchestrator page skeleton --- .../companion/app/(tabs)/orchestrator.tsx | 428 +++++++++++++++++- aris/apps/companion/components/ui/badge.tsx | 67 +++ aris/apps/companion/components/ui/card.tsx | 96 ++-- 3 files changed, 545 insertions(+), 46 deletions(-) create mode 100644 aris/apps/companion/components/ui/badge.tsx diff --git a/aris/apps/companion/app/(tabs)/orchestrator.tsx b/aris/apps/companion/app/(tabs)/orchestrator.tsx index 22219cf..1af3439 100644 --- a/aris/apps/companion/app/(tabs)/orchestrator.tsx +++ b/aris/apps/companion/app/(tabs)/orchestrator.tsx @@ -1,22 +1,426 @@ +import type { ReactNode } from "react"; import { View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Container } from "@/components/Container"; +import { SafeAreaScrollView } from "@/components/safe-area-scroll-view"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Text } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +const fixture = { + location: { + authorization: "When In Use", + lastLocation: { + latitude: 37.33182, + longitude: -122.03118, + accuracyMeters: 12.4, + speedMps: 0.4, + }, + }, + recompute: { + lastReason: "manual", + lastTime: new Date("2024-05-21T09:32:00Z"), + elapsedMs: 482, + fetchFailed: false, + error: null as string | null, + }, + winner: { + title: "Glass Now online", + subtitle: "Connected to iPhone", + type: "INFO", + bucket: "RIGHT_NOW", + priority: 0.8, + ttlSec: 86400, + poiType: "CAFE", + startsAt: 1767717000, + id: "demo:welcome", + }, + feed: [ + { + id: "demo:welcome", + title: "Glass Now online", + subtitle: "Connected to iPhone", + type: "INFO", + bucket: "RIGHT_NOW", + priority: 0.8, + ttlSec: 86400, + }, + { + id: "cal:demo:1767717000", + title: "Team Sync", + subtitle: "", + type: "CALENDAR_EVENT", + bucket: "FYI", + priority: 0.7, + ttlSec: 5400, + }, + { + id: "demo:next", + title: "Next: Calendar", + subtitle: "Then Weather + POI", + type: "INFO", + bucket: "FYI", + priority: 0.4, + ttlSec: 86400, + }, + { + id: "music:now:demo", + title: "Midnight City", + subtitle: "M83 - Hurry Up, We're Dreaming", + type: "NOW_PLAYING", + bucket: "FYI", + priority: 0.35, + ttlSec: 30, + }, + ], + nowPlaying: { + auth: "Authorized", + title: "Midnight City", + artist: "M83", + album: "Hurry Up, We're Dreaming", + playbackStatus: "playing", + }, + weatherDiagnostics: { + provider: "WeatherKit", + last_fetch: "2024-05-21 09:30:14", + alerts: "0", + conditions: "mostly_clear_day", + }, + calendarDiagnostics: { + events_upcoming: "3", + next_event: "Team Sync - 10:30", + }, + test: { + note: "Test actions are not available in the React Native client.", + }, +}; + +const formatTime = (date: Date | null) => { + if (!date) { + return "--"; + } + return date.toLocaleTimeString(); +}; + +const formatSpeed = (speedMps: number) => { + if (speedMps < 0) { + return "--"; + } + return `${speedMps.toFixed(1)} m/s`; +}; + +const formatLatLon = (value: number) => value.toFixed(5); + +const formatPriority = (value: number) => value.toFixed(2); export default function OrchestratorScreen() { + const insets = useSafeAreaInsets(); + return ( - - - Orchestrator - - Port of SwiftUI OrchestratorView will live here. - - + + + + + Orchestrator + + Context engine status and diagnostics. + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } -const styles = { - container: "flex flex-1 bg-white", - subtitle: "mt-2 text-sm text-muted-foreground", -}; +function ValueRow({ label, value }: { label: ReactNode; value: ReactNode }) { + return ( + + {typeof label === "string" ? ( + {label} + ) : ( + label + )} + {typeof value === "string" || typeof value === "number" ? ( + {value} + ) : ( + value + )} + + ); +} + +function LocationSection() { + const { authorization, lastLocation } = fixture.location; + + return ( + + + Location + + + + {lastLocation ? ( + <> + + {`${formatLatLon(lastLocation.latitude)}, ${formatLatLon(lastLocation.longitude)}`} + + } + /> + + + + ) : ( + No location yet + )} + + + ); +} + +function RecomputeSection() { + const { lastReason, lastTime, elapsedMs, fetchFailed, error } = + fixture.recompute; + + return ( + + + Recompute + + + + + + + {error ? ( + {error} + ) : null} + + + + ); +} + +function WinnerSection() { + const winner = fixture.winner; + + return ( + + + Winner + + {winner ? ( + + + + {winner.title} + + {winner.subtitle ?? "No description"} + + + + {winner.type} + + + + + + + {winner.poiType ? ( + + ) : null} + {winner.startsAt ? ( + + ) : null} + + {winner.id} + + } + /> + + + ) : ( + No winner yet + )} + + ); +} + +function FeedSection() { + const feed = fixture.feed; + + return ( + + + Feed + + {feed.length === 0 ? ( + No feed items yet + ) : ( + + {feed.map((item, i) => ( + + + + + {item.title} + + + {item.subtitle || "No description"} + + + {`bucket ${item.bucket} | prio ${formatPriority(item.priority)} | ttl ${item.ttlSec}s`} + + + + {item.type} + + + + ))} + + )} + + ); +} + +function NowPlayingSection() { + const { auth, title, artist, album, playbackStatus } = fixture.nowPlaying; + const subtitle = [artist, album].filter(Boolean).join(" | ") || "Apple Music"; + + return ( + + + Now Playing + + + + {title ? ( + <> + + {title} + + + {subtitle} + + + {playbackStatus} + + + ) : ( + + {auth === "Authorized" ? "Nothing playing" : "Not authorized"} + + )} + + + ); +} + +function DiagnosticsSection({ + title, + entries, +}: { + title: string; + entries: Record; +}) { + const keys = Object.keys(entries).sort(); + + return ( + + + {title} + + + {keys.map((key) => ( + + {entries[key]} + + } + /> + ))} + + + ); +} + +function TestSection() { + return ( + + + Test + + {fixture.test.note} + + ); +} diff --git a/aris/apps/companion/components/ui/badge.tsx b/aris/apps/companion/components/ui/badge.tsx new file mode 100644 index 0000000..b1cd3f6 --- /dev/null +++ b/aris/apps/companion/components/ui/badge.tsx @@ -0,0 +1,67 @@ +import { TextClassContext } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; +import * as Slot from '@rn-primitives/slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Platform, View, ViewProps } from 'react-native'; + +const badgeVariants = cva( + cn( + 'border-border group shrink-0 flex-row items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5', + Platform.select({ + web: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive w-fit whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3', + }) + ), + { + variants: { + variant: { + default: cn( + 'bg-primary border-transparent', + Platform.select({ web: '[a&]:hover:bg-primary/90' }) + ), + secondary: cn( + 'bg-secondary border-transparent', + Platform.select({ web: '[a&]:hover:bg-secondary/90' }) + ), + destructive: cn( + 'bg-destructive border-transparent', + Platform.select({ web: '[a&]:hover:bg-destructive/90' }) + ), + outline: Platform.select({ web: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground' }), + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +const badgeTextVariants = cva('text-xs font-medium', { + variants: { + variant: { + default: 'text-primary-foreground', + secondary: 'text-secondary-foreground', + destructive: 'text-white', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, +}); + +type BadgeProps = ViewProps & + React.RefAttributes & { + asChild?: boolean; + } & VariantProps; + +function Badge({ className, variant, asChild, ...props }: BadgeProps) { + const Component = asChild ? Slot.View : View; + return ( + + + + ); +} + +export { Badge, badgeTextVariants, badgeVariants }; +export type { BadgeProps }; diff --git a/aris/apps/companion/components/ui/card.tsx b/aris/apps/companion/components/ui/card.tsx index e3c2e30..dfea633 100644 --- a/aris/apps/companion/components/ui/card.tsx +++ b/aris/apps/companion/components/ui/card.tsx @@ -1,52 +1,80 @@ -import { Text, TextClassContext } from '@/components/ui/text'; -import { cn } from '@/lib/utils'; -import { View, type ViewProps } from 'react-native'; +import { Text, TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; +import { View, type ViewProps } from "react-native"; function Card({ className, ...props }: ViewProps & React.RefAttributes) { - return ( - - - - ); + return ( + + + + ); } -function CardHeader({ className, ...props }: ViewProps & React.RefAttributes) { - return ; +function CardHeader({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ( + + ); } function CardTitle({ - className, - ...props + className, + ...props }: React.ComponentProps & React.RefAttributes) { - return ( - - ); + return ( + + ); } function CardDescription({ - className, - ...props + className, + ...props }: React.ComponentProps & React.RefAttributes) { - return ; + return ( + + ); } -function CardContent({ className, ...props }: ViewProps & React.RefAttributes) { - return ; +function CardContent({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ; } -function CardFooter({ className, ...props }: ViewProps & React.RefAttributes) { - return ; +function CardFooter({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ( + + ); } -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +};