tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | )
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | )
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | )
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | )
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | }
117 |
--------------------------------------------------------------------------------
/src/lib/canvas/updates.ts:
--------------------------------------------------------------------------------
1 | import { ChartData, ProjectData } from "@/lib/canvas/types";
2 |
3 | export function projectAddField4Item(data: ProjectData, text?: string): { next: ProjectData; createdId: string } {
4 | const existing = data.field4 ?? [];
5 | const nextCount = (data.field4_id ?? 0) + 1;
6 | const id = String(nextCount).padStart(3, "0");
7 | const next = [...existing, { id, text: text ?? "", done: false, proposed: false }];
8 | return { next: { ...data, field4: next, field4_id: nextCount }, createdId: id };
9 | }
10 |
11 | export function projectSetField4ItemText(data: ProjectData, checklistItemId: string, text: string): ProjectData {
12 | const next = (data.field4 ?? []).map((item) => (item.id === checklistItemId ? { ...item, text } : item));
13 | return { ...data, field4: next } as ProjectData;
14 | }
15 |
16 | export function projectSetField4ItemDone(data: ProjectData, checklistItemId: string, done: boolean): ProjectData {
17 | const next = (data.field4 ?? []).map((item) => (item.id === checklistItemId ? { ...item, done } : item));
18 | return { ...data, field4: next } as ProjectData;
19 | }
20 |
21 | export function projectRemoveField4Item(data: ProjectData, checklistItemId: string): ProjectData {
22 | const next = (data.field4 ?? []).filter((item) => item.id !== checklistItemId);
23 | return { ...data, field4: next } as ProjectData;
24 | }
25 |
26 | export function chartAddField1Metric(data: ChartData, label?: string, value?: number | ""): { next: ChartData; createdId: string } {
27 | const existing = data.field1 ?? [];
28 | const nextCount = (data.field1_id ?? 0) + 1;
29 | const id = String(nextCount).padStart(3, "0");
30 | const safe: number | "" = typeof value === "number" && Number.isFinite(value)
31 | ? Math.max(0, Math.min(100, value))
32 | : value === "" ? "" : 0;
33 | const next = [...existing, { id, label: label ?? "", value: safe }];
34 | return { next: { ...data, field1: next, field1_id: nextCount }, createdId: id };
35 | }
36 |
37 | export function chartSetField1Label(data: ChartData, index: number, label: string): ChartData {
38 | const next = [...(data.field1 ?? [])];
39 | if (index >= 0 && index < next.length) {
40 | next[index] = { ...next[index], label };
41 | return { ...data, field1: next } as ChartData;
42 | }
43 | return data;
44 | }
45 |
46 | export function chartSetField1Value(data: ChartData, index: number, value: number | ""): ChartData {
47 | const next = [...(data.field1 ?? [])];
48 | if (index >= 0 && index < next.length) {
49 | if (value === "") {
50 | next[index] = { ...next[index], value: "" };
51 | } else {
52 | const clamped = Math.max(0, Math.min(100, value));
53 | next[index] = { ...next[index], value: clamped };
54 | }
55 | return { ...data, field1: next } as ChartData;
56 | }
57 | return data;
58 | }
59 |
60 | export function chartRemoveField1Metric(data: ChartData, index: number): ChartData {
61 | const next = [...(data.field1 ?? [])];
62 | if (index >= 0 && index < next.length) {
63 | next.splice(index, 1);
64 | return { ...data, field1: next } as ChartData;
65 | }
66 | return data;
67 | }
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {
3 | ChevronLeftIcon,
4 | ChevronRightIcon,
5 | MoreHorizontalIcon,
6 | } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Button, buttonVariants } from "@/components/ui/button"
10 |
11 | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12 | return (
13 |
20 | )
21 | }
22 |
23 | function PaginationContent({
24 | className,
25 | ...props
26 | }: React.ComponentProps<"ul">) {
27 | return (
28 |
33 | )
34 | }
35 |
36 | function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37 | return
38 | }
39 |
40 | type PaginationLinkProps = {
41 | isActive?: boolean
42 | } & Pick, "size"> &
43 | React.ComponentProps<"a">
44 |
45 | function PaginationLink({
46 | className,
47 | isActive,
48 | size = "icon",
49 | ...props
50 | }: PaginationLinkProps) {
51 | return (
52 |
65 | )
66 | }
67 |
68 | function PaginationPrevious({
69 | className,
70 | ...props
71 | }: React.ComponentProps) {
72 | return (
73 |
79 |
80 | Previous
81 |
82 | )
83 | }
84 |
85 | function PaginationNext({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
96 | Next
97 |
98 |
99 | )
100 | }
101 |
102 | function PaginationEllipsis({
103 | className,
104 | ...props
105 | }: React.ComponentProps<"span">) {
106 | return (
107 |
113 |
114 | More pages
115 |
116 | )
117 | }
118 |
119 | export {
120 | Pagination,
121 | PaginationContent,
122 | PaginationLink,
123 | PaginationItem,
124 | PaginationPrevious,
125 | PaginationNext,
126 | PaginationEllipsis,
127 | }
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "langgraph-python-canvas-starter",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
7 | "dev:debug": "LOG_LEVEL=debug npm run dev",
8 | "dev:agent": "cd agent && npx @langchain/langgraph-cli dev --port 8123 --no-browser",
9 | "dev:ui": "next dev --turbopack",
10 | "build": "next build",
11 | "start": "next start",
12 | "lint": "next lint",
13 | "install:agent": "sh ./scripts/setup-agent.sh || scripts/setup-agent.bat",
14 | "postinstall": "npm run install:agent"
15 | },
16 | "dependencies": {
17 | "@ai-sdk/openai": "^1.3.24",
18 | "@copilotkit/react-core": "^1.10.4",
19 | "@copilotkit/react-textarea": "1.10.4",
20 | "@copilotkit/react-ui": "^1.10.4",
21 | "@copilotkit/runtime": "^1.10.4",
22 | "@radix-ui/react-accordion": "^1.2.12",
23 | "@radix-ui/react-alert-dialog": "1.1.15",
24 | "@radix-ui/react-aspect-ratio": "1.1.7",
25 | "@radix-ui/react-avatar": "^1.1.10",
26 | "@radix-ui/react-checkbox": "1.3.3",
27 | "@radix-ui/react-collapsible": "1.1.12",
28 | "@radix-ui/react-context-menu": "2.2.16",
29 | "@radix-ui/react-dialog": "1.1.15",
30 | "@radix-ui/react-dropdown-menu": "^2.1.16",
31 | "@radix-ui/react-hover-card": "1.1.15",
32 | "@radix-ui/react-label": "2.1.7",
33 | "@radix-ui/react-menubar": "1.1.16",
34 | "@radix-ui/react-navigation-menu": "1.2.14",
35 | "@radix-ui/react-popover": "1.1.15",
36 | "@radix-ui/react-progress": "^1.1.7",
37 | "@radix-ui/react-radio-group": "1.3.8",
38 | "@radix-ui/react-scroll-area": "1.2.10",
39 | "@radix-ui/react-select": "2.2.6",
40 | "@radix-ui/react-separator": "1.1.7",
41 | "@radix-ui/react-slider": "1.3.6",
42 | "@radix-ui/react-slot": "^1.2.3",
43 | "@radix-ui/react-switch": "1.2.6",
44 | "@radix-ui/react-tabs": "1.1.13",
45 | "@radix-ui/react-toast": "1.2.15",
46 | "@radix-ui/react-toggle": "1.1.10",
47 | "@radix-ui/react-toggle-group": "1.1.11",
48 | "@radix-ui/react-tooltip": "1.2.8",
49 | "@types/lodash": "^4.17.20",
50 | "class-variance-authority": "^0.7.1",
51 | "clsx": "^2.1.1",
52 | "cmdk": "1.1.1",
53 | "embla-carousel": "8.6.0",
54 | "embla-carousel-react": "8.6.0",
55 | "input-otp": "1.4.2",
56 | "lodash": "^4.17.21",
57 | "lucide-react": "^0.542.0",
58 | "motion": "^12.23.12",
59 | "next": "^15.3.3",
60 | "next-themes": "^0.4.6",
61 | "react": "^19.1.1",
62 | "react-day-picker": "^9.9.0",
63 | "react-dom": "^19.1.1",
64 | "react-hook-form": "^7.62.0",
65 | "react-resizable-panels": "^3.0.5",
66 | "react-shiki": "^0.7.3",
67 | "react-textarea-autosize": "^8.5.9",
68 | "recharts": "^3.2.0",
69 | "sonner": "^2.0.7",
70 | "tailwind-merge": "^3.3.1",
71 | "tw-animate-css": "^1.3.7",
72 | "vaul": "^1.1.2",
73 | "zod": "^3.25.76",
74 | "geist": "^1.4.2"
75 | },
76 | "devDependencies": {
77 | "@eslint/eslintrc": "^3.3.1",
78 | "@langchain/langgraph-cli": "0.0.40",
79 | "@tailwindcss/postcss": "^4.1.12",
80 | "@types/node": "^20.19.11",
81 | "@types/react": "^19.1.12",
82 | "@types/react-dom": "^19.1.9",
83 | "concurrently": "^9.2.1",
84 | "eslint": "^9.34.0",
85 | "eslint-config-next": "15.3.2",
86 | "geist": "^1.4.2",
87 | "tailwindcss": "^4.1.12",
88 | "typescript": "^5.9.2"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form"
15 |
16 | import { cn } from "@/lib/utils"
17 | import { Label } from "@/components/ui/label"
18 |
19 | const Form = FormProvider
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName
26 | }
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | )
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext)
47 | const itemContext = React.useContext(FormItemContext)
48 | const { getFieldState } = useFormContext()
49 | const formState = useFormState({ name: fieldContext.name })
50 | const fieldState = getFieldState(fieldContext.name, formState)
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ")
54 | }
55 |
56 | const { id } = itemContext
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | }
66 | }
67 |
68 | type FormItemContextValue = {
69 | id: string
70 | }
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | )
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
86 |
87 | )
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField()
95 |
96 | return (
97 |
104 | )
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | }
124 |
125 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 | const { formDescriptionId } = useFormField()
127 |
128 | return (
129 |
135 | )
136 | }
137 |
138 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 | const { error, formMessageId } = useFormField()
140 | const body = error ? String(error?.message ?? "") : props.children
141 |
142 | if (!body) {
143 | return null
144 | }
145 |
146 | return (
147 |
153 | {body}
154 |
155 | )
156 | }
157 |
158 | export {
159 | useFormField,
160 | Form,
161 | FormItem,
162 | FormLabel,
163 | FormControl,
164 | FormDescription,
165 | FormMessage,
166 | FormField,
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | function AlertDialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function AlertDialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | function AlertDialogPortal({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 | )
29 | }
30 |
31 | function AlertDialogOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function AlertDialogContent({
48 | className,
49 | ...props
50 | }: React.ComponentProps) {
51 | return (
52 |
53 |
54 |
62 |
63 | )
64 | }
65 |
66 | function AlertDialogHeader({
67 | className,
68 | ...props
69 | }: React.ComponentProps<"div">) {
70 | return (
71 |
76 | )
77 | }
78 |
79 | function AlertDialogFooter({
80 | className,
81 | ...props
82 | }: React.ComponentProps<"div">) {
83 | return (
84 |
92 | )
93 | }
94 |
95 | function AlertDialogTitle({
96 | className,
97 | ...props
98 | }: React.ComponentProps) {
99 | return (
100 |
105 | )
106 | }
107 |
108 | function AlertDialogDescription({
109 | className,
110 | ...props
111 | }: React.ComponentProps) {
112 | return (
113 |
118 | )
119 | }
120 |
121 | function AlertDialogAction({
122 | className,
123 | ...props
124 | }: React.ComponentProps) {
125 | return (
126 |
130 | )
131 | }
132 |
133 | function AlertDialogCancel({
134 | className,
135 | ...props
136 | }: React.ComponentProps) {
137 | return (
138 |
142 | )
143 | }
144 |
145 | export {
146 | AlertDialog,
147 | AlertDialogPortal,
148 | AlertDialogOverlay,
149 | AlertDialogTrigger,
150 | AlertDialogContent,
151 | AlertDialogHeader,
152 | AlertDialogFooter,
153 | AlertDialogTitle,
154 | AlertDialogDescription,
155 | AlertDialogAction,
156 | AlertDialogCancel,
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | )
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | showCloseButton = true,
53 | ...props
54 | }: React.ComponentProps & {
55 | showCloseButton?: boolean
56 | }) {
57 | return (
58 |
59 |
60 |
68 | {children}
69 | {showCloseButton && (
70 |
74 |
75 | Close
76 |
77 | )}
78 |
79 |
80 | )
81 | }
82 |
83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84 | return (
85 |
90 | )
91 | }
92 |
93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94 | return (
95 |
103 | )
104 | }
105 |
106 | function DialogTitle({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | function DialogDescription({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | )
130 | }
131 |
132 | export {
133 | Dialog,
134 | DialogClose,
135 | DialogContent,
136 | DialogDescription,
137 | DialogFooter,
138 | DialogHeader,
139 | DialogOverlay,
140 | DialogPortal,
141 | DialogTitle,
142 | DialogTrigger,
143 | }
144 |
--------------------------------------------------------------------------------
/.github/workflows/smoke.yml:
--------------------------------------------------------------------------------
1 | name: Smoke
2 |
3 | on:
4 | push:
5 | branches: main
6 | pull_request:
7 | branches: main
8 | schedule:
9 | - cron: '0 0 * * *' # Run daily at midnight UTC
10 |
11 | jobs:
12 | smoke:
13 | name: ${{ matrix.os }} / Node ${{ matrix.node }} / Python ${{ matrix.python }}
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | os: [ubuntu-latest, windows-latest, macos-latest]
19 | node: [20, 22]
20 | python: [3.11, 3.12]
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup Node.js
27 | uses: actions/setup-node@v4
28 | with:
29 | node-version: ${{ matrix.node }}
30 |
31 | - name: Setup Python
32 | uses: actions/setup-python@v4
33 | with:
34 | python-version: ${{ matrix.python }}
35 |
36 | - name: Install Node.js dependencies (root)
37 | run: npm install
38 |
39 | - name: Install Node.js dependencies (agent)
40 | run: |
41 | cd agent
42 | npm install
43 |
44 | - name: Install Python dependencies (agent)
45 | run: |
46 | cd agent
47 | pip install -r requirements.txt
48 |
49 | - name: Build frontend
50 | run: npm run build
51 |
52 | - name: Test frontend startup (Linux/macOS)
53 | if: runner.os != 'Windows'
54 | run: |
55 | # Start the Next.js frontend in background
56 | npm start &
57 | FRONTEND_PID=$!
58 |
59 | # Wait for frontend to start (max 30 seconds)
60 | timeout=30
61 | elapsed=0
62 | started=false
63 |
64 | while [ $elapsed -lt $timeout ] && [ "$started" = false ]; do
65 | if curl -s http://localhost:3000 > /dev/null 2>&1; then
66 | started=true
67 | echo "✅ Frontend started successfully"
68 | else
69 | sleep 1
70 | elapsed=$((elapsed + 1))
71 | fi
72 | done
73 |
74 | # Clean up background process
75 | kill $FRONTEND_PID 2>/dev/null || true
76 |
77 | if [ "$started" = false ]; then
78 | echo "❌ Frontend failed to start within 30 seconds"
79 | exit 1
80 | fi
81 | shell: bash
82 |
83 | - name: Test frontend startup (Windows)
84 | if: runner.os == 'Windows'
85 | run: |
86 | # Start the Next.js frontend in background
87 | npm start &
88 |
89 | # Wait for frontend to start (max 30 seconds)
90 | $timeout = 30
91 | $elapsed = 0
92 | $started = $false
93 |
94 | while ($elapsed -lt $timeout -and -not $started) {
95 | try {
96 | $response = Invoke-WebRequest -Uri "http://localhost:3000" -TimeoutSec 1 -ErrorAction SilentlyContinue
97 | if ($response.StatusCode -eq 200) {
98 | $started = $true
99 | Write-Host "✅ Frontend started successfully"
100 | }
101 | } catch {
102 | Start-Sleep -Seconds 1
103 | $elapsed++
104 | }
105 | }
106 |
107 | if (-not $started) {
108 | Write-Host "❌ Frontend failed to start within 30 seconds"
109 | exit 1
110 | }
111 | shell: pwsh
112 |
113 | - name: Run linting
114 | run: npm run lint
115 |
116 | notify-slack:
117 | name: Notify Slack on Failure
118 | runs-on: ubuntu-latest
119 | needs: smoke
120 | if: |
121 | failure() &&
122 | github.event_name == 'schedule'
123 | steps:
124 | - name: Notify Slack
125 | uses: slackapi/slack-github-action@v2.1.0
126 | with:
127 | webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
128 | webhook-type: incoming-webhook
129 | payload: |
130 | {
131 | "text": ":warning: *Smoke test failed for `with-langgraph-python` :warning:.*",
132 | "blocks": [
133 | {
134 | "type": "section",
135 | "text": {
136 | "type": "mrkdwn",
137 | "text": ":warning: *Smoke test failed for :warning:*\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run details>"
138 | }
139 | }
140 | ]
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left"
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 |
76 |
77 | Close
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
91 | )
92 | }
93 |
94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | function SheetTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | )
115 | }
116 |
117 | function SheetDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | )
128 | }
129 |
130 | export {
131 | Sheet,
132 | SheetTrigger,
133 | SheetClose,
134 | SheetContent,
135 | SheetHeader,
136 | SheetFooter,
137 | SheetTitle,
138 | SheetDescription,
139 | }
140 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Drawer({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function DrawerTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function DrawerPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return
24 | }
25 |
26 | function DrawerClose({
27 | ...props
28 | }: React.ComponentProps) {
29 | return
30 | }
31 |
32 | function DrawerOverlay({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | )
46 | }
47 |
48 | function DrawerContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
56 |
68 |
69 | {children}
70 |
71 |
72 | )
73 | }
74 |
75 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
85 | )
86 | }
87 |
88 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
89 | return (
90 |
95 | )
96 | }
97 |
98 | function DrawerTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | )
109 | }
110 |
111 | function DrawerDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | )
122 | }
123 |
124 | export {
125 | Drawer,
126 | DrawerPortal,
127 | DrawerOverlay,
128 | DrawerTrigger,
129 | DrawerClose,
130 | DrawerContent,
131 | DrawerHeader,
132 | DrawerFooter,
133 | DrawerTitle,
134 | DrawerDescription,
135 | }
136 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: actionTypes.REMOVE_TOAST,
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case actionTypes.ADD_TOAST:
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case actionTypes.UPDATE_TOAST:
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case actionTypes.DISMISS_TOAST: {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case actionTypes.REMOVE_TOAST:
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: actionTypes.UPDATE_TOAST,
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
154 |
155 | dispatch({
156 | type: actionTypes.ADD_TOAST,
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Command as CommandPrimitive } from "cmdk"
5 | import { SearchIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from "@/components/ui/dialog"
15 |
16 | function Command({
17 | className,
18 | ...props
19 | }: React.ComponentProps) {
20 | return (
21 |
29 | )
30 | }
31 |
32 | function CommandDialog({
33 | title = "Command Palette",
34 | description = "Search for a command to run...",
35 | children,
36 | className,
37 | showCloseButton = true,
38 | ...props
39 | }: React.ComponentProps & {
40 | title?: string
41 | description?: string
42 | className?: string
43 | showCloseButton?: boolean
44 | }) {
45 | return (
46 |
60 | )
61 | }
62 |
63 | function CommandInput({
64 | className,
65 | ...props
66 | }: React.ComponentProps) {
67 | return (
68 |
72 |
73 |
81 |
82 | )
83 | }
84 |
85 | function CommandList({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
98 | )
99 | }
100 |
101 | function CommandEmpty({
102 | ...props
103 | }: React.ComponentProps) {
104 | return (
105 |
110 | )
111 | }
112 |
113 | function CommandGroup({
114 | className,
115 | ...props
116 | }: React.ComponentProps) {
117 | return (
118 |
126 | )
127 | }
128 |
129 | function CommandSeparator({
130 | className,
131 | ...props
132 | }: React.ComponentProps) {
133 | return (
134 |
139 | )
140 | }
141 |
142 | function CommandItem({
143 | className,
144 | ...props
145 | }: React.ComponentProps) {
146 | return (
147 |
155 | )
156 | }
157 |
158 | function CommandShortcut({
159 | className,
160 | ...props
161 | }: React.ComponentProps<"span">) {
162 | return (
163 |
171 | )
172 | }
173 |
174 | export {
175 | Command,
176 | CommandDialog,
177 | CommandInput,
178 | CommandList,
179 | CommandEmpty,
180 | CommandGroup,
181 | CommandItem,
182 | CommandShortcut,
183 | CommandSeparator,
184 | }
185 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | :root {
7 | /* Updated to Apple-inspired B2B SaaS color palette */
8 | --background: #f8fafc; /* Light gray for overall background */
9 | --foreground: #374151; /* Dark gray for primary text */
10 | --card: #ffffff; /* Pure white card background */
11 | --card-foreground: #374151; /* Dark gray card text */
12 | --primary: #374151; /* Primary button background */
13 | --primary-foreground: #ffffff; /* Primary button text */
14 | --secondary: #6366f1; /* Blue accent color */
15 | --secondary-foreground: #ffffff; /* Secondary text */
16 | --muted: #f8fafc; /* Muted background */
17 | --muted-foreground: #4b5563; /* Muted text */
18 | --accent: #6366f1; /* Blue accent for highlights */
19 | --accent-foreground: #ffffff; /* Accent text */
20 | --destructive: #be123c; /* Error messages */
21 | --destructive-foreground: #ffffff;
22 | --border: #e5e7eb; /* Light gray borders */
23 | --input: #ffffff; /* Input background */
24 | --ring: rgba(99, 102, 241, 0.5); /* Blue focus ring */
25 | --chart-1: #6366f1;
26 | --chart-2: #4b5563;
27 | --chart-3: #be123c;
28 | --chart-4: #0891b2;
29 | --chart-5: #ec4899;
30 | --radius: 0.5rem; /* Subtle corner rounding */
31 | --sidebar: #ffffff; /* Sidebar background */
32 | --sidebar-foreground: #374151; /* Sidebar text */
33 | --sidebar-primary: #f8fafc; /* Sidebar primary background */
34 | --sidebar-primary-foreground: #374151; /* Sidebar primary text */
35 | --sidebar-accent: #6366f1; /* Sidebar accent */
36 | --sidebar-accent-foreground: #ffffff; /* Sidebar accent text */
37 | --sidebar-border: #e5e7eb; /* Sidebar border */
38 | --sidebar-ring: rgba(99, 102, 241, 0.5); /* Sidebar focus ring */
39 | --font-geist: "Geist", sans-serif;
40 | --font-manrope: "Manrope", sans-serif;
41 | }
42 |
43 | .dark {
44 | --background: #1f2937;
45 | --foreground: #f8fafc;
46 | --card: #374151;
47 | --card-foreground: #f8fafc;
48 | --primary: #6366f1;
49 | --primary-foreground: #ffffff;
50 | --secondary: #374151;
51 | --secondary-foreground: #f8fafc;
52 | --muted: #4b5563;
53 | --muted-foreground: #9ca3af;
54 | --accent: #6366f1;
55 | --accent-foreground: #ffffff;
56 | --destructive: #be123c;
57 | --destructive-foreground: #ffffff;
58 | --border: #4b5563;
59 | --input: #374151;
60 | --ring: rgba(99, 102, 241, 0.5);
61 | --sidebar: #374151;
62 | --sidebar-foreground: #f8fafc;
63 | --sidebar-primary: #4b5563;
64 | --sidebar-primary-foreground: #f8fafc;
65 | --sidebar-accent: #6366f1;
66 | --sidebar-accent-foreground: #ffffff;
67 | --sidebar-border: #4b5563;
68 | --sidebar-ring: rgba(99, 102, 241, 0.5);
69 | }
70 |
71 | @theme inline {
72 | /* Default to Manrope as app-wide sans font */
73 | --font-sans: var(--font-manrope);
74 | --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
75 | --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
76 | --color-background: var(--background);
77 | --color-foreground: var(--foreground);
78 | --color-card: var(--card);
79 | --color-card-foreground: var(--card-foreground);
80 | --color-primary: var(--primary);
81 | --color-primary-foreground: var(--primary-foreground);
82 | --color-secondary: var(--secondary);
83 | --color-secondary-foreground: var(--secondary-foreground);
84 | --color-muted: var(--muted);
85 | --color-muted-foreground: var(--muted-foreground);
86 | --color-accent: var(--accent);
87 | --color-accent-foreground: var(--accent-foreground);
88 | --color-destructive: var(--destructive);
89 | --color-destructive-foreground: var(--destructive-foreground);
90 | --color-border: var(--border);
91 | --color-input: var(--input);
92 | --color-ring: var(--ring);
93 | --color-chart-1: var(--chart-1);
94 | --color-chart-2: var(--chart-2);
95 | --color-chart-3: var(--chart-3);
96 | --color-chart-4: var(--chart-4);
97 | --color-chart-5: var(--chart-5);
98 | --radius-sm: calc(var(--radius) - 4px);
99 | --radius-md: calc(var(--radius) - 2px);
100 | --radius-lg: var(--radius);
101 | --radius-xl: calc(var(--radius) + 4px);
102 | --color-sidebar: var(--sidebar);
103 | --color-sidebar-foreground: var(--sidebar-foreground);
104 | --color-sidebar-primary: var(--sidebar-primary);
105 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
106 | --color-sidebar-accent: var(--sidebar-accent);
107 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
108 | --color-sidebar-border: var(--sidebar-border);
109 | --color-sidebar-ring: var(--sidebar-ring);
110 | }
111 |
112 | @layer base {
113 | * {
114 | @apply border-border outline-ring/50;
115 | }
116 | body {
117 | @apply bg-background text-foreground;
118 | }
119 | }
120 |
121 | body {
122 | background: var(--background);
123 | color: var(--foreground);
124 | font-family: var(--font-sans);
125 | }
126 |
127 | body,
128 | html {
129 | height: 100%;
130 | }
131 |
132 | /*
133 | The default border color has changed to `currentcolor` in Tailwind CSS v4,
134 | so we've added these compatibility styles to make sure everything still
135 | looks the same as it did with Tailwind CSS v3.
136 |
137 | If we ever want to remove these styles, we need to add an explicit border
138 | color utility to any element that depends on these defaults.
139 | */
140 | @layer base {
141 | *,
142 | ::after,
143 | ::before,
144 | ::backdrop,
145 | ::file-selector-button {
146 | border-color: var(--color-gray-200, currentColor);
147 | }
148 | }
149 |
150 | .copilotKitMessages {
151 | @apply pr-0!;
152 | div:empty {
153 | display: none;
154 | }
155 | .copilotKitMessage {
156 | &, & * {
157 | @apply font-sans text-sm! subpixel-antialiased leading-normal text-primary!;
158 | }
159 | &.copilotKitAssistantMessage {
160 | @apply rounded-bl-none! pl-3! mb-10! border-1 border-accent/50! bg-accent/10!;
161 | .copilotKitMessageControls {
162 | @apply mt-3! gap-x-2.5! opacity-100!;
163 | &, & * {
164 | @apply text-primary/50! transition-colors!;
165 | }
166 | & button:hover, & button:hover * {
167 | @apply text-accent!;
168 | transform: scale(1) !important;
169 | }
170 | }
171 | }
172 | &.copilotKitUserMessage {
173 | @apply rounded-br-none! pr-3! mb-4! border-1 border-primary/25! bg-primary/5!;
174 | }
175 | }
176 | }
--------------------------------------------------------------------------------
/src/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { ArrowLeft, ArrowRight } from "lucide-react"
8 |
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 |
12 | type CarouselApi = UseEmblaCarouselType[1]
13 | type UseCarouselParameters = Parameters
14 | type CarouselOptions = UseCarouselParameters[0]
15 | type CarouselPlugin = UseCarouselParameters[1]
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions
19 | plugins?: CarouselPlugin
20 | orientation?: "horizontal" | "vertical"
21 | setApi?: (api: CarouselApi) => void
22 | }
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0]
26 | api: ReturnType[1]
27 | scrollPrev: () => void
28 | scrollNext: () => void
29 | canScrollPrev: boolean
30 | canScrollNext: boolean
31 | } & CarouselProps
32 |
33 | const CarouselContext = React.createContext(null)
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext)
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ")
40 | }
41 |
42 | return context
43 | }
44 |
45 | function Carousel({
46 | orientation = "horizontal",
47 | opts,
48 | setApi,
49 | plugins,
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps<"div"> & CarouselProps) {
54 | const [carouselRef, api] = useEmblaCarousel(
55 | {
56 | ...opts,
57 | axis: orientation === "horizontal" ? "x" : "y",
58 | },
59 | plugins
60 | )
61 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
62 | const [canScrollNext, setCanScrollNext] = React.useState(false)
63 |
64 | const onSelect = React.useCallback((api: CarouselApi) => {
65 | if (!api) return
66 | setCanScrollPrev(api.canScrollPrev())
67 | setCanScrollNext(api.canScrollNext())
68 | }, [])
69 |
70 | const scrollPrev = React.useCallback(() => {
71 | api?.scrollPrev()
72 | }, [api])
73 |
74 | const scrollNext = React.useCallback(() => {
75 | api?.scrollNext()
76 | }, [api])
77 |
78 | const handleKeyDown = React.useCallback(
79 | (event: React.KeyboardEvent) => {
80 | if (event.key === "ArrowLeft") {
81 | event.preventDefault()
82 | scrollPrev()
83 | } else if (event.key === "ArrowRight") {
84 | event.preventDefault()
85 | scrollNext()
86 | }
87 | },
88 | [scrollPrev, scrollNext]
89 | )
90 |
91 | React.useEffect(() => {
92 | if (!api || !setApi) return
93 | setApi(api)
94 | }, [api, setApi])
95 |
96 | React.useEffect(() => {
97 | if (!api) return
98 | onSelect(api)
99 | api.on("reInit", onSelect)
100 | api.on("select", onSelect)
101 |
102 | return () => {
103 | api?.off("select", onSelect)
104 | }
105 | }, [api, onSelect])
106 |
107 | return (
108 |
121 |
129 | {children}
130 |
131 |
132 | )
133 | }
134 |
135 | function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136 | const { carouselRef, orientation } = useCarousel()
137 |
138 | return (
139 |
153 | )
154 | }
155 |
156 | function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157 | const { orientation } = useCarousel()
158 |
159 | return (
160 |
171 | )
172 | }
173 |
174 | function CarouselPrevious({
175 | className,
176 | variant = "outline",
177 | size = "icon",
178 | ...props
179 | }: React.ComponentProps) {
180 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
181 |
182 | return (
183 |
201 | )
202 | }
203 |
204 | function CarouselNext({
205 | className,
206 | variant = "outline",
207 | size = "icon",
208 | ...props
209 | }: React.ComponentProps) {
210 | const { orientation, scrollNext, canScrollNext } = useCarousel()
211 |
212 | return (
213 |
231 | )
232 | }
233 |
234 | export {
235 | type CarouselApi,
236 | Carousel,
237 | CarouselContent,
238 | CarouselItem,
239 | CarouselPrevious,
240 | CarouselNext,
241 | }
242 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default"
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | )
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | )
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | )
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | )
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | )
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | }
186 |
--------------------------------------------------------------------------------
/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDownIcon } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function NavigationMenu({
9 | className,
10 | children,
11 | viewport = true,
12 | ...props
13 | }: React.ComponentProps & {
14 | viewport?: boolean
15 | }) {
16 | return (
17 |
26 | {children}
27 | {viewport && }
28 |
29 | )
30 | }
31 |
32 | function NavigationMenuList({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | )
46 | }
47 |
48 | function NavigationMenuItem({
49 | className,
50 | ...props
51 | }: React.ComponentProps) {
52 | return (
53 |
58 | )
59 | }
60 |
61 | const navigationMenuTriggerStyle = cva(
62 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
63 | )
64 |
65 | function NavigationMenuTrigger({
66 | className,
67 | children,
68 | ...props
69 | }: React.ComponentProps) {
70 | return (
71 |
76 | {children}{" "}
77 |
81 |
82 | )
83 | }
84 |
85 | function NavigationMenuContent({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
99 | )
100 | }
101 |
102 | function NavigationMenuViewport({
103 | className,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
112 |
120 |
121 | )
122 | }
123 |
124 | function NavigationMenuLink({
125 | className,
126 | ...props
127 | }: React.ComponentProps) {
128 | return (
129 |
137 | )
138 | }
139 |
140 | function NavigationMenuIndicator({
141 | className,
142 | ...props
143 | }: React.ComponentProps) {
144 | return (
145 |
153 |
154 |
155 | )
156 | }
157 |
158 | export {
159 | NavigationMenu,
160 | NavigationMenuList,
161 | NavigationMenuItem,
162 | NavigationMenuContent,
163 | NavigationMenuTrigger,
164 | NavigationMenuLink,
165 | NavigationMenuIndicator,
166 | NavigationMenuViewport,
167 | navigationMenuTriggerStyle,
168 | }
169 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | ChevronDownIcon,
6 | ChevronLeftIcon,
7 | ChevronRightIcon,
8 | } from "lucide-react"
9 | import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10 |
11 | import { cn } from "@/lib/utils"
12 | import { Button, buttonVariants } from "@/components/ui/button"
13 |
14 | function Calendar({
15 | className,
16 | classNames,
17 | showOutsideDays = true,
18 | captionLayout = "label",
19 | buttonVariant = "ghost",
20 | formatters,
21 | components,
22 | ...props
23 | }: React.ComponentProps & {
24 | buttonVariant?: React.ComponentProps["variant"]
25 | }) {
26 | const defaultClassNames = getDefaultClassNames()
27 |
28 | return (
29 | svg]:rotate-180`,
34 | String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35 | className
36 | )}
37 | captionLayout={captionLayout}
38 | formatters={{
39 | formatMonthDropdown: (date) =>
40 | date.toLocaleString("default", { month: "short" }),
41 | ...formatters,
42 | }}
43 | classNames={{
44 | root: cn("w-fit", defaultClassNames.root),
45 | months: cn(
46 | "flex gap-4 flex-col md:flex-row relative",
47 | defaultClassNames.months
48 | ),
49 | month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
50 | nav: cn(
51 | "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52 | defaultClassNames.nav
53 | ),
54 | button_previous: cn(
55 | buttonVariants({ variant: buttonVariant }),
56 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
57 | defaultClassNames.button_previous
58 | ),
59 | button_next: cn(
60 | buttonVariants({ variant: buttonVariant }),
61 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
62 | defaultClassNames.button_next
63 | ),
64 | month_caption: cn(
65 | "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66 | defaultClassNames.month_caption
67 | ),
68 | dropdowns: cn(
69 | "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70 | defaultClassNames.dropdowns
71 | ),
72 | dropdown_root: cn(
73 | "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
74 | defaultClassNames.dropdown_root
75 | ),
76 | dropdown: cn(
77 | "absolute bg-popover inset-0 opacity-0",
78 | defaultClassNames.dropdown
79 | ),
80 | caption_label: cn(
81 | "select-none font-medium",
82 | captionLayout === "label"
83 | ? "text-sm"
84 | : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85 | defaultClassNames.caption_label
86 | ),
87 | table: "w-full border-collapse",
88 | weekdays: cn("flex", defaultClassNames.weekdays),
89 | weekday: cn(
90 | "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
91 | defaultClassNames.weekday
92 | ),
93 | week: cn("flex w-full mt-2", defaultClassNames.week),
94 | week_number_header: cn(
95 | "select-none w-(--cell-size)",
96 | defaultClassNames.week_number_header
97 | ),
98 | week_number: cn(
99 | "text-[0.8rem] select-none text-muted-foreground",
100 | defaultClassNames.week_number
101 | ),
102 | day: cn(
103 | "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
104 | defaultClassNames.day
105 | ),
106 | range_start: cn(
107 | "rounded-l-md bg-accent",
108 | defaultClassNames.range_start
109 | ),
110 | range_middle: cn("rounded-none", defaultClassNames.range_middle),
111 | range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
112 | today: cn(
113 | "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114 | defaultClassNames.today
115 | ),
116 | outside: cn(
117 | "text-muted-foreground aria-selected:text-muted-foreground",
118 | defaultClassNames.outside
119 | ),
120 | disabled: cn(
121 | "text-muted-foreground opacity-50",
122 | defaultClassNames.disabled
123 | ),
124 | hidden: cn("invisible", defaultClassNames.hidden),
125 | ...classNames,
126 | }}
127 | components={{
128 | Root: ({ className, rootRef, ...props }) => {
129 | return (
130 |
136 | )
137 | },
138 | Chevron: ({ className, orientation, ...props }) => {
139 | if (orientation === "left") {
140 | return (
141 |
142 | )
143 | }
144 |
145 | if (orientation === "right") {
146 | return (
147 |
151 | )
152 | }
153 |
154 | return (
155 |
156 | )
157 | },
158 | DayButton: CalendarDayButton,
159 | WeekNumber: ({ children, ...props }) => {
160 | return (
161 |
162 |
163 | {children}
164 |
165 | |
166 | )
167 | },
168 | ...components,
169 | }}
170 | {...props}
171 | />
172 | )
173 | }
174 |
175 | function CalendarDayButton({
176 | className,
177 | day,
178 | modifiers,
179 | ...props
180 | }: React.ComponentProps) {
181 | const defaultClassNames = getDefaultClassNames()
182 |
183 | const ref = React.useRef(null)
184 | React.useEffect(() => {
185 | if (modifiers.focused) ref.current?.focus()
186 | }, [modifiers.focused])
187 |
188 | return (
189 | |