This commit is contained in:
MarkLo 2025-12-02 23:03:41 +08:00
parent 65c0033cca
commit 34dc5b0f87
19 changed files with 482 additions and 191 deletions

View File

@ -44,7 +44,7 @@ export default function AnalysisPage() {
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 dark:from-gray-900 dark:via-purple-900/20 dark:to-blue-900/20">
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 via-pink-50/20 to-purple-50/30 dark:from-gray-950 dark:via-purple-950/40 dark:to-gray-950">
<div className="container mx-auto px-4 py-12">
<div className="max-w-6xl mx-auto space-y-8">
{/* 標題區域 - 置中對齊 */}

View File

@ -129,7 +129,7 @@ export default function AnalysisResultsPage() {
const currentReport = getNestedValue(analysisResult.reports, currentAnalyst?.reportKey || "");
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 dark:from-gray-900 dark:via-purple-900/20 dark:to-blue-900/20">
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 via-pink-50/20 to-purple-50/30 dark:from-gray-950 dark:via-purple-950/40 dark:to-gray-950">
<div className="container mx-auto px-4 py-12">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
@ -180,7 +180,7 @@ export default function AnalysisResultsPage() {
)}
{/* 分析師報告 */}
<Card className="animate-scale-up hover-lift gradient-card gradient-shine">
<Card className="animate-scale-up hover-lift">
<CardHeader>
<CardTitle>{analyst.label} </CardTitle>
<CardDescription>

View File

@ -257,13 +257,13 @@
GRADIENT UTILITIES
======================================== */
/* Gradient Backgrounds */
/* Gradient Backgrounds - Purple/Pink theme */
.gradient-bg-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #9333EA 0%, #EC4899 100%);
}
.dark .gradient-bg-primary {
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
background: linear-gradient(135deg, #7C3AED 0%, #DB2777 100%);
}
.gradient-bg-secondary {
@ -410,9 +410,9 @@
}
}
/* Gradient Text */
/* Gradient Text - Purple/Pink theme */
.gradient-text-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #9333EA 0%, #EC4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@ -535,181 +535,67 @@
}
/* ========================================
DATE PICKER CUSTOM STYLING
GLASSMORPHISM UTILITIES
======================================== */
/* Modern Date Input Styling */
input[type="date"] {
position: relative;
padding: 0.75rem 1rem;
font-size: 1rem;
font-weight: 500;
line-height: 1.5;
color: var(--foreground);
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.05) 0%,
rgba(118, 75, 162, 0.05) 100%
);
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 0.75rem;
transition: all 0.3s ease;
cursor: pointer;
/* Base glassmorphism style - Subtle white with hint of pink */
.glass {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(147, 51, 234, 0.08);
box-shadow: 0 2px 12px 0 rgba(147, 51, 234, 0.04);
}
input[type="date"]:hover {
.dark .glass {
background: rgba(30, 20, 40, 0.95);
border: 1px solid rgba(147, 51, 234, 0.15);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4);
}
/* Enhanced glass with gradient - Very subtle pink/purple tint */
.glass-gradient {
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.1) 0%,
rgba(118, 75, 162, 0.1) 100%
rgba(255, 255, 255, 0.98) 0%,
rgba(252, 248, 255, 0.98) 100%
);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(147, 51, 234, 0.1);
box-shadow: 0 2px 16px 0 rgba(147, 51, 234, 0.06),
inset 0 1px 0 0 rgba(255, 255, 255, 0.5);
}
.dark .glass-gradient {
background: linear-gradient(
135deg,
rgba(30, 20, 40, 0.95) 0%,
rgba(40, 25, 50, 0.95) 100%
);
border: 1px solid rgba(147, 51, 234, 0.2);
box-shadow: 0 2px 16px 0 rgba(147, 51, 234, 0.15),
inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
}
/* Glass hover effect - Subtle */
.glass-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-hover:hover {
background: rgba(255, 255, 255, 1);
border-color: rgba(147, 51, 234, 0.15);
box-shadow: 0 4px 20px 0 rgba(147, 51, 234, 0.1),
inset 0 1px 0 0 rgba(255, 255, 255, 0.8);
transform: translateY(-2px);
}
input[type="date"]:focus {
outline: none;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.15) 0%,
rgba(118, 75, 162, 0.15) 100%
);
border-color: rgba(102, 126, 234, 0.6);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1),
0 8px 16px rgba(102, 126, 234, 0.25);
transform: translateY(-2px);
.dark .glass-hover:hover {
background: rgba(30, 20, 40, 1);
border-color: rgba(147, 51, 234, 0.3);
box-shadow: 0 4px 20px 0 rgba(147, 51, 234, 0.2),
inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
}
/* Dark mode date input */
.dark input[type="date"] {
background: linear-gradient(
135deg,
rgba(76, 81, 191, 0.15) 0%,
rgba(85, 60, 154, 0.15) 100%
);
border-color: rgba(76, 81, 191, 0.4);
}
.dark input[type="date"]:hover {
background: linear-gradient(
135deg,
rgba(76, 81, 191, 0.2) 0%,
rgba(85, 60, 154, 0.2) 100%
);
border-color: rgba(76, 81, 191, 0.6);
box-shadow: 0 4px 12px rgba(76, 81, 191, 0.3);
}
.dark input[type="date"]:focus {
background: linear-gradient(
135deg,
rgba(76, 81, 191, 0.25) 0%,
rgba(85, 60, 154, 0.25) 100%
);
border-color: rgba(76, 81, 191, 0.7);
box-shadow: 0 0 0 3px rgba(76, 81, 191, 0.15),
0 8px 16px rgba(76, 81, 191, 0.35);
}
/* Calendar icon styling */
input[type="date"]::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 0.8;
padding: 0.25rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
filter: brightness(0.9);
}
input[type="date"]::-webkit-calendar-picker-indicator:hover {
opacity: 1;
background: rgba(102, 126, 234, 0.15);
transform: scale(1.1);
filter: brightness(1.1);
}
.dark input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(0.9);
}
.dark input[type="date"]::-webkit-calendar-picker-indicator:hover {
background: rgba(76, 81, 191, 0.25);
filter: invert(1) brightness(1.1);
}
/* Date input inner elements (webkit) */
input[type="date"]::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
input[type="date"]::-webkit-datetime-edit-text {
padding: 0 0.25rem;
opacity: 0.7;
}
input[type="date"]::-webkit-datetime-edit-month-field,
input[type="date"]::-webkit-datetime-edit-day-field,
input[type="date"]::-webkit-datetime-edit-year-field {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
input[type="date"]::-webkit-datetime-edit-month-field:hover,
input[type="date"]::-webkit-datetime-edit-day-field:hover,
input[type="date"]::-webkit-datetime-edit-year-field:hover {
background: rgba(102, 126, 234, 0.2);
color: var(--foreground);
}
input[type="date"]::-webkit-datetime-edit-month-field:focus,
input[type="date"]::-webkit-datetime-edit-day-field:focus,
input[type="date"]::-webkit-datetime-edit-year-field:focus {
background: rgba(102, 126, 234, 0.25);
color: var(--foreground);
outline: none;
}
.dark input[type="date"]::-webkit-datetime-edit-month-field:hover,
.dark input[type="date"]::-webkit-datetime-edit-day-field:hover,
.dark input[type="date"]::-webkit-datetime-edit-year-field:hover {
background: rgba(76, 81, 191, 0.3);
}
.dark input[type="date"]::-webkit-datetime-edit-month-field:focus,
.dark input[type="date"]::-webkit-datetime-edit-day-field:focus,
.dark input[type="date"]::-webkit-datetime-edit-year-field:focus {
background: rgba(76, 81, 191, 0.35);
}
/* Placeholder styling when empty */
input[type="date"]:invalid::-webkit-datetime-edit {
color: var(--muted-foreground);
}
/* Animation for date picker open */
@keyframes datePickerSlide {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Additional polish */
input[type="date"]::before {
content: attr(placeholder);
width: 100%;
color: var(--muted-foreground);
}
input[type="date"]:focus::before,
input[type="date"]:valid::before {
display: none;
}

View File

@ -11,7 +11,7 @@ import {
export default function HomePage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 dark:from-gray-900 dark:via-purple-900/20 dark:to-blue-900/20">
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 via-pink-50/20 to-purple-50/30 dark:from-gray-950 dark:via-purple-950/40 dark:to-gray-950">
<div className="container mx-auto px-4 py-12">
{/* Hero Section */}
<div className="text-center mb-16 animate-fade-in relative py-8">
@ -248,7 +248,7 @@ export default function HomePage() {
<p className="text-center text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
TradingAgentsX LLM
</p>
<Card className="shadow-lg gradient-card gradient-shine hover-lift">
<Card className="shadow-lg hover-lift">
<CardContent className="pt-6">
<div className="space-y-4">
<WorkflowStep
@ -356,7 +356,7 @@ function FeatureCard({
icon: string;
}) {
return (
<Card className="hover-lift animate-slide-up gradient-shine gradient-card">
<Card className="hover-lift animate-slide-up">
<CardHeader>
<div className="text-4xl mb-2">{icon}</div>
<CardTitle className="text-lg">{title}</CardTitle>
@ -380,7 +380,7 @@ function AgentCard({
responsibilities: string[];
}) {
return (
<Card className="hover-lift animate-scale-up gradient-card">
<Card className="hover-lift animate-scale-up">
<CardHeader>
<CardTitle className="text-base">{name}</CardTitle>
<CardDescription className="text-xs">{role}</CardDescription>
@ -421,7 +421,7 @@ function LLMProviderCard({
const logoSrc = logoMap[name];
return (
<Card className="hover-lift animate-slide-up animate-delay-100 gradient-card gradient-shine">
<Card className="hover-lift animate-slide-up animate-delay-100">
<CardHeader>
<div className="flex items-center gap-3">
{logoSrc ? (
@ -462,7 +462,7 @@ function TechnicalCard({
features: string[];
}) {
return (
<Card className="hover-lift animate-slide-up animate-delay-300 gradient-card">
<Card className="hover-lift animate-slide-up animate-delay-300">
<CardHeader>
<CardTitle className="text-lg">{title}</CardTitle>
</CardHeader>

View File

@ -10,6 +10,7 @@ import * as z from "zod";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker";
import {
Form,
FormControl,
@ -122,7 +123,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
}
return (
<Card className="shadow-lg gradient-card gradient-shine hover-lift animate-scale-up">
<Card className="shadow-lg hover-lift animate-scale-up">
<CardContent className="pt-6">
<Form {...form}>
<form
@ -225,7 +226,14 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="date" {...field} />
<DatePicker
date={field.value ? new Date(field.value) : undefined}
onDateChange={(date) => {
field.onChange(date ? format(date, "yyyy-MM-dd") : "")
}}
placeholder="選擇分析日期"
className="w-full"
/>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />

View File

@ -35,7 +35,7 @@ export function AnalystReport({ reports }: AnalystReportProps) {
}
return (
<Card className="shadow-lg gradient-card gradient-shine hover-lift animate-scale-up">
<Card className="shadow-lg hover-lift animate-scale-up">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>

View File

@ -134,7 +134,7 @@ export function DownloadReports({
const isAllSelected = selectedAnalysts.length === availableAnalysts.length && availableAnalysts.length > 0;
return (
<Card className="gradient-card gradient-shine hover-lift animate-scale-up">
<Card className="hover-lift animate-scale-up">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileDown className="h-5 w-5" />

View File

@ -44,7 +44,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
const priceRange = maxPrice - minPrice;
return (
<Card className="w-full gradient-card gradient-shine hover-lift animate-scale-up">
<Card className="w-full hover-lift animate-scale-up">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">{ticker} </CardTitle>

View File

@ -41,7 +41,7 @@ export function TradingDecision({ result }: TradingDecisionProps) {
};
return (
<Card className="shadow-lg border-2 border-blue-500 gradient-card gradient-shine hover-lift animate-scale-up">
<Card className="shadow-lg border-2 border-blue-500 hover-lift animate-scale-up">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle></CardTitle>

View File

@ -13,7 +13,7 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"glass-gradient border-2 shadow-md hover:shadow-lg hover:scale-105 hover:border-primary/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:

View File

@ -0,0 +1,216 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"glass-gradient flex flex-col gap-6 rounded-xl py-6 shadow-lg transition-all duration-300 hover:shadow-2xl",
className
)}
{...props}

View File

@ -14,7 +14,10 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer glass-gradient data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 rounded-md border-2 shadow-md transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50",
"hover:shadow-lg hover:scale-110",
"focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20",
"aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/20",
className
)}
{...props}

View File

@ -0,0 +1,59 @@
"use client"
import * as React from "react"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface DatePickerProps {
date: Date | undefined
onDateChange: (date: Date | undefined) => void
placeholder?: string
className?: string
}
export function DatePicker({
date,
onDateChange,
placeholder = "選擇日期",
className,
}: DatePickerProps) {
const [open, setOpen] = React.useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"justify-start text-left font-normal",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "yyyy-MM-dd") : <span>{placeholder}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={(selectedDate) => {
onDateChange(selectedDate)
setOpen(false)
}}
initialFocus
/>
</PopoverContent>
</Popover>
)
}

View File

@ -8,9 +8,10 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"glass-gradient file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground h-9 w-full min-w-0 rounded-lg border-2 px-3 py-1 text-base shadow-md transition-all outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"hover:border-primary/50 hover:shadow-lg hover:scale-[1.01]",
"focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 focus-visible:shadow-xl focus-visible:scale-[1.01]",
"aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/20",
className
)}
{...props}

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -37,7 +37,10 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"glass-gradient data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex w-fit items-center justify-between gap-2 rounded-lg border-2 px-3 py-2 text-sm whitespace-nowrap shadow-md transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"hover:border-primary/50 hover:shadow-lg hover:scale-105",
"focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 focus-visible:shadow-xl",
"aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/20",
className
)}
{...props}
@ -62,7 +65,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"glass-gradient text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border-2 shadow-xl",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View File

@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
@ -25,6 +26,7 @@
"next": "16.0.3",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-day-picker": "^9.11.3",
"react-dom": "19.2.0",
"react-hook-form": "^7.66.1",
"react-markdown": "^10.1.0",

View File

@ -23,6 +23,9 @@ importers:
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-popover':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-select':
specifier: ^2.2.6
version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -56,6 +59,9 @@ importers:
react:
specifier: 19.2.0
version: 19.2.0
react-day-picker:
specifier: ^9.11.3
version: 9.11.3(react@19.2.0)
react-dom:
specifier: 19.2.0
version: 19.2.0(react@19.2.0)
@ -185,6 +191,9 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@ -662,6 +671,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-popover@1.1.15':
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-popper@1.2.8':
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
peerDependencies:
@ -1461,6 +1483,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@ -2531,6 +2556,12 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-day-picker@9.11.3:
resolution: {integrity: sha512-7lD12UvGbkyXqgzbYIGQTbl+x29B9bAf+k0pP5Dcs1evfpKk6zv4EdH/edNc8NxcmCiTNXr2HIYPrSZ3XvmVBg==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8.0'
react-dom@19.2.0:
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
peerDependencies:
@ -3100,6 +3131,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@date-fns/tz@1.4.1': {}
'@emnapi/core@1.7.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@ -3530,6 +3563,29 @@ snapshots:
'@types/react': 19.2.6
'@types/react-dom': 19.2.3(@types/react@19.2.6)
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.6)(react@19.2.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.6)(react@19.2.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.6)(react@19.2.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.6)(react@19.2.0)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.6)(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.6)(react@19.2.0)
aria-hidden: 1.2.6
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-remove-scroll: 2.7.1(@types/react@19.2.6)(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.6
'@types/react-dom': 19.2.3(@types/react@19.2.6)
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -4314,6 +4370,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {}
debug@3.2.7:
@ -5749,6 +5807,13 @@ snapshots:
queue-microtask@1.2.3: {}
react-day-picker@9.11.3(react@19.2.0):
dependencies:
'@date-fns/tz': 1.4.1
date-fns: 4.1.0
date-fns-jalali: 4.1.0-0
react: 19.2.0
react-dom@19.2.0(react@19.2.0):
dependencies:
react: 19.2.0