From 2aa7150716e1fe6bd1b9a2fb02f3703241273181 Mon Sep 17 00:00:00 2001 From: sai karthik Date: Thu, 23 Jul 2026 20:05:48 +0530 Subject: [PATCH] build mobile workspace flow --- .../mobile-workspace/mobile-flow-page.tsx | 66 + apps/web/src/hooks/use-mobile-workspace.ts | 35 + apps/web/src/index.css | 1 - apps/web/src/root.tsx | 5 +- apps/web/src/routes.ts | 27 +- apps/web/src/routes/_app._index.tsx | 12 - apps/web/src/routes/_app.todos.tsx | 102 -- .../dashboard/page.tsx} | 0 .../src/routes/{_app.tsx => app/layout.tsx} | 6 +- apps/web/src/routes/app/mobile/chat/page.tsx | 12 + apps/web/src/routes/app/mobile/layout.tsx | 9 + apps/web/src/routes/app/mobile/page.tsx | 15 + .../src/routes/app/mobile/work-list/page.tsx | 15 + apps/web/src/routes/app/mobile/work/page.tsx | 15 + apps/web/src/routes/app/todos/page.tsx | 123 ++ .../src/routes/{_auth.tsx => auth/layout.tsx} | 10 +- .../{_auth.login.tsx => auth/login/page.tsx} | 2 +- .../signup/page.tsx} | 2 +- apps/web/src/routes/design.mobile-chat.tsx | 89 -- apps/web/src/routes/design/chat/page.tsx | 12 + apps/web/src/routes/design/layout.tsx | 9 + apps/web/src/routes/design/page.tsx | 15 + apps/web/src/routes/design/work-unit/page.tsx | 15 + apps/web/src/routes/design/work/page.tsx | 15 + packages/ui/src/components/mobile-product.tsx | 8 + .../components/mobile-workspace-screens.tsx | 1168 +++++++++++++++++ .../ui/src/components/mobile-workspace.tsx | 937 +++++++++++++ .../src/components/mobile-workspace/index.ts | 6 + .../mobile-assistant-chat-screen.tsx | 6 + .../mobile-expanded-work-screen.tsx | 6 + .../mobile-workspace/mobile-home-screen.tsx | 6 + .../mobile-work-list-screen.tsx | 6 + .../mobile-work-unit-detail-screen.tsx | 6 + .../src/components/mobile-workspace/types.ts | 6 + packages/ui/src/styles/globals.css | 2 +- 35 files changed, 2552 insertions(+), 217 deletions(-) create mode 100644 apps/web/src/components/mobile-workspace/mobile-flow-page.tsx create mode 100644 apps/web/src/hooks/use-mobile-workspace.ts delete mode 100644 apps/web/src/routes/_app._index.tsx delete mode 100644 apps/web/src/routes/_app.todos.tsx rename apps/web/src/routes/{_app.dashboard.tsx => app/dashboard/page.tsx} (100%) rename apps/web/src/routes/{_app.tsx => app/layout.tsx} (74%) create mode 100644 apps/web/src/routes/app/mobile/chat/page.tsx create mode 100644 apps/web/src/routes/app/mobile/layout.tsx create mode 100644 apps/web/src/routes/app/mobile/page.tsx create mode 100644 apps/web/src/routes/app/mobile/work-list/page.tsx create mode 100644 apps/web/src/routes/app/mobile/work/page.tsx create mode 100644 apps/web/src/routes/app/todos/page.tsx rename apps/web/src/routes/{_auth.tsx => auth/layout.tsx} (65%) rename apps/web/src/routes/{_auth.login.tsx => auth/login/page.tsx} (88%) rename apps/web/src/routes/{_auth.signup.tsx => auth/signup/page.tsx} (88%) delete mode 100644 apps/web/src/routes/design.mobile-chat.tsx create mode 100644 apps/web/src/routes/design/chat/page.tsx create mode 100644 apps/web/src/routes/design/layout.tsx create mode 100644 apps/web/src/routes/design/page.tsx create mode 100644 apps/web/src/routes/design/work-unit/page.tsx create mode 100644 apps/web/src/routes/design/work/page.tsx create mode 100644 packages/ui/src/components/mobile-product.tsx create mode 100644 packages/ui/src/components/mobile-workspace-screens.tsx create mode 100644 packages/ui/src/components/mobile-workspace.tsx create mode 100644 packages/ui/src/components/mobile-workspace/index.ts create mode 100644 packages/ui/src/components/mobile-workspace/mobile-assistant-chat-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-expanded-work-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-home-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-work-list-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/mobile-work-unit-detail-screen.tsx create mode 100644 packages/ui/src/components/mobile-workspace/types.ts diff --git a/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx new file mode 100644 index 0000000..6ad3c6d --- /dev/null +++ b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx @@ -0,0 +1,66 @@ +import { + MobileAssistantChatScreen, + MobileExpandedWorkScreen, + MobileHomeScreen, + MobileWorkListScreen, + MobileWorkUnitDetailScreen, +} from "@code/ui/components/mobile-product"; +import { useNavigate } from "react-router"; + +import { useMobileWorkspace } from "@/hooks/use-mobile-workspace"; + +export type MobileFlowScreen = + | "assistant-chat" + | "home" + | "work-list" + | "work-unit-detail"; + +interface MobileFlowPageProps { + publicDemo?: boolean; + screen: MobileFlowScreen; +} + +export const MobileFlowPage = ({ + publicDemo = false, + screen, +}: MobileFlowPageProps) => { + const navigate = useNavigate(); + const workspace = useMobileWorkspace(); + const homePath = publicDemo ? "/design" : "/"; + const workPath = publicDemo ? "/design/work" : "/work"; + const chatPath = publicDemo ? "/design/chat" : "/chat"; + const detailPath = publicDemo ? "/design/work/flow-08" : "/work/flow-08"; + + const handleOpenUnit = () => { + if (screen === "work-list" && !workspace.expanded) { + workspace.setExpanded(true); + return; + } + navigate(detailPath); + }; + + const screenProps = { + composerValue: workspace.composerValue, + onBack: () => navigate(homePath), + onComposerChange: workspace.handleComposerChange, + onComposerSubmit: workspace.handleComposerSubmit, + onOpenAssistant: () => navigate(chatPath), + onOpenUnit: handleOpenUnit, + onViewWork: () => navigate(workPath), + statusMessage: workspace.statusMessage, + }; + + if (screen === "assistant-chat") { + return ; + } + if (screen === "home") { + return ; + } + if (screen === "work-unit-detail") { + return ; + } + if (workspace.expanded) { + return ; + } + return ; +}; diff --git a/apps/web/src/hooks/use-mobile-workspace.ts b/apps/web/src/hooks/use-mobile-workspace.ts new file mode 100644 index 0000000..ff19753 --- /dev/null +++ b/apps/web/src/hooks/use-mobile-workspace.ts @@ -0,0 +1,35 @@ +import type { FormEvent } from "react"; +import { useState } from "react"; + +export const useMobileWorkspace = (initialExpanded = false) => { + const [activeIndex, setActiveIndex] = useState(0); + const [composerValue, setComposerValue] = useState(""); + const [expanded, setExpanded] = useState(initialExpanded); + const [statusMessage, setStatusMessage] = useState(); + + const handleComposerChange = (value: string) => { + setComposerValue(value); + setStatusMessage(undefined); + }; + + const handleComposerSubmit = (event: FormEvent) => { + event.preventDefault(); + const message = composerValue.trim(); + if (!message) { + return; + } + setComposerValue(""); + setStatusMessage("Added to Zopu’s queue"); + }; + + return { + activeIndex, + composerValue, + expanded, + handleActiveIndexChange: setActiveIndex, + handleComposerChange, + handleComposerSubmit, + setExpanded, + statusMessage, + }; +}; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 872efa3..779a563 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -4,7 +4,6 @@ html, body { min-height: 100%; - overflow: hidden; } .ai-conversation-mobile > div { diff --git a/apps/web/src/root.tsx b/apps/web/src/root.tsx index 7bdf35b..6d9e01a 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -1,6 +1,5 @@ import { WebAuthProvider } from "@code/auth/web"; import { env } from "@code/env/web"; -import { MobileViewport } from "@code/ui/components/mobile-chat"; import { Toaster } from "@code/ui/components/sonner"; import { FlueProvider } from "@flue/react"; @@ -95,9 +94,7 @@ const App = () => ( disableTransitionOnChange storageKey="vite-ui-theme" > - - - + diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts index 4c05936..bbcd50f 100644 --- a/apps/web/src/routes.ts +++ b/apps/web/src/routes.ts @@ -1,4 +1,25 @@ -import { type RouteConfig } from "@react-router/dev/routes"; -import { flatRoutes } from "@react-router/fs-routes"; +import { index, layout, route } from "@react-router/dev/routes"; +import type { RouteConfig } from "@react-router/dev/routes"; -export default flatRoutes() satisfies RouteConfig; +export default [ + layout("./routes/auth/layout.tsx", [ + route("login", "./routes/auth/login/page.tsx"), + route("signup", "./routes/auth/signup/page.tsx"), + ]), + layout("./routes/app/layout.tsx", [ + layout("./routes/app/mobile/layout.tsx", [ + index("./routes/app/mobile/page.tsx"), + route("chat", "./routes/app/mobile/chat/page.tsx"), + route("work", "./routes/app/mobile/work-list/page.tsx"), + route("work/:workUnitId", "./routes/app/mobile/work/page.tsx"), + ]), + route("dashboard", "./routes/app/dashboard/page.tsx"), + route("todos", "./routes/app/todos/page.tsx"), + ]), + route("design", "./routes/design/layout.tsx", [ + index("./routes/design/page.tsx"), + route("chat", "./routes/design/chat/page.tsx"), + route("work", "./routes/design/work/page.tsx"), + route("work/:workUnitId", "./routes/design/work-unit/page.tsx"), + ]), +] satisfies RouteConfig; diff --git a/apps/web/src/routes/_app._index.tsx b/apps/web/src/routes/_app._index.tsx deleted file mode 100644 index 9b7eaec..0000000 --- a/apps/web/src/routes/_app._index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ChatPage } from "@/components/chat/chat-page"; - -import type { Route } from "./+types/_app._index"; - -export const meta = (_args: Route.MetaArgs) => [ - { title: "Zopu" }, - { content: "Chat with Zopu", name: "description" }, -]; - -const Home = () => ; - -export default Home; diff --git a/apps/web/src/routes/_app.todos.tsx b/apps/web/src/routes/_app.todos.tsx deleted file mode 100644 index 9bc6699..0000000 --- a/apps/web/src/routes/_app.todos.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { api } from "@code/backend/convex/_generated/api"; -import type { Id } from "@code/backend/convex/_generated/dataModel"; -import { Button } from "@code/ui/components/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@code/ui/components/card"; -import { Checkbox } from "@code/ui/components/checkbox"; -import { Input } from "@code/ui/components/input"; -import { useMutation, useQuery } from "convex/react"; -import { Loader2, Trash2 } from "lucide-react"; -import { useState, type FormEvent } from "react"; - -export default function Todos() { - const [newTodoText, setNewTodoText] = useState(""); - - const todos = useQuery(api.todos.getAll); - const createTodo = useMutation(api.todos.create); - const toggleTodo = useMutation(api.todos.toggle); - const deleteTodo = useMutation(api.todos.deleteTodo); - - const handleAddTodo = async (e: FormEvent) => { - e.preventDefault(); - const text = newTodoText.trim(); - if (!text) return; - await createTodo({ text }); - setNewTodoText(""); - }; - - const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => { - toggleTodo({ id, completed: !currentCompleted }); - }; - - const handleDeleteTodo = (id: Id<"todos">) => { - deleteTodo({ id }); - }; - - return ( -
- - - Todo List - Manage your tasks efficiently - - -
- setNewTodoText(e.target.value)} - placeholder="Add a new task..." - /> - -
- - {todos === undefined ? ( -
- -
- ) : todos.length === 0 ? ( -

No todos yet. Add one above!

- ) : ( -
    - {todos.map((todo) => ( -
  • -
    - handleToggleTodo(todo._id, todo.completed)} - id={`todo-${todo._id}`} - /> - -
    - -
  • - ))} -
- )} -
-
-
- ); -} diff --git a/apps/web/src/routes/_app.dashboard.tsx b/apps/web/src/routes/app/dashboard/page.tsx similarity index 100% rename from apps/web/src/routes/_app.dashboard.tsx rename to apps/web/src/routes/app/dashboard/page.tsx diff --git a/apps/web/src/routes/_app.tsx b/apps/web/src/routes/app/layout.tsx similarity index 74% rename from apps/web/src/routes/_app.tsx rename to apps/web/src/routes/app/layout.tsx index 62cc28c..76deb1a 100644 --- a/apps/web/src/routes/_app.tsx +++ b/apps/web/src/routes/app/layout.tsx @@ -6,7 +6,11 @@ export default function AppLayout() { const location = useLocation(); if (isLoading) { - return
Loading…
; + return ( +
+ Loading… +
+ ); } if (!isAuthenticated) { diff --git a/apps/web/src/routes/app/mobile/chat/page.tsx b/apps/web/src/routes/app/mobile/chat/page.tsx new file mode 100644 index 0000000..1326aab --- /dev/null +++ b/apps/web/src/routes/app/mobile/chat/page.tsx @@ -0,0 +1,12 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Zopu" }, + { content: "Chat with Zopu", name: "description" }, +]; + +export default function MobileChatPage() { + return ; +} diff --git a/apps/web/src/routes/app/mobile/layout.tsx b/apps/web/src/routes/app/mobile/layout.tsx new file mode 100644 index 0000000..ae63b03 --- /dev/null +++ b/apps/web/src/routes/app/mobile/layout.tsx @@ -0,0 +1,9 @@ +import { Outlet } from "react-router"; + +export default function MobileProductLayout() { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/routes/app/mobile/page.tsx b/apps/web/src/routes/app/mobile/page.tsx new file mode 100644 index 0000000..4e058d8 --- /dev/null +++ b/apps/web/src/routes/app/mobile/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Daily focus | Zopu" }, + { + content: "Supervise active work and open the next action", + name: "description", + }, +]; + +export default function MobileHomePage() { + return ; +} diff --git a/apps/web/src/routes/app/mobile/work-list/page.tsx b/apps/web/src/routes/app/mobile/work-list/page.tsx new file mode 100644 index 0000000..95421d2 --- /dev/null +++ b/apps/web/src/routes/app/mobile/work-list/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Active work | Zopu" }, + { + content: "Browse active work and expand a work unit in place", + name: "description", + }, +]; + +export default function MobileWorkListPage() { + return ; +} diff --git a/apps/web/src/routes/app/mobile/work/page.tsx b/apps/web/src/routes/app/mobile/work/page.tsx new file mode 100644 index 0000000..27a88fb --- /dev/null +++ b/apps/web/src/routes/app/mobile/work/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Work unit | Zopu" }, + { + content: "Review the state, next milestone, and activity for a work unit", + name: "description", + }, +]; + +export default function MobileWorkUnitPage() { + return ; +} diff --git a/apps/web/src/routes/app/todos/page.tsx b/apps/web/src/routes/app/todos/page.tsx new file mode 100644 index 0000000..ce4be3c --- /dev/null +++ b/apps/web/src/routes/app/todos/page.tsx @@ -0,0 +1,123 @@ +import { api } from "@code/backend/convex/_generated/api"; +import type { Id } from "@code/backend/convex/_generated/dataModel"; +import { Button } from "@code/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@code/ui/components/card"; +import { Checkbox } from "@code/ui/components/checkbox"; +import { Input } from "@code/ui/components/input"; +import { useMutation, useQuery } from "convex/react"; +import { Loader2, Trash2 } from "lucide-react"; +import { useState } from "react"; +import type { FormEvent, ReactNode } from "react"; + +export default function Todos() { + const [newTodoText, setNewTodoText] = useState(""); + + const todos = useQuery(api.todos.getAll); + const createTodo = useMutation(api.todos.create); + const toggleTodo = useMutation(api.todos.toggle); + const deleteTodo = useMutation(api.todos.deleteTodo); + + const handleAddTodo = async (event: FormEvent) => { + event.preventDefault(); + const text = newTodoText.trim(); + if (!text) { + return; + } + await createTodo({ text }); + setNewTodoText(""); + }; + + const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => { + void toggleTodo({ completed: !currentCompleted, id }); + }; + + const handleDeleteTodo = (id: Id<"todos">) => { + void deleteTodo({ id }); + }; + + let todoContent: ReactNode; + if (todos === undefined) { + todoContent = ( +
+ +
+ ); + } else if (todos.length === 0) { + todoContent = ( +

No todos yet. Add one above!

+ ); + } else { + todoContent = ( +
    + {todos.map((todo) => ( +
  • +
    + + handleToggleTodo(todo._id, todo.completed) + } + /> + +
    + +
  • + ))} +
+ ); + } + + return ( +
+ + + Todo List + Manage your tasks efficiently + + +
+ setNewTodoText(event.target.value)} + placeholder="Add a new task..." + value={newTodoText} + /> + +
+ + {todoContent} +
+
+
+ ); +} diff --git a/apps/web/src/routes/_auth.tsx b/apps/web/src/routes/auth/layout.tsx similarity index 65% rename from apps/web/src/routes/_auth.tsx rename to apps/web/src/routes/auth/layout.tsx index 4aa4173..dc8f4d8 100644 --- a/apps/web/src/routes/_auth.tsx +++ b/apps/web/src/routes/auth/layout.tsx @@ -5,10 +5,16 @@ export default function AuthLayout() { const { isAuthenticated, isLoading } = useConvexAuth(); if (isLoading) { - return
Loading…
; + return ( +
+ Loading… +
+ ); } - if (isAuthenticated) return ; + if (isAuthenticated) { + return ; + } return (
diff --git a/apps/web/src/routes/_auth.login.tsx b/apps/web/src/routes/auth/login/page.tsx similarity index 88% rename from apps/web/src/routes/_auth.login.tsx rename to apps/web/src/routes/auth/login/page.tsx index 676bdf9..167dfcc 100644 --- a/apps/web/src/routes/_auth.login.tsx +++ b/apps/web/src/routes/auth/login/page.tsx @@ -1,7 +1,7 @@ import { LoginForm } from "@code/auth/web"; import { useNavigate } from "react-router"; -import type { Route } from "./+types/_auth.login"; +import type { Route } from "./+types/page"; export const meta = (_args: Route.MetaArgs) => [ { title: "Sign in | Zopu" }, diff --git a/apps/web/src/routes/_auth.signup.tsx b/apps/web/src/routes/auth/signup/page.tsx similarity index 88% rename from apps/web/src/routes/_auth.signup.tsx rename to apps/web/src/routes/auth/signup/page.tsx index 4bb1dee..aad7dbe 100644 --- a/apps/web/src/routes/_auth.signup.tsx +++ b/apps/web/src/routes/auth/signup/page.tsx @@ -1,7 +1,7 @@ import { SignupForm } from "@code/auth/web"; import { useNavigate } from "react-router"; -import type { Route } from "./+types/_auth.signup"; +import type { Route } from "./+types/page"; export const meta = (_args: Route.MetaArgs) => [ { title: "Create account | Zopu" }, diff --git a/apps/web/src/routes/design.mobile-chat.tsx b/apps/web/src/routes/design.mobile-chat.tsx deleted file mode 100644 index 8fcd85d..0000000 --- a/apps/web/src/routes/design.mobile-chat.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { - Conversation, - ConversationContent, -} from "@code/ui/components/ai-elements/conversation"; -import { - Message, - MessageContent, -} from "@code/ui/components/ai-elements/message"; -import { Tool, ToolContent } from "@code/ui/components/ai-elements/tool"; -import { - MobileChatAssistantLabel, - MobileChatBubble, - MobileChatComposer, - MobileChatHeader, - MobileChatMessage, - MobileChatToolCall, - MobileChatToolResult, -} from "@code/ui/components/mobile-chat"; - -import type { Route } from "./+types/design.mobile-chat"; - -export const meta = (_args: Route.MetaArgs) => [ - { title: "Mobile chat components | Zopu" }, - { - content: "Mobile component showcase for the Zopu chat experience", - name: "description", - }, -]; - -const MobileChatShowcase = () => ( -
- - - - - - - - Summarize Q3 and find recent AI news. - - - - - - - - -
- - - - - - - - - - -

- Revenue grew 18% QoQ to $4.2M. Top risks: customer - concentration, sales cycles, and infra costs. -

-
-
-
-
-
-
-
- - event.preventDefault()} - textareaProps={{}} - /> -
-); - -export default MobileChatShowcase; diff --git a/apps/web/src/routes/design/chat/page.tsx b/apps/web/src/routes/design/chat/page.tsx new file mode 100644 index 0000000..c1301d7 --- /dev/null +++ b/apps/web/src/routes/design/chat/page.tsx @@ -0,0 +1,12 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Chat | Zopu" }, + { content: "Chat with Zopu", name: "description" }, +]; + +export default function PublicMobileChatPage() { + return ; +} diff --git a/apps/web/src/routes/design/layout.tsx b/apps/web/src/routes/design/layout.tsx new file mode 100644 index 0000000..df53ece --- /dev/null +++ b/apps/web/src/routes/design/layout.tsx @@ -0,0 +1,9 @@ +import { Outlet } from "react-router"; + +export default function DesignLayout() { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/routes/design/page.tsx b/apps/web/src/routes/design/page.tsx new file mode 100644 index 0000000..d76a4c3 --- /dev/null +++ b/apps/web/src/routes/design/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Zopu" }, + { + content: "Review active work and open the next action", + name: "description", + }, +]; + +export default function PublicMobileHomePage() { + return ; +} diff --git a/apps/web/src/routes/design/work-unit/page.tsx b/apps/web/src/routes/design/work-unit/page.tsx new file mode 100644 index 0000000..6f8d914 --- /dev/null +++ b/apps/web/src/routes/design/work-unit/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Work unit | Zopu" }, + { + content: "Review the state, next milestone, and activity for a work unit", + name: "description", + }, +]; + +export default function PublicMobileWorkUnitPage() { + return ; +} diff --git a/apps/web/src/routes/design/work/page.tsx b/apps/web/src/routes/design/work/page.tsx new file mode 100644 index 0000000..dcafd8a --- /dev/null +++ b/apps/web/src/routes/design/work/page.tsx @@ -0,0 +1,15 @@ +import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page"; + +import type { Route } from "./+types/page"; + +export const meta = (_args: Route.MetaArgs) => [ + { title: "Active work | Zopu" }, + { + content: "Browse active work and expand a work unit in place", + name: "description", + }, +]; + +export default function PublicMobileWorkPage() { + return ; +} diff --git a/packages/ui/src/components/mobile-product.tsx b/packages/ui/src/components/mobile-product.tsx new file mode 100644 index 0000000..0bd4f06 --- /dev/null +++ b/packages/ui/src/components/mobile-product.tsx @@ -0,0 +1,8 @@ +export { + MobileAssistantChatScreen, + MobileExpandedWorkScreen, + MobileHomeScreen, + MobileWorkListScreen, + MobileWorkUnitDetailScreen, +} from "./mobile-workspace/index"; +export type { MobileWorkspaceScreenProps } from "./mobile-workspace/index"; diff --git a/packages/ui/src/components/mobile-workspace-screens.tsx b/packages/ui/src/components/mobile-workspace-screens.tsx new file mode 100644 index 0000000..25ff921 --- /dev/null +++ b/packages/ui/src/components/mobile-workspace-screens.tsx @@ -0,0 +1,1168 @@ +import { cn } from "@code/ui/lib/utils"; +import { + ArrowLeft, + ArrowUp, + Check, + ChevronRight, + FileText, + ListChecks, + Mic, + MoreHorizontal, + Paperclip, + Plus, + Sparkles, + X, +} from "lucide-react"; +import type { FormEvent, ReactNode } from "react"; + +export type MobileWorkspaceVariant = + | "character-pass" + | "expanded-work-unit" + | "home-expanded-in-place" + | "stacked-final" + | "stacked-final-check" + | "stacked-work-units" + | "vertical-work-stack" + | "work-units-home"; + +export interface MobileWorkspaceRendererProps { + composerValue: string; + onBack?: () => void; + onComposerChange: (value: string) => void; + onComposerSubmit: (event: FormEvent) => void; + onOpenAssistant?: () => void; + onOpenUnit?: () => void; + onViewWork?: () => void; + statusMessage?: string; + variant: MobileWorkspaceVariant; +} + +const BrandGlyph = ({ className }: { className?: string }) => ( + +); + +interface ProductHeaderProps { + assistant?: boolean; + avatar?: boolean; + letterLogo?: boolean; + onAssistant?: () => void; + profileInset?: boolean; + subtitle?: string; +} + +const ProductHeader = ({ + assistant = false, + avatar = false, + letterLogo = false, + onAssistant, + profileInset = false, + subtitle = "Workspace is active", +}: ProductHeaderProps) => ( +
+
+ {letterLogo ? "Z" : } +
+
+

+ Zopu +

+

+ + {subtitle} +

+
+ {avatar ? ( + <> + {assistant ? ( + + ) : null} + + + ) : ( + <> + + + + )} +
+); + +interface ComposerProps { + contextLabel?: string; + deviceHomeIndicator?: boolean; + hint?: string; + homeIndicator?: boolean; + onChange: (value: string) => void; + onSubmit: (event: FormEvent) => void; + placeholder: string; + sendBlue?: boolean; + statusMessage?: string; + value: string; +} + +const ReferenceComposer = ({ + contextLabel, + deviceHomeIndicator = false, + hint, + homeIndicator = false, + onChange, + onSubmit, + placeholder, + sendBlue = false, + statusMessage, + value, +}: ComposerProps) => ( +