integrate thin Zopu MVP lanes (#14) #16
@@ -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 <MobileAssistantChatScreen {...screenProps} />;
|
||||
}
|
||||
if (screen === "home") {
|
||||
return <MobileHomeScreen {...screenProps} />;
|
||||
}
|
||||
if (screen === "work-unit-detail") {
|
||||
return <MobileWorkUnitDetailScreen {...screenProps} />;
|
||||
}
|
||||
if (workspace.expanded) {
|
||||
return <MobileExpandedWorkScreen {...screenProps} />;
|
||||
}
|
||||
return <MobileWorkListScreen {...screenProps} />;
|
||||
};
|
||||
35
apps/web/src/hooks/use-mobile-workspace.ts
Normal file
35
apps/web/src/hooks/use-mobile-workspace.ts
Normal file
@@ -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<string>();
|
||||
|
||||
const handleComposerChange = (value: string) => {
|
||||
setComposerValue(value);
|
||||
setStatusMessage(undefined);
|
||||
};
|
||||
|
||||
const handleComposerSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,6 @@
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ai-conversation-mobile > div {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<MobileViewport>
|
||||
<Outlet />
|
||||
</MobileViewport>
|
||||
<Outlet />
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</WebAuthProvider>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => <ChatPage />;
|
||||
|
||||
export default Home;
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="w-full mx-auto max-w-md py-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Todo List</CardTitle>
|
||||
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleAddTodo} className="mb-6 flex items-center space-x-2">
|
||||
<Input
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
/>
|
||||
<Button type="submit" disabled={!newTodoText.trim()}>
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todos === undefined ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : todos.length === 0 ? (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
key={todo._id}
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() => handleToggleTodo(todo._id, todo.completed)}
|
||||
id={`todo-${todo._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
className={`${todo.completed ? "line-through text-muted-foreground" : ""}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
aria-label="Delete todo"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,11 @@ export default function AppLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading…</div>;
|
||||
return (
|
||||
<div className="grid min-h-svh place-items-center text-sm text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
12
apps/web/src/routes/app/mobile/chat/page.tsx
Normal file
12
apps/web/src/routes/app/mobile/chat/page.tsx
Normal file
@@ -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 <MobileFlowPage screen="assistant-chat" />;
|
||||
}
|
||||
9
apps/web/src/routes/app/mobile/layout.tsx
Normal file
9
apps/web/src/routes/app/mobile/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function MobileProductLayout() {
|
||||
return (
|
||||
<div className="min-h-svh bg-[#11110f]">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/page.tsx
Normal file
@@ -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 <MobileFlowPage screen="home" />;
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/work-list/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/work-list/page.tsx
Normal file
@@ -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 <MobileFlowPage screen="work-list" />;
|
||||
}
|
||||
15
apps/web/src/routes/app/mobile/work/page.tsx
Normal file
15
apps/web/src/routes/app/mobile/work/page.tsx
Normal file
@@ -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 <MobileFlowPage screen="work-unit-detail" />;
|
||||
}
|
||||
123
apps/web/src/routes/app/todos/page.tsx
Normal file
123
apps/web/src/routes/app/todos/page.tsx
Normal file
@@ -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<HTMLFormElement>) => {
|
||||
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 = (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
} else if (todos.length === 0) {
|
||||
todoContent = (
|
||||
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||
);
|
||||
} else {
|
||||
todoContent = (
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo) => (
|
||||
<li
|
||||
className="flex items-center justify-between rounded-md border p-2"
|
||||
key={todo._id}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
id={`todo-${todo._id}`}
|
||||
onCheckedChange={() =>
|
||||
handleToggleTodo(todo._id, todo.completed)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className={
|
||||
todo.completed
|
||||
? "text-muted-foreground line-through"
|
||||
: undefined
|
||||
}
|
||||
htmlFor={`todo-${todo._id}`}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Delete todo"
|
||||
onClick={() => handleDeleteTodo(todo._id)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md py-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Todo List</CardTitle>
|
||||
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
className="mb-6 flex items-center space-x-2"
|
||||
onSubmit={handleAddTodo}
|
||||
>
|
||||
<Input
|
||||
onChange={(event) => setNewTodoText(event.target.value)}
|
||||
placeholder="Add a new task..."
|
||||
value={newTodoText}
|
||||
/>
|
||||
<Button disabled={!newTodoText.trim()} type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{todoContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,16 @@ export default function AuthLayout() {
|
||||
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading…</div>;
|
||||
return (
|
||||
<div className="grid min-h-svh place-items-center text-sm text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) return <Navigate replace to="/" />;
|
||||
if (isAuthenticated) {
|
||||
return <Navigate replace to="/" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="grid min-h-svh place-items-center bg-muted/30 p-6 md:p-10">
|
||||
@@ -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" },
|
||||
@@ -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" },
|
||||
@@ -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 = () => (
|
||||
<section className="flex h-full min-h-0 flex-col bg-[#fefefe]">
|
||||
<MobileChatHeader active statusLabel="Ready" />
|
||||
<Conversation className="ai-conversation-mobile min-h-0 flex-1">
|
||||
<ConversationContent className="min-h-full w-full gap-0 px-4 pt-4 pb-6">
|
||||
<Message className="max-w-full gap-0" from="user">
|
||||
<MessageContent className="w-full max-w-none gap-0 overflow-visible rounded-none bg-transparent p-0 group-[.is-user]:rounded-none group-[.is-user]:bg-transparent group-[.is-user]:p-0">
|
||||
<MobileChatMessage sender="user">
|
||||
<MobileChatBubble sender="user">
|
||||
Summarize Q3 and find recent AI news.
|
||||
</MobileChatBubble>
|
||||
</MobileChatMessage>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
|
||||
<Message className="mt-5 max-w-full gap-0" from="assistant">
|
||||
<MessageContent className="w-full max-w-none gap-0 overflow-visible p-0">
|
||||
<MobileChatMessage sender="assistant">
|
||||
<div className="w-full min-w-0">
|
||||
<MobileChatAssistantLabel />
|
||||
<MobileChatBubble sender="assistant">
|
||||
<Tool className="mb-0 w-full border-0" defaultOpen>
|
||||
<ToolContent className="space-y-0 p-0">
|
||||
<MobileChatToolCall
|
||||
status="done"
|
||||
tone="success"
|
||||
toolName="Web Search"
|
||||
>
|
||||
<MobileChatToolResult
|
||||
source="news.ycombinator.com"
|
||||
title="AI funding hits record highs in 2026"
|
||||
/>
|
||||
<MobileChatToolResult
|
||||
source="news.ycombinator.com"
|
||||
title="Show HN: Series B raised for new AI lab"
|
||||
/>
|
||||
</MobileChatToolCall>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
<p>
|
||||
Revenue grew 18% QoQ to $4.2M. Top risks: customer
|
||||
concentration, sales cycles, and infra costs.
|
||||
</p>
|
||||
</MobileChatBubble>
|
||||
</div>
|
||||
</MobileChatMessage>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</ConversationContent>
|
||||
</Conversation>
|
||||
|
||||
<MobileChatComposer
|
||||
canSend
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
textareaProps={{}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
export default MobileChatShowcase;
|
||||
12
apps/web/src/routes/design/chat/page.tsx
Normal file
12
apps/web/src/routes/design/chat/page.tsx
Normal file
@@ -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 <MobileFlowPage publicDemo screen="assistant-chat" />;
|
||||
}
|
||||
9
apps/web/src/routes/design/layout.tsx
Normal file
9
apps/web/src/routes/design/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function DesignLayout() {
|
||||
return (
|
||||
<div className="min-h-svh bg-[#11110f]">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
apps/web/src/routes/design/page.tsx
Normal file
15
apps/web/src/routes/design/page.tsx
Normal file
@@ -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 <MobileFlowPage publicDemo screen="home" />;
|
||||
}
|
||||
15
apps/web/src/routes/design/work-unit/page.tsx
Normal file
15
apps/web/src/routes/design/work-unit/page.tsx
Normal file
@@ -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 <MobileFlowPage publicDemo screen="work-unit-detail" />;
|
||||
}
|
||||
15
apps/web/src/routes/design/work/page.tsx
Normal file
15
apps/web/src/routes/design/work/page.tsx
Normal file
@@ -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 <MobileFlowPage publicDemo screen="work-list" />;
|
||||
}
|
||||
8
packages/ui/src/components/mobile-product.tsx
Normal file
8
packages/ui/src/components/mobile-product.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
MobileAssistantChatScreen,
|
||||
MobileExpandedWorkScreen,
|
||||
MobileHomeScreen,
|
||||
MobileWorkListScreen,
|
||||
MobileWorkUnitDetailScreen,
|
||||
} from "./mobile-workspace/index";
|
||||
export type { MobileWorkspaceScreenProps } from "./mobile-workspace/index";
|
||||
1168
packages/ui/src/components/mobile-workspace-screens.tsx
Normal file
1168
packages/ui/src/components/mobile-workspace-screens.tsx
Normal file
File diff suppressed because it is too large
Load Diff
937
packages/ui/src/components/mobile-workspace.tsx
Normal file
937
packages/ui/src/components/mobile-workspace.tsx
Normal file
@@ -0,0 +1,937 @@
|
||||
import { cn } from "@code/ui/lib/utils";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
Check,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Mic,
|
||||
MoreHorizontal,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Sparkles,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
ComponentProps,
|
||||
FormEvent,
|
||||
KeyboardEvent,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
type WorkUnitTone = "blocked" | "progress" | "ready" | "research";
|
||||
|
||||
interface WorkUnitSummary {
|
||||
code: string;
|
||||
description: string;
|
||||
id: string;
|
||||
progress?: number;
|
||||
status: string;
|
||||
title: string;
|
||||
tone: WorkUnitTone;
|
||||
}
|
||||
|
||||
interface WorkUnitDetail extends WorkUnitSummary {
|
||||
activity: {
|
||||
detail: string;
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
artifacts: number;
|
||||
currentState: string;
|
||||
nextMilestone: string;
|
||||
nextMilestoneDetail: string;
|
||||
weeklyActivity: number;
|
||||
}
|
||||
|
||||
const toneClasses: Record<
|
||||
WorkUnitTone,
|
||||
{
|
||||
badge: string;
|
||||
card: string;
|
||||
copy: string;
|
||||
muted: string;
|
||||
}
|
||||
> = {
|
||||
blocked: {
|
||||
badge: "bg-[#ffe0cf] text-[#c53c08]",
|
||||
card: "bg-[#fff0e8]",
|
||||
copy: "text-[#8a2e13]",
|
||||
muted: "text-[#c68168]",
|
||||
},
|
||||
progress: {
|
||||
badge: "bg-[#d6e2ff] text-[#315dc0]",
|
||||
card: "bg-[#e8efff]",
|
||||
copy: "text-[#14265f]",
|
||||
muted: "text-[#6e83b4]",
|
||||
},
|
||||
ready: {
|
||||
badge: "bg-[#dff4e8] text-[#238050]",
|
||||
card: "bg-[#e8f7ef]",
|
||||
copy: "text-[#105c35]",
|
||||
muted: "text-[#61a17e]",
|
||||
},
|
||||
research: {
|
||||
badge: "bg-[#e7d8ff] text-[#7137ef]",
|
||||
card: "bg-[#f1e8ff]",
|
||||
copy: "text-[#3e0b74]",
|
||||
muted: "text-[#9a7ac5]",
|
||||
},
|
||||
};
|
||||
|
||||
const MobileWorkspaceHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<"header">) => (
|
||||
<header
|
||||
className={cn(
|
||||
"flex h-[72px] shrink-0 items-center bg-[#fbfcf9] px-4",
|
||||
className
|
||||
)}
|
||||
data-slot="mobile-workspace-header"
|
||||
{...props}
|
||||
>
|
||||
<div className="flex size-10 items-center justify-center rounded-[14px] bg-[#c7ff00] text-xl font-bold text-black">
|
||||
Z
|
||||
</div>
|
||||
<div className="ml-3 min-w-0">
|
||||
<h1 className="text-[20px] leading-6 font-semibold tracking-[-0.025em] text-[#11110f]">
|
||||
Zopu
|
||||
</h1>
|
||||
<p className="mt-0.5 flex items-center gap-2 text-[13px] text-[#77736f]">
|
||||
<span className="size-1.5 rounded-full bg-[#62a91e]" />4 active work
|
||||
units
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Open Zopu assistant"
|
||||
className="ml-auto flex size-10 items-center justify-center rounded-full bg-[#f1f2ee] text-[9px] font-semibold text-[#11110f] transition-transform active:scale-95"
|
||||
type="button"
|
||||
>
|
||||
ZOPU
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open profile"
|
||||
className="ml-2 flex size-10 items-center justify-center rounded-full bg-[#0b0b0a] text-[13px] font-semibold text-white transition-transform active:scale-95"
|
||||
type="button"
|
||||
>
|
||||
YM
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
interface MobileCommandComposerProps extends Omit<
|
||||
ComponentProps<"form">,
|
||||
"onChange" | "onSubmit"
|
||||
> {
|
||||
contextLabel?: string;
|
||||
hint?: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
placeholder: string;
|
||||
statusMessage?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const handleCommandKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const MobileCommandComposer = ({
|
||||
className,
|
||||
contextLabel,
|
||||
hint = "Use @ to reference a work unit",
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
statusMessage,
|
||||
value,
|
||||
...props
|
||||
}: MobileCommandComposerProps) => (
|
||||
<footer
|
||||
className="shrink-0 border-t border-[#eff0ec] bg-[#fbfcf9] px-3 pt-2.5 pb-[max(12px,env(safe-area-inset-bottom))]"
|
||||
data-slot="mobile-command-composer"
|
||||
>
|
||||
{contextLabel ? (
|
||||
<div className="mb-2 flex h-6 w-fit items-center gap-2 rounded-full bg-[#dce7ff] px-2.5 text-[10px] font-semibold text-[#315dc0]">
|
||||
<span className="size-1.5 rounded-full bg-[#315dc0]" />
|
||||
{contextLabel}
|
||||
<X className="size-3" />
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
className={cn(
|
||||
"flex min-h-[62px] items-center rounded-[31px] bg-[#f1f2ef] p-2",
|
||||
className
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
>
|
||||
<button
|
||||
aria-label="Add attachment"
|
||||
className="flex size-11 shrink-0 items-center justify-center rounded-full bg-white text-[#5d5a56] transition-transform active:scale-95"
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-5" strokeWidth={1.8} />
|
||||
</button>
|
||||
<label className="ml-2 min-w-0 flex-1">
|
||||
<span className="sr-only">{placeholder}</span>
|
||||
<textarea
|
||||
className="block max-h-20 min-h-5 w-full resize-none bg-transparent text-[14px] leading-5 text-[#24231f] outline-none placeholder:text-[#b1adaa]"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onKeyDown={handleCommandKeyDown}
|
||||
placeholder={placeholder}
|
||||
rows={1}
|
||||
value={value}
|
||||
/>
|
||||
<span className="block truncate text-[10px] leading-4 text-[#c0bcb8]">
|
||||
{statusMessage ?? hint}
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
aria-label="Send"
|
||||
className="ml-2 flex size-11 shrink-0 items-center justify-center rounded-full bg-[#0b0b0a] text-white transition-transform active:scale-95 disabled:bg-[#d7d7d2] disabled:text-[#999792]"
|
||||
disabled={!value.trim()}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowUp className="size-5" strokeWidth={2.2} />
|
||||
</button>
|
||||
</form>
|
||||
</footer>
|
||||
);
|
||||
|
||||
const WorkStatusBadge = ({
|
||||
className,
|
||||
tone,
|
||||
...props
|
||||
}: ComponentProps<"span"> & { tone: WorkUnitTone }) => (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-6 items-center rounded-full px-2 text-[10px] leading-none font-semibold",
|
||||
toneClasses[tone].badge,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
interface FocusWorkCardProps extends ComponentProps<"article"> {
|
||||
onOpen: () => void;
|
||||
unit: WorkUnitSummary;
|
||||
}
|
||||
|
||||
const FocusWorkCard = ({
|
||||
className,
|
||||
onOpen,
|
||||
unit,
|
||||
...props
|
||||
}: FocusWorkCardProps) => (
|
||||
<article
|
||||
className={cn(
|
||||
"absolute inset-x-5 top-[86px] z-30 min-h-[365px] rounded-[30px] px-5 pt-5 pb-4",
|
||||
toneClasses[unit.tone].card,
|
||||
toneClasses[unit.tone].copy,
|
||||
className
|
||||
)}
|
||||
data-slot="focus-work-card"
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkStatusBadge tone={unit.tone}>{unit.status}</WorkStatusBadge>
|
||||
<span className={cn("text-[10px]", toneClasses[unit.tone].muted)}>
|
||||
{unit.code}
|
||||
</span>
|
||||
<MoreHorizontal className="ml-auto size-5" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-[25px] leading-[1.05] font-semibold tracking-[-0.045em]">
|
||||
{unit.title}
|
||||
</h3>
|
||||
<p className={cn("mt-2 text-[13px]", toneClasses[unit.tone].muted)}>
|
||||
Customer experience · Updated 4 min ago
|
||||
</p>
|
||||
<div className="mt-3 border-t border-[#315dc0]/15 pt-3">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">CURRENT STATE</p>
|
||||
<p className="mt-1 line-clamp-2 text-[14px] leading-5 text-[#33405c]">
|
||||
{unit.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 h-2 overflow-hidden rounded-full bg-[#315dc0]/15">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#315dc0]"
|
||||
style={{ width: `${unit.progress ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 flex text-[10px] font-semibold text-[#315dc0]">
|
||||
<span>{unit.progress}% COMPLETE</span>
|
||||
<span className="ml-auto font-normal text-[#6e83b4]">On track</span>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center border-t border-[#315dc0]/15 pt-4">
|
||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-[14px] bg-white text-[#315dc0]">
|
||||
<Check className="size-5" />
|
||||
</span>
|
||||
<div className="ml-2.5 min-w-0">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">NEXT ACTION</p>
|
||||
<p className="truncate text-[14px] font-semibold">
|
||||
Review welcome copy
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">About 5 minutes</p>
|
||||
</div>
|
||||
<button
|
||||
className="ml-auto flex h-12 shrink-0 items-center gap-1 rounded-[19px] bg-[#c7ff00] px-4 text-[13px] font-semibold text-black transition-transform active:scale-[0.98]"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
>
|
||||
Open unit
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
interface MobileStackedFocusProps extends Omit<
|
||||
ComponentProps<"main">,
|
||||
"onSelect"
|
||||
> {
|
||||
activeIndex: number;
|
||||
onOpen: () => void;
|
||||
onSelect: (index: number) => void;
|
||||
units: WorkUnitSummary[];
|
||||
}
|
||||
|
||||
const MobileStackedFocus = ({
|
||||
activeIndex,
|
||||
className,
|
||||
onOpen,
|
||||
onSelect,
|
||||
units,
|
||||
...props
|
||||
}: MobileStackedFocusProps) => {
|
||||
const activeUnit = units[activeIndex] ?? units[0];
|
||||
if (!activeUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto bg-[#f7f8f5] px-5 pt-7",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-[29px] font-semibold tracking-[-0.045em] text-[#11110f]">
|
||||
Daily focus
|
||||
</h2>
|
||||
<span className="ml-auto rounded-full bg-white px-3 py-1.5 text-[11px] font-semibold text-[#5f5b57]">
|
||||
MON 20
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[14px] text-[#807b76]">
|
||||
One priority is moving. Two items need attention.
|
||||
</p>
|
||||
|
||||
<section
|
||||
aria-label="Active work units"
|
||||
className="relative mx-[-20px] mt-5 h-[502px]"
|
||||
>
|
||||
<button
|
||||
className="absolute inset-x-16 top-0 z-10 flex h-24 items-start gap-2 rounded-[26px] bg-[#f1e8ff] px-4 pt-4 text-left"
|
||||
onClick={() => onSelect(0)}
|
||||
type="button"
|
||||
>
|
||||
<WorkStatusBadge tone="research">RESEARCHING</WorkStatusBadge>
|
||||
<span className="pt-1 text-[10px] text-[#9a7ac5]">WORK-402</span>
|
||||
</button>
|
||||
<button
|
||||
className="absolute inset-x-11 top-[37px] z-20 flex h-24 items-start gap-2 rounded-[27px] bg-[#fff0e8] px-4 pt-4 text-left"
|
||||
onClick={() => onSelect(1)}
|
||||
type="button"
|
||||
>
|
||||
<WorkStatusBadge tone="blocked">BLOCKED</WorkStatusBadge>
|
||||
<span className="pt-1 text-[10px] text-[#c68168]">BUG-12</span>
|
||||
</button>
|
||||
<FocusWorkCard onOpen={onOpen} unit={activeUnit} />
|
||||
<div
|
||||
aria-label={`Work unit ${activeIndex + 1} of ${units.length}`}
|
||||
className="absolute inset-x-0 bottom-3 flex justify-center gap-2"
|
||||
>
|
||||
{units.map((unit, index) => (
|
||||
<button
|
||||
aria-label={`Show ${unit.title}`}
|
||||
className={cn(
|
||||
"size-1.5 rounded-full bg-[#d3d3cf]",
|
||||
index === activeIndex && "w-6 bg-[#11110f]"
|
||||
)}
|
||||
key={unit.id}
|
||||
onClick={() => onSelect(index)}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
interface MobileWorkFeedProps extends ComponentProps<"main"> {
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
const MobileWorkFeed = ({
|
||||
className,
|
||||
onOpen,
|
||||
...props
|
||||
}: MobileWorkFeedProps) => (
|
||||
<main
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto bg-[#f7f8f5] px-4 pt-3 pb-5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<h2 className="text-[29px] font-semibold tracking-[-0.045em]">
|
||||
Good morning.
|
||||
</h2>
|
||||
<p className="mt-1 text-[14px] text-[#807b76]">
|
||||
Your work is moving. Two things need you.
|
||||
</p>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
["ACTIVE", "4 units", "bg-white text-black"],
|
||||
["NEEDS YOU", "2 blockers", "bg-[#fff0e8] text-[#9a3215]"],
|
||||
["SHIPPED", "3 this week", "bg-[#e8f7ef] text-[#23633d]"],
|
||||
].map(([label, value, color]) => (
|
||||
<div
|
||||
className={cn("h-14 rounded-[18px] px-2.5 py-2", color)}
|
||||
key={label}
|
||||
>
|
||||
<p className="text-[9px] font-semibold">{label}</p>
|
||||
<p className="mt-0.5 whitespace-nowrap text-[14px] font-semibold">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 flex items-center">
|
||||
<h3 className="text-[14px] font-semibold">Now</h3>
|
||||
<span className="ml-auto text-[12px] text-[#807b76]">View all 6</span>
|
||||
</div>
|
||||
<article className="mt-3 rounded-[27px] bg-[#e8efff] p-3.5 text-[#14265f]">
|
||||
<div className="flex items-center text-[11px] font-semibold text-[#315dc0]">
|
||||
IN PROGRESS · 68%
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</div>
|
||||
<h3 className="mt-3 text-[20px] font-semibold tracking-[-0.03em]">
|
||||
Launch the new onboarding flow
|
||||
</h3>
|
||||
<p className="mt-1 line-clamp-2 text-[13px] text-[#526482]">
|
||||
First-run flow mapped. Zopu is drafting the welcome sequence.
|
||||
</p>
|
||||
<div className="mt-4 h-2 rounded-full bg-[#315dc0]/15">
|
||||
<div className="h-full w-[68%] rounded-full bg-[#315dc0]" />
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<div className="min-w-0 flex-1 rounded-[16px] bg-white px-3 py-2">
|
||||
<p className="text-[9px] font-semibold text-[#8a97b5]">NEXT ACTION</p>
|
||||
<p className="truncate text-[12px] font-semibold">
|
||||
Review the welcome copy
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-[16px] bg-[#c7ff00] px-4 text-[12px] font-semibold text-black active:scale-[0.98]"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
>
|
||||
Open unit →
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<article className="mt-3 rounded-[25px] bg-[#fff0e8] p-3.5 text-[#8a2e13]">
|
||||
<p className="text-[11px] font-semibold text-[#c53c08]">
|
||||
● WAITING ON YOU
|
||||
</p>
|
||||
<h3 className="mt-3 text-[18px] font-semibold">
|
||||
Fix GitHub authentication bug
|
||||
</h3>
|
||||
<p className="mt-1 text-[12px]">Blocked by missing Safari repro logs</p>
|
||||
<button
|
||||
className="mt-3 ml-auto block rounded-[14px] bg-white px-5 py-2 text-[11px] font-semibold"
|
||||
type="button"
|
||||
>
|
||||
Add logs
|
||||
</button>
|
||||
</article>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<article className="rounded-[24px] bg-[#f1e8ff] p-4 text-[#3e0b74]">
|
||||
<p className="text-[10px] font-semibold text-[#7137ef]">RESEARCHING</p>
|
||||
<h3 className="mt-3 text-[15px] font-semibold">Pricing page rewrite</h3>
|
||||
<p className="mt-1 text-[11px] text-[#7f7190]">8 competitors</p>
|
||||
</article>
|
||||
<article className="rounded-[24px] bg-[#e8f7ef] p-4 text-[#105c35]">
|
||||
<p className="text-[10px] font-semibold text-[#238050]">READY</p>
|
||||
<h3 className="mt-3 text-[15px] font-semibold">Q3 launch brief</h3>
|
||||
<p className="mt-1 text-[11px] text-[#668678]">Ready for review</p>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
interface MobileVerticalWorkStackProps extends ComponentProps<"main"> {
|
||||
expanded: boolean;
|
||||
onOpen: () => void;
|
||||
onToggleExpanded: () => void;
|
||||
}
|
||||
|
||||
const MobileVerticalWorkStack = ({
|
||||
className,
|
||||
expanded,
|
||||
onOpen,
|
||||
onToggleExpanded,
|
||||
...props
|
||||
}: MobileVerticalWorkStackProps) => (
|
||||
<main
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto bg-[#f7f8f5] px-5 pt-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-[29px] font-semibold tracking-[-0.045em]">
|
||||
Work in motion
|
||||
</h2>
|
||||
<span className="ml-auto rounded-full bg-white px-3 py-1.5 text-[11px] font-semibold text-[#5f5b57]">
|
||||
MON 20
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[14px] text-[#807b76]">
|
||||
{expanded
|
||||
? "One work unit is expanded in the stack."
|
||||
: "Scroll vertically. Tap a card to expand it in place."}
|
||||
</p>
|
||||
<div className="mt-8 flex text-[12px]">
|
||||
<span className="font-semibold">ACTIVE WORK</span>
|
||||
<span className="ml-auto text-[#807b76]">4 units</span>
|
||||
</div>
|
||||
<section className="relative mx-[-4px] mt-3 h-[550px]">
|
||||
<button
|
||||
className="absolute inset-x-3 top-0 h-[150px] rounded-[28px] bg-[#f1e8ff] p-5 text-left text-[#3e0b74]"
|
||||
onClick={onToggleExpanded}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-[10px] font-semibold text-[#7137ef]">
|
||||
RESEARCHING
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#9a7ac5]">WORK-402</span>
|
||||
<strong className="mt-4 block text-[22px]">Rewrite pricing page</strong>
|
||||
</button>
|
||||
<button
|
||||
className="absolute inset-x-1 top-[70px] h-[150px] rounded-[28px] bg-[#fff0e8] p-5 text-left text-[#8a2e13]"
|
||||
onClick={onToggleExpanded}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-[10px] font-semibold text-[#c53c08]">
|
||||
BLOCKED
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#c68168]">BUG-12</span>
|
||||
<strong className="mt-4 block text-[22px]">
|
||||
Fix GitHub authentication
|
||||
</strong>
|
||||
</button>
|
||||
<article className="absolute inset-x-[-3px] top-[142px] h-[150px] rounded-[28px] bg-[#e8f7ef] p-5 text-[#105c35]">
|
||||
<span className="text-[10px] font-semibold text-[#238050]">
|
||||
READY FOR REVIEW
|
||||
</span>
|
||||
<span className="float-right text-[10px] text-[#61a17e]">BRIEF-03</span>
|
||||
<strong className="mt-4 block text-[22px]">Q3 launch brief</strong>
|
||||
</article>
|
||||
<article
|
||||
className={cn(
|
||||
"absolute inset-x-[-7px] top-[216px] rounded-[30px] bg-[#e8efff] p-5 text-[#14265f] transition-[height] duration-300",
|
||||
expanded ? "h-[490px]" : "h-[330px]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<WorkStatusBadge tone="progress">IN PROGRESS</WorkStatusBadge>
|
||||
<span className="ml-auto text-[10px] text-[#6e83b4]">FLOW-08</span>
|
||||
</div>
|
||||
<h3 className="mt-4 text-[24px] leading-[1.08] font-semibold tracking-[-0.04em]">
|
||||
Launch onboarding flow
|
||||
</h3>
|
||||
<p className="mt-2 text-[13px] text-[#60718e]">
|
||||
Welcome copy and activation checklist are being drafted.
|
||||
</p>
|
||||
<div className="mt-4 h-2 rounded-full bg-[#315dc0]/15">
|
||||
<div className="h-full w-[68%] rounded-full bg-[#315dc0]" />
|
||||
</div>
|
||||
<div className="mt-2 flex text-[10px] font-semibold text-[#315dc0]">
|
||||
68% COMPLETE
|
||||
<span className="ml-auto font-normal text-[#6e83b4]">On track</span>
|
||||
</div>
|
||||
<button
|
||||
className="mt-5 w-full border-t border-[#315dc0]/15 pt-4 text-left"
|
||||
onClick={onOpen}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-[10px] font-semibold text-[#6e83b4]">
|
||||
NEXT MILESTONE
|
||||
</span>
|
||||
<span className="mt-1 block text-[14px] font-semibold">
|
||||
Review welcome copy →
|
||||
</span>
|
||||
</button>
|
||||
{expanded ? (
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-[18px] bg-[#dce6fc] p-3">
|
||||
<span className="text-[10px] text-[#6e83b4]">ARTIFACTS</span>
|
||||
<strong className="mt-1 block text-xl">2</strong>
|
||||
</div>
|
||||
<div className="rounded-[18px] bg-[#dce6fc] p-3">
|
||||
<span className="text-[10px] text-[#6e83b4]">ACTIVITY</span>
|
||||
<strong className="mt-1 block text-xl">12</strong>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
interface MobileWorkUnitDetailProps extends ComponentProps<"main"> {
|
||||
unit: WorkUnitDetail;
|
||||
}
|
||||
|
||||
const MobileWorkUnitDetail = ({
|
||||
className,
|
||||
unit,
|
||||
...props
|
||||
}: MobileWorkUnitDetailProps) => (
|
||||
<main
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto bg-[#f7f8f5] px-4 py-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<article className="rounded-[30px] bg-[#e8efff] p-5 text-[#14265f]">
|
||||
<div className="flex items-center">
|
||||
<WorkStatusBadge tone="progress">{unit.status}</WorkStatusBadge>
|
||||
<span className="ml-auto text-[11px] text-[#6e83b4]">
|
||||
{unit.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="mt-5 text-[29px] leading-[1.05] font-semibold tracking-[-0.045em]">
|
||||
{unit.title}
|
||||
</h1>
|
||||
<p className="mt-8 text-[14px] leading-5 text-[#60718e]">
|
||||
{unit.description}
|
||||
</p>
|
||||
<div className="mt-4 h-2 rounded-full bg-[#315dc0]/15">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#315dc0]"
|
||||
style={{ width: `${unit.progress ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex text-[10px] font-semibold text-[#315dc0]">
|
||||
ON TRACK
|
||||
<span className="ml-auto font-normal text-[#6e83b4]">
|
||||
Updated 4m ago
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-5 border-t border-[#315dc0]/15 pt-4">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">
|
||||
CURRENT STATE
|
||||
</p>
|
||||
<p className="mt-1 text-[14px] leading-5 text-[#33405c]">
|
||||
{unit.currentState}
|
||||
</p>
|
||||
</div>
|
||||
<section className="mt-5 flex items-center rounded-[19px] bg-white p-3">
|
||||
<span className="flex size-10 items-center justify-center rounded-[13px] bg-[#c7ff00] text-black">
|
||||
<Check className="size-5" />
|
||||
</span>
|
||||
<div className="ml-3 min-w-0">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">
|
||||
NEXT MILESTONE
|
||||
</p>
|
||||
<p className="truncate text-[14px] font-semibold">
|
||||
{unit.nextMilestone}
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">
|
||||
{unit.nextMilestoneDetail}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-[19px] bg-[#dce6fc] p-3">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">ARTIFACTS</p>
|
||||
<strong className="mt-1 block text-xl">{unit.artifacts}</strong>
|
||||
<span className="text-[10px] text-[#60718e]">
|
||||
Flow map · Copy draft
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-[19px] bg-[#dce6fc] p-3">
|
||||
<p className="text-[10px] font-semibold text-[#6e83b4]">ACTIVITY</p>
|
||||
<strong className="mt-1 block text-xl">{unit.weeklyActivity}</strong>
|
||||
<span className="text-[10px] text-[#60718e]">Actions this week</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center text-[11px] font-semibold text-[#6e83b4]">
|
||||
RECENT ACTIVITY
|
||||
<button className="ml-auto font-normal text-[#315dc0]" type="button">
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-2">
|
||||
{unit.activity.map((item) => (
|
||||
<div className="flex items-center gap-3" key={item.id}>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-[11px] bg-white text-[#315dc0]">
|
||||
{item.id === "updated" ? "Z" : "↗"}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[12px] font-semibold text-[#33405c]">
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-[10px] text-[#6e83b4]">{item.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
|
||||
interface MobileWorkUnitHeaderProps extends ComponentProps<"header"> {
|
||||
onBack: () => void;
|
||||
unit: WorkUnitSummary;
|
||||
}
|
||||
|
||||
const MobileWorkUnitHeader = ({
|
||||
className,
|
||||
onBack,
|
||||
unit,
|
||||
...props
|
||||
}: MobileWorkUnitHeaderProps) => (
|
||||
<header
|
||||
className={cn(
|
||||
"flex h-[78px] shrink-0 items-center bg-[#fbfcf9] px-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<button
|
||||
aria-label="Back to work units"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white text-[#24231f] active:scale-95"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="size-5" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1 text-center">
|
||||
<p className="text-[16px] font-semibold">Work unit</p>
|
||||
<p className="text-[11px] text-[#807b76]">
|
||||
{unit.code} · {unit.status}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-label="More work unit actions"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white active:scale-95"
|
||||
type="button"
|
||||
>
|
||||
<MoreHorizontal className="size-5" />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
interface MobileCharacterChatProps extends Omit<
|
||||
ComponentProps<"main">,
|
||||
"onChange" | "onSubmit"
|
||||
> {
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
statusMessage?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const MobileCharacterChat = ({
|
||||
className,
|
||||
onChange,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
value,
|
||||
...props
|
||||
}: MobileCharacterChatProps) => (
|
||||
<main
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col overflow-y-auto bg-white px-4 pt-5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<header className="flex items-center">
|
||||
<span className="flex size-10 items-center justify-center rounded-[14px] bg-[#c7ff00]">
|
||||
<Sparkles className="size-5" />
|
||||
</span>
|
||||
<div className="ml-3">
|
||||
<h1 className="text-[20px] font-semibold">Zopu</h1>
|
||||
<p className="flex items-center gap-2 text-[12px] text-[#64615d]">
|
||||
<span className="size-1.5 rounded-full bg-[#1acb72]" />
|
||||
Ready to help
|
||||
</p>
|
||||
</div>
|
||||
<MoreHorizontal className="ml-auto size-5" />
|
||||
</header>
|
||||
<div className="mt-10 w-fit rounded-full bg-[#f4f4f2] px-3 py-1.5 text-[10px] text-[#aaa7a3]">
|
||||
TODAY • 9:41 AM
|
||||
</div>
|
||||
<div className="mt-5 flex items-center gap-2 text-[14px] text-[#55524f]">
|
||||
<Sparkles className="size-4" />
|
||||
<strong>Zopu</strong>
|
||||
<span className="text-[#b5b2ae]">• a fresh start</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[18px] leading-6">
|
||||
Good morning. What are we making clearer today?
|
||||
</p>
|
||||
<div className="mt-4 flex gap-6 text-[12px] font-semibold text-[#55524f]">
|
||||
<button className="flex items-center gap-2" type="button">
|
||||
<FileText className="size-4" />
|
||||
Summarize
|
||||
</button>
|
||||
<button type="button">☷ Plan</button>
|
||||
<button type="button">◉ Explore</button>
|
||||
</div>
|
||||
<div className="mt-7 ml-auto max-w-[275px] rounded-[22px] bg-[#0c0c0b] px-4 py-3 text-[17px] leading-6 text-white">
|
||||
Turn my messy notes into a focused plan.
|
||||
</div>
|
||||
<p className="mt-1 ml-auto text-[10px] text-[#b5b2ae]">
|
||||
9:42 AM <span className="text-[#1acb72]">✓</span>
|
||||
</p>
|
||||
<div className="mt-5 flex items-center gap-2">
|
||||
<span className="flex size-7 items-center justify-center rounded-[9px] bg-[#c7ff00]">
|
||||
<Sparkles className="size-4" />
|
||||
</span>
|
||||
<strong className="text-[14px]">Zopu</strong>
|
||||
<span className="rounded-full bg-[#f1f1ef] px-2 py-1 text-[9px] text-[#aaa7a3]">
|
||||
THINKING PARTNER
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[17px] leading-6">
|
||||
Absolutely. I’ll turn the noise into a small set of next steps, then leave
|
||||
room for the unexpected.
|
||||
</p>
|
||||
<section className="mt-3 rounded-[22px] bg-[#f4f4f2] p-4">
|
||||
<div className="flex items-center">
|
||||
<span className="flex size-8 items-center justify-center rounded-[10px] bg-[#c7ff00]">
|
||||
↗
|
||||
</span>
|
||||
<div className="ml-3">
|
||||
<h2 className="text-[15px] font-semibold">Weekly reset</h2>
|
||||
<p className="text-[10px] text-[#b5b2ae]">
|
||||
3 focus areas · drafted just now
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</div>
|
||||
<ul className="mt-3 space-y-1.5 text-[13px]">
|
||||
<li>🟢 Finish the launch brief</li>
|
||||
<li>● Block two hours for deep work</li>
|
||||
<li className="text-[#77736f]">● Leave an hour for cleanup</li>
|
||||
</ul>
|
||||
</section>
|
||||
<form
|
||||
className="sticky bottom-0 mt-auto bg-white pt-5 pb-[max(12px,env(safe-area-inset-bottom))]"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="flex items-center rounded-[30px] bg-[#f4f4f2] p-2">
|
||||
<button
|
||||
aria-label="Attach file"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Paperclip className="size-5" />
|
||||
</button>
|
||||
<input
|
||||
className="min-w-0 flex-1 bg-transparent px-2 text-[14px] outline-none placeholder:text-[#b5b2ae]"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="Ask anything..."
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
aria-label="Record voice message"
|
||||
className="flex size-10 items-center justify-center rounded-full bg-white"
|
||||
type="button"
|
||||
>
|
||||
<Mic className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Send"
|
||||
className="ml-1 flex size-10 items-center justify-center rounded-full bg-black text-white disabled:bg-[#d7d7d2]"
|
||||
disabled={!value.trim()}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowUp className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-center text-[9px] text-[#b5b2ae]">
|
||||
{statusMessage ?? "Press Enter to send · Shift + Enter for a new line"}
|
||||
</p>
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
|
||||
interface MobileScreenFrameProps extends ComponentProps<"div"> {
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
const MobileScreenFrame = ({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
...props
|
||||
}: MobileScreenFrameProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-auto flex h-[100dvh] min-h-0 w-full max-w-[402px] flex-col overflow-hidden bg-[#fbfcf9] text-[#11110f] sm:border-x sm:border-white/10",
|
||||
className
|
||||
)}
|
||||
data-slot="mobile-screen-frame"
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export {
|
||||
MobileCharacterChat,
|
||||
MobileCommandComposer,
|
||||
MobileScreenFrame,
|
||||
MobileStackedFocus,
|
||||
MobileVerticalWorkStack,
|
||||
MobileWorkFeed,
|
||||
MobileWorkspaceHeader,
|
||||
MobileWorkUnitDetail,
|
||||
MobileWorkUnitHeader,
|
||||
WorkStatusBadge,
|
||||
};
|
||||
export type {
|
||||
MobileCharacterChatProps,
|
||||
MobileCommandComposerProps,
|
||||
MobileScreenFrameProps,
|
||||
MobileStackedFocusProps,
|
||||
MobileVerticalWorkStackProps,
|
||||
MobileWorkFeedProps,
|
||||
MobileWorkUnitDetailProps,
|
||||
MobileWorkUnitHeaderProps,
|
||||
WorkUnitDetail,
|
||||
WorkUnitSummary,
|
||||
WorkUnitTone,
|
||||
};
|
||||
6
packages/ui/src/components/mobile-workspace/index.ts
Normal file
6
packages/ui/src/components/mobile-workspace/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { MobileAssistantChatScreen } from "./mobile-assistant-chat-screen";
|
||||
export { MobileExpandedWorkScreen } from "./mobile-expanded-work-screen";
|
||||
export { MobileHomeScreen } from "./mobile-home-screen";
|
||||
export { MobileWorkListScreen } from "./mobile-work-list-screen";
|
||||
export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-screen";
|
||||
export type { MobileWorkspaceScreenProps } from "./types";
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileAssistantChatScreen = (
|
||||
props: MobileWorkspaceScreenProps
|
||||
) => <MobileWorkspaceScreenRenderer {...props} variant="character-pass" />;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileExpandedWorkScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="home-expanded-in-place" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileHomeScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="work-units-home" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileWorkListScreen = (props: MobileWorkspaceScreenProps) => (
|
||||
<MobileWorkspaceScreenRenderer {...props} variant="vertical-work-stack" />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MobileWorkspaceScreenRenderer } from "../mobile-workspace-screens";
|
||||
import type { MobileWorkspaceScreenProps } from "./types";
|
||||
|
||||
export const MobileWorkUnitDetailScreen = (
|
||||
props: MobileWorkspaceScreenProps
|
||||
) => <MobileWorkspaceScreenRenderer {...props} variant="expanded-work-unit" />;
|
||||
6
packages/ui/src/components/mobile-workspace/types.ts
Normal file
6
packages/ui/src/components/mobile-workspace/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { MobileWorkspaceRendererProps } from "../mobile-workspace-screens";
|
||||
|
||||
export type MobileWorkspaceScreenProps = Omit<
|
||||
MobileWorkspaceRendererProps,
|
||||
"variant"
|
||||
>;
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
|
||||
Reference in New Issue
Block a user