build mobile workspace flow

This commit is contained in:
sai karthik
2026-07-23 20:05:48 +05:30
parent 732c458fd4
commit 2aa7150716
35 changed files with 2552 additions and 217 deletions

View File

@@ -0,0 +1,8 @@
export {
MobileAssistantChatScreen,
MobileExpandedWorkScreen,
MobileHomeScreen,
MobileWorkListScreen,
MobileWorkUnitDetailScreen,
} from "./mobile-workspace/index";
export type { MobileWorkspaceScreenProps } from "./mobile-workspace/index";

File diff suppressed because it is too large Load Diff

View 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&nbsp;&nbsp;&nbsp;&nbsp;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">&nbsp; Plan</button>
<button type="button">&nbsp; 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&nbsp; <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. Ill 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>🟢&nbsp; Finish the launch brief</li>
<li>&nbsp; Block two hours for deep work</li>
<li className="text-[#77736f]">&nbsp; 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,
};

View 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";

View File

@@ -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" />;

View File

@@ -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" />
);

View File

@@ -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" />
);

View File

@@ -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" />
);

View File

@@ -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" />;

View File

@@ -0,0 +1,6 @@
import type { MobileWorkspaceRendererProps } from "../mobile-workspace-screens";
export type MobileWorkspaceScreenProps = Omit<
MobileWorkspaceRendererProps,
"variant"
>;

View File

@@ -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);