Files
temetro/frontend/components/ai-elements/plan.tsx
T
Khalid Abdi 97bf9ed8cc Build temetro app: dark theme, clinical sidebar, AI chat, settings page
- Linear-inspired dark theme (app/globals.css), forced dark in layout
- Clinical sidebar (components/sidebar-02) with temetro brand
- AI chat UI (components/chat): Cursor-style input, UI-only mock replies
- Settings page (components/settings): Polar-style tabs/sections, UI only
- Route-group app shell: app/(app)/{layout,page,settings}
- shadcn (Base UI) + ai-elements component libraries
- CLAUDE.md: architecture notes + per-folder commit policy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:25:19 +03:00

133 lines
3.4 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronsUpDownIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { createContext, useContext, useMemo } from "react";
import { Shimmer } from "./shimmer";
interface PlanContextValue {
isStreaming: boolean;
}
const PlanContext = createContext<PlanContextValue | null>(null);
const usePlan = () => {
const context = useContext(PlanContext);
if (!context) {
throw new Error("Plan components must be used within Plan");
}
return context;
};
export type PlanProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean;
};
export const Plan = ({
className,
isStreaming = false,
children,
...props
}: PlanProps) => {
const contextValue = useMemo(() => ({ isStreaming }), [isStreaming]);
return (
<PlanContext.Provider value={contextValue}>
<Collapsible data-slot="plan" {...props} render={<Card className={cn("shadow-none", className)} />}>{children}</Collapsible>
</PlanContext.Provider>
);
};
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
<CardHeader
className={cn("flex items-start justify-between", className)}
data-slot="plan-header"
{...props}
/>
);
export type PlanTitleProps = Omit<
ComponentProps<typeof CardTitle>,
"children"
> & {
children: string;
};
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
const { isStreaming } = usePlan();
return (
<CardTitle data-slot="plan-title" {...props}>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardTitle>
);
};
export type PlanDescriptionProps = Omit<
ComponentProps<typeof CardDescription>,
"children"
> & {
children: string;
};
export const PlanDescription = ({
className,
children,
...props
}: PlanDescriptionProps) => {
const { isStreaming } = usePlan();
return (
<CardDescription
className={cn("text-balance", className)}
data-slot="plan-description"
{...props}
>
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
</CardDescription>
);
};
export type PlanActionProps = ComponentProps<typeof CardAction>;
export const PlanAction = (props: PlanActionProps) => (
<CardAction data-slot="plan-action" {...props} />
);
export type PlanContentProps = ComponentProps<typeof CardContent>;
export const PlanContent = (props: PlanContentProps) => (
<CollapsibleContent render={<CardContent data-slot="plan-content" {...props} />}></CollapsibleContent>
);
export type PlanFooterProps = ComponentProps<"div">;
export const PlanFooter = (props: PlanFooterProps) => (
<CardFooter data-slot="plan-footer" {...props} />
);
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
<CollapsibleTrigger render={<Button className={cn("size-8", className)} data-slot="plan-trigger" size="icon" variant="ghost" {...props} />}><ChevronsUpDownIcon className="size-4" /><span className="sr-only">Toggle plan</span></CollapsibleTrigger>
);