feat: redesign

Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
2026-04-21 20:23:15 -04:00
parent 420a971ff7
commit 915e0b7cf8
21 changed files with 1401 additions and 537 deletions

View File

@@ -281,183 +281,126 @@ export const AIToolbar = ({
</div>
</div>
) : isAuthenticated ? (
<div className="space-y-4">
<div className="rounded-2xl border border-border/70 bg-background/90 shadow-sm focus-within:ring-2 focus-within:ring-primary/30">
<Textarea
id="ai-event-prompt"
className="wrap-anywhere field-sizing-content min-h-40 w-full resize-none rounded-none border-0 bg-transparent px-4 py-3 text-sm shadow-none placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="Type or paste event details…"
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
onKeyDown={(e) => {
// ⌘↵ — generate
if (
e.key === "Enter" &&
(e.metaKey || e.ctrlKey) &&
(aiPrompt.trim() || hasImages)
) {
e.preventDefault();
onAiCreate();
}
// ⌘⇧A — attach image
if (
e.key === "A" &&
e.shiftKey &&
(e.metaKey || e.ctrlKey) &&
!aiLoading
) {
e.preventDefault();
imageTriggerRef.current?.open();
}
// Esc — clear prompt (only when not composing a native action)
if (e.key === "Escape" && aiPrompt) {
e.preventDefault();
setAiPrompt("");
}
}}
onPaste={(e) => {
const images = extractAllImagesFromClipboard(
e.clipboardData ?? null,
);
if (images.length > 0) {
e.preventDefault();
onImagesSelect(images);
}
}}
/>
<div className="sticky bottom-0 z-10 border-t border-border/60 bg-background/95 px-3 py-2 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
Try:
</span>
{examplePrompts.map((prompt) => (
<Button
key={prompt}
type="button"
variant="secondary"
size="sm"
className="h-7 max-w-full rounded-full px-2.5 text-[11px]"
onClick={() => onAiTemplateSelect(prompt)}
disabled={aiLoading || !canUseAi}
>
<span className="truncate">{prompt}</span>
</Button>
))}
</div>
</div>
</div>
<AnimatePresence>
{hasImages && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<div className="flex gap-2 overflow-x-auto py-1 ml-3">
{imagePreviews.map((preview, index) => (
<motion.div
key={preview}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.85 }}
transition={{ duration: 0.12 }}
className="relative inline-block shrink-0"
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="space-y-3">
<div className="rounded-[10px] bg-card shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_2px_rgba(0,0,0,0.04),0_0_0_1px_#fafafa] focus-within:ring-[3px] focus-within:ring-ring/20">
<Textarea
id="ai-event-prompt"
className="wrap-anywhere field-sizing-content min-h-48 w-full resize-none rounded-none border-0 bg-transparent px-4 py-3 text-sm shadow-none placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="Type or paste event details..."
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
(e.metaKey || e.ctrlKey) &&
(aiPrompt.trim() || hasImages)
) {
e.preventDefault();
onAiCreate();
}
if (
e.key === "A" &&
e.shiftKey &&
(e.metaKey || e.ctrlKey) &&
!aiLoading
) {
e.preventDefault();
imageTriggerRef.current?.open();
}
if (e.key === "Escape" && aiPrompt) {
e.preventDefault();
setAiPrompt("");
}
}}
onPaste={(e) => {
const images = extractAllImagesFromClipboard(
e.clipboardData ?? null,
);
if (images.length > 0) {
e.preventDefault();
onImagesSelect(images);
}
}}
/>
<div className="border-t border-border px-3 py-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
Try:
</span>
{examplePrompts.map((prompt) => (
<Button
key={prompt}
type="button"
variant="secondary"
size="sm"
className="h-7 max-w-full rounded-full px-2.5 text-[11px]"
onClick={() => onAiTemplateSelect(prompt)}
disabled={aiLoading || !canUseAi}
>
<Image
src={preview}
alt={`Attached image ${index + 1}`}
className="h-16 w-16 rounded-md object-cover ring-1 ring-primary/30"
width={64}
height={64}
unoptimized
/>
<Button
variant="destructive"
size="icon"
className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full"
onClick={() => onImageRemove(index)}
aria-label={`Remove image ${index + 1}`}
>
<X className="h-2.5 w-2.5" />
</Button>
</motion.div>
<span className="truncate">{prompt}</span>
</Button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex items-center justify-between gap-2">
<ImagePicker
onFilesSelect={onImagesSelect}
disabled={aiLoading || !canUseAi}
multiple
variant="ghost"
size="sm"
className="gap-1.5 text-xs h-7 px-2 text-muted-foreground hover:text-foreground"
triggerRef={imageTriggerRef}
>
<ImageIcon className="h-3.5 w-3.5" />
Attach image
</ImagePicker>
<div className="flex items-center gap-1.5">
<HoverCard
openDelay={300}
closeDelay={100}
open={isPopoverOpen ? false : undefined}
>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<HoverCardTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hidden h-8 w-8 text-muted-foreground/70 hover:text-foreground md:inline-flex"
aria-label="Keyboard shortcuts"
>
<Info className="h-3.5 w-3.5" />
</Button>
</PopoverTrigger>
</HoverCardTrigger>
<PopoverContent
align="end"
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-1.5">
<HoverCard
openDelay={300}
closeDelay={100}
open={isPopoverOpen ? false : undefined}
>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<HoverCardTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hidden h-8 w-8 text-muted-foreground/70 hover:text-foreground md:inline-flex"
aria-label="Keyboard shortcuts"
>
<Info className="h-3.5 w-3.5" />
</Button>
</PopoverTrigger>
</HoverCardTrigger>
<PopoverContent
align="start"
side="top"
sideOffset={6}
className="w-52 p-3"
>
<ShortcutsList os={os} />
</PopoverContent>
</Popover>
<HoverCardContent
align="start"
side="top"
sideOffset={6}
className="w-52 p-3"
>
<ShortcutsList os={os} />
</PopoverContent>
</Popover>
<HoverCardContent
align="end"
side="top"
sideOffset={6}
className="w-52 p-3"
>
<ShortcutsList os={os} />
</HoverCardContent>
</HoverCard>
</HoverCardContent>
</HoverCard>
{events.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={onAiSummarize}
disabled={aiLoading || !canUseAi}
className="h-9 gap-1.5 rounded-xl px-3 text-xs text-muted-foreground hover:text-primary"
>
<Bot className="h-3 w-3" />
Summarize
</Button>
)}
{events.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={onAiSummarize}
disabled={aiLoading || !canUseAi}
className="h-9 gap-1.5 px-3 text-xs text-muted-foreground hover:text-primary"
>
<Bot className="h-3 w-3" />
Summarize
</Button>
)}
</div>
<Button
size="sm"
className="h-10 gap-1.5 rounded-xl px-4 text-xs"
className="h-10 gap-1.5 px-4 text-xs"
onClick={onAiCreate}
disabled={
aiLoading || !canUseAi || (!aiPrompt.trim() && !hasImages)
@@ -468,10 +411,84 @@ export const AIToolbar = ({
) : (
<Sparkles className="h-3.5 w-3.5" />
)}
{aiLoading ? "Generating" : "Generate draft"}
{aiLoading ? "Generating..." : "Generate event"}
</Button>
</div>
</div>
<div className="rounded-[10px] bg-card p-3 shadow-[0_0_0_1px_rgba(0,0,0,0.08)]">
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
Attachments
</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
Add screenshots, flyers, or pasted images alongside the prompt.
</p>
</div>
<span className="rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
{imagePreviews.length} file{imagePreviews.length === 1 ? "" : "s"}
</span>
</div>
<ImagePicker
onFilesSelect={onImagesSelect}
disabled={aiLoading || !canUseAi}
multiple
variant="outline"
size="sm"
className="h-10 w-full justify-center gap-2"
triggerRef={imageTriggerRef}
>
<ImageIcon className="h-4 w-4" />
Attach images
</ImagePicker>
<AnimatePresence>
{hasImages ? (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.15 }}
className="mt-3 grid gap-2 sm:grid-cols-2"
>
{imagePreviews.map((preview, index) => (
<motion.div
key={preview}
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.96 }}
transition={{ duration: 0.12 }}
className="relative overflow-hidden rounded-[8px] bg-muted"
>
<Image
src={preview}
alt={`Attached image ${index + 1}`}
className="h-32 w-full object-cover"
width={256}
height={160}
unoptimized
/>
<Button
variant="destructive"
size="icon"
className="absolute top-2 right-2 h-7 w-7 rounded-full"
onClick={() => onImageRemove(index)}
aria-label={`Remove image ${index + 1}`}
>
<X className="h-3 w-3" />
</Button>
</motion.div>
))}
</motion.div>
) : (
<div className="mt-3 rounded-[8px] border border-dashed border-border px-3 py-6 text-center text-sm text-muted-foreground">
Drop or paste images here to pair them with the prompt.
</div>
)}
</AnimatePresence>
</div>
</div>
) : (
<div className="flex items-center gap-3 py-2">

View File

@@ -19,6 +19,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { formatEventRangeLabel } from "@/lib/event-date-format";
import { getEventValidationIssues } from "@/lib/event-form";
import type { CalendarEvent } from "@/lib/types";
interface EventCardProps {
@@ -28,9 +29,11 @@ interface EventCardProps {
}
export const EVENT_CARD_SURFACE_CLASSES =
"glass-card group cursor-pointer p-4 transition-[background-color,border-color,transform] duration-150 hover:-translate-y-0.5 hover:bg-accent/30 hover:border-primary/15";
"group cursor-pointer rounded-[10px] bg-card p-4 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_2px_rgba(0,0,0,0.04),0_0_0_1px_#fafafa] transition-[background-color,transform,box-shadow] duration-150 hover:-translate-y-0.5 hover:bg-accent/20";
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
const validationIssues = getEventValidationIssues(event);
const handleEdit = () => {
onEdit({
id: event.id,
@@ -66,7 +69,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
</p>
)}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Clock className="h-3 w-3 shrink-0" />
{formatEventRangeLabel(event)}
@@ -83,7 +86,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
<Button
variant="link"
size="sm"
className="h-auto gap-1 p-0 text-xs text-primary/70 hover:text-primary"
className="h-auto gap-1 p-0 text-xs"
asChild
>
<a
@@ -102,6 +105,12 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
{event.recurrenceRule && (
<RRuleDisplay rrule={event.recurrenceRule} start={event.start} />
)}
{validationIssues.length > 0 && (
<div className="rounded-[8px] bg-[#fff4f2] px-3 py-2 text-xs text-[#b42318] shadow-[inset_0_0_0_1px_rgba(180,35,24,0.14)] dark:bg-[#2a1715] dark:text-[#ff8a80]">
Warning: {validationIssues[0]}.
</div>
)}
</div>
<DropdownMenu>

View File

@@ -147,13 +147,15 @@ export const EventDialog = ({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="glass-strong max-w-md">
<DialogHeader>
<DialogTitle className="text-base">{titleText}</DialogTitle>
<DialogContent className="max-w-2xl rounded-[10px] bg-card p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_12px_40px_rgba(0,0,0,0.12)]">
<DialogHeader className="border-b border-foreground/10 px-6 py-5">
<DialogTitle className="text-[28px] tracking-[-0.06em]">
{titleText}
</DialogTitle>
<DialogDescription>{descriptionText}</DialogDescription>
</DialogHeader>
<form className="space-y-3" onSubmit={onSubmit}>
<form className="grid gap-6 px-6 py-5" onSubmit={onSubmit}>
{isAiDraft && (
<div className="rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-xs leading-relaxed text-primary">
This draft was generated from natural language. Double-check
@@ -161,149 +163,165 @@ export const EventDialog = ({
</div>
)}
<div className="space-y-1.5">
<Label htmlFor="event-title">Title</Label>
<Input
id="event-title"
placeholder="Event title"
className="font-medium"
{...register("title")}
/>
{errors.title && (
<p className="text-xs text-destructive">{errors.title.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="event-description">Description / notes</Label>
<Textarea
id="event-description"
className="field-sizing-content min-h-[60px] max-h-40 resize-none placeholder:text-muted-foreground/50"
placeholder="Add a description..."
{...register("description")}
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<section className="grid gap-3">
<p className="font-mono text-[11px] uppercase text-muted-foreground">
Event details
</p>
<div className="space-y-1.5">
<Label htmlFor="event-location">Location</Label>
<Label htmlFor="event-title">Title</Label>
<Input
id="event-title"
placeholder="Event title"
className="font-medium"
{...register("title")}
/>
{errors.title && (
<p className="text-xs text-destructive">{errors.title.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="event-description">Description / notes</Label>
<Textarea
id="event-description"
className="field-sizing-content min-h-[60px] max-h-40 resize-none placeholder:text-muted-foreground/50"
placeholder="Add a description..."
{...register("description")}
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="event-location">Location</Label>
<Controller
name="location"
control={control}
render={({ field }) => (
<LocationAutocomplete
id="event-location"
onChange={field.onChange}
value={field.value}
/>
)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="event-url">URL</Label>
<Input id="event-url" placeholder="URL" {...register("url")} />
{errors.url && (
<p className="text-xs text-destructive">{errors.url.message}</p>
)}
</div>
</div>
</section>
<section className="grid gap-3">
<p className="font-mono text-[11px] uppercase text-muted-foreground">
Schedule
</p>
<div className="flex items-center gap-2 py-1">
<Controller
name="location"
name="allDay"
control={control}
render={({ field }) => (
<LocationAutocomplete
id="event-location"
onChange={field.onChange}
value={field.value}
<Checkbox
id="event-all-day"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(checked === true)
}
/>
)}
/>
<Label
htmlFor="event-all-day"
className="cursor-pointer text-sm font-normal"
>
All day
</Label>
</div>
<div className="space-y-1.5">
<Label htmlFor="event-url">URL</Label>
<Input id="event-url" placeholder="URL" {...register("url")} />
{errors.url && (
<p className="text-xs text-destructive">{errors.url.message}</p>
)}
</div>
</div>
<Controller
name="recurrenceRule"
control={control}
render={({ field }) => (
<RecurrencePicker
value={field.value}
onChange={field.onChange}
start={start}
<div className="space-y-2">
<Controller
name="start"
control={control}
render={({ field }) => (
<DateTimePicker
value={field.value}
onChange={field.onChange}
allDay={allDay}
placeholder="Start date"
/>
)}
/>
)}
/>
{errors.recurrenceRule && (
<p className="text-xs text-destructive">
{errors.recurrenceRule.message}
</p>
)}
<div className="flex items-center gap-2 py-1">
<Controller
name="allDay"
control={control}
render={({ field }) => (
<Checkbox
id="event-all-day"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(checked === true)
}
{!allDay && (
<Controller
name="end"
control={control}
render={() => (
<div className="flex gap-1 pl-0.5">
{DURATIONS.map(({ label, minutes }) => (
<Button
key={label}
type="button"
variant="ghost"
size="sm"
disabled={!start}
onClick={() =>
handleApplyDuration(minutes, allDay, start)
}
className="px-2 py-1 text-xs text-muted-foreground"
>
{label}
</Button>
))}
</div>
)}
/>
)}
/>
<Label
htmlFor="event-all-day"
className="cursor-pointer text-sm font-normal"
>
All day
</Label>
</div>
<div className="space-y-2">
<Controller
name="start"
control={control}
render={({ field }) => (
<DateTimePicker
value={field.value}
onChange={field.onChange}
allDay={allDay}
placeholder="Start date"
/>
)}
/>
{!allDay && (
<Controller
name="end"
control={control}
render={() => (
<div className="flex gap-1 pl-0.5">
{DURATIONS.map(({ label, minutes }) => (
<Button
key={label}
type="button"
variant="ghost"
size="sm"
disabled={!start}
onClick={() =>
handleApplyDuration(minutes, allDay, start)
}
className="px-2 py-1 text-xs text-muted-foreground"
>
{label}
</Button>
))}
</div>
render={({ field }) => (
<DateTimePicker
value={field.value}
onChange={field.onChange}
allDay={allDay}
placeholder="End date"
/>
)}
/>
)}
{errors.start && (
<p className="text-xs text-destructive">{errors.start.message}</p>
)}
{errors.end && (
<p className="text-xs text-destructive">{errors.end.message}</p>
)}
</div>
</section>
<section className="grid gap-3">
<p className="font-mono text-[11px] uppercase text-muted-foreground">
Recurrence
</p>
<Controller
name="end"
name="recurrenceRule"
control={control}
render={({ field }) => (
<DateTimePicker
<RecurrencePicker
value={field.value}
onChange={field.onChange}
allDay={allDay}
placeholder="End date"
start={start}
/>
)}
/>
{errors.start && (
<p className="text-xs text-destructive">{errors.start.message}</p>
{errors.recurrenceRule && (
<p className="text-xs text-destructive">
{errors.recurrenceRule.message}
</p>
)}
{errors.end && (
<p className="text-xs text-destructive">{errors.end.message}</p>
)}
</div>
</section>
<DialogFooter className="gap-2 sm:gap-0">
<Button

View File

@@ -17,13 +17,13 @@ export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-border/80 bg-muted/20 px-6 py-16 text-center"
className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-border/80 bg-muted/30 px-6 py-16 text-center shadow-[inset_0_0_0_1px_rgba(255,255,255,0.35)]"
>
<Calendar1Icon className="h-10 w-10 text-muted-foreground/40 mb-3" />
<h3 className="text-sm font-medium text-foreground">No events yet</h3>
<p className="mt-1 max-w-sm text-xs leading-relaxed text-muted-foreground/70">
Generate a draft from natural language or add an event manually to
start building your local calendar timeline.
Capture something with AI or open manual create from More to start
building your event timeline.
</p>
</motion.div>
);

View File

@@ -14,7 +14,7 @@ interface SettingsPanelProps {
}
const settingRowClasses =
"rounded-2xl border border-border/70 bg-background/55 p-4 shadow-sm backdrop-blur-sm";
"rounded-[10px] bg-card p-4 shadow-[0_0_0_1px_rgba(0,0,0,0.08)]";
export function SettingsPanel({
adminAiEnabled,

View File

@@ -5,18 +5,18 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none 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 transition-[color,box-shadow] overflow-hidden",
"inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-medium [&>svg]:size-3 [&>svg]:pointer-events-none focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 transition-[color,box-shadow,background-color] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
"bg-secondary text-secondary-foreground shadow-[0_0_0_1px_rgba(0,0,0,0.08)] [a&]:hover:bg-accent",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
"bg-[#ebf5ff] text-[#0068d6] shadow-[0_0_0_1px_rgba(0,0,0,0.08)] [a&]:hover:opacity-90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive/12 text-destructive shadow-[0_0_0_1px_rgba(255,91,79,0.2)] [a&]:hover:bg-destructive/18 focus-visible:ring-destructive/25",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
"bg-background text-foreground shadow-[0_0_0_1px_rgba(0,0,0,0.08)] [a&]:hover:bg-accent",
},
},
defaultVariants: {

View File

@@ -5,26 +5,24 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"active:scale-[.95] inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium duration-100 transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none 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 [&::-moz-focus-inner]:border-0 [&::-moz-focus-inner]:p-0",
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium transition-[background-color,color,box-shadow,opacity] duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&::-moz-focus-inner]:border-0 [&::-moz-focus-inner]:p-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
default: "bg-primary text-primary-foreground hover:opacity-92",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-destructive-foreground hover:opacity-92 focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/35",
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",
"bg-background text-foreground shadow-[0_0_0_1px_rgba(0,0,0,0.08)] hover:bg-accent",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
"bg-secondary text-secondary-foreground shadow-[0_0_0_1px_rgba(0,0,0,0.05)] hover:bg-accent",
ghost: "text-muted-foreground hover:bg-accent hover:text-foreground",
link: "text-[#0072f5] underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
},
},

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",
"bg-card text-card-foreground flex flex-col gap-6 rounded-[10px] py-6 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_2px_rgba(0,0,0,0.04),0_0_0_1px_#fafafa]",
className,
)}
{...props}

View File

@@ -38,7 +38,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-background/80 backdrop-blur-[2px]",
className,
)}
{...props}
@@ -60,7 +60,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-card text-card-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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-5 rounded-[10px] p-6 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_2px_rgba(0,0,0,0.04),0_0_0_1px_#fafafa] duration-200 sm:max-w-lg",
className,
)}
{...props}
@@ -69,7 +69,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className="focus:ring-ring/30 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-[6px] p-1 opacity-70 transition-[background-color,opacity] hover:opacity-100 focus:ring-[3px] focus:outline-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
@@ -84,7 +84,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
className={cn("flex flex-col gap-2 text-left", className)}
{...props}
/>
);
@@ -110,7 +110,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
className={cn("text-[24px] leading-none font-semibold tracking-[-0.04em]", className)}
{...props}
/>
);
@@ -123,7 +123,7 @@ function DialogDescription({
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-muted-foreground text-sm leading-6", className)}
{...props}
/>
);

View File

@@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[10px] p-1.5 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_8px_24px_rgba(0,0,0,0.08)]",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-[6px] px-2.5 py-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-[6px] py-2 pr-2.5 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-[6px] py-2 pr-2.5 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -155,7 +155,7 @@ function DropdownMenuLabel({
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
"px-2.5 py-1.5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase data-[inset]:pl-8",
className,
)}
{...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-[6px] px-2.5 py-2 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
@@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"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 min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-[10px] p-1.5 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_8px_24px_rgba(0,0,0,0.08)]",
className,
)}
{...props}

View File

@@ -8,9 +8,9 @@ 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 flex 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",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-10 w-full min-w-0 rounded-[8px] bg-background px-3 py-2 text-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] 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",
"focus-visible:ring-[3px] focus-visible:ring-ring/25",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}