Compare commits

...

4 Commits

Author SHA1 Message Date
ddc7a82e0e refactor(ai-event): migrate to OpenRouter chat.send API
Replace the deprecated callModel / getText pattern with the chat.send
method and extract the response content via extractContentFromChatResponse.
This aligns with the current OpenRouter SDK interface.
2026-04-15 18:21:23 -04:00
6bc84d5b58 feat(date-time-picker): add native date input alongside calendar popover
Replace the button-only trigger with a native <input type="date"> (or
datetime-local when not all-day) paired with an icon button that opens
the calendar popover.  This gives users direct keyboard entry while
keeping the rich calendar + quick-shortcut picker accessible.

- Add getInputValue helper to format the current value for the native
  input.
- Import the Input component.
- Restructure the layout to place the input and popover trigger
  side-by-side.
2026-04-15 18:21:17 -04:00
29bf4d2200 fix(time-picker): improve layout stability, a11y, and separator styling
- Change input group horizontal padding from px-3 to pl-3 pr-2 to
  better accommodate the trigger button.
- Add shrink-0 to the trigger button so it does not collapse in flex
  layouts.
- Mark the Clock icon with aria-hidden to keep it out of the
  accessibility tree.
- Forward the className prop in TimePickerSeparator and apply default
  muted-foreground text styling.
2026-04-15 18:21:09 -04:00
8d7948298b fix(button): suppress Firefox inner focus ring and padding
Add [&::-moz-focus-inner]:border-0 and [&::-moz-focus-inner]:p-0 to
the base buttonVariants to prevent the extra inner dotted outline and
padding that Firefox renders on focused buttons.
2026-04-15 18:21:03 -04:00
4 changed files with 76 additions and 56 deletions

View File

@@ -39,13 +39,17 @@ Rules:
`; `;
const callTextOnly = async (systemPrompt: string, prompt: string) => { const callTextOnly = async (systemPrompt: string, prompt: string) => {
const result = openRouterClient.callModel({ const response = await openRouterClient.chat.send({
model: MODEL, chatRequest: {
instructions: systemPrompt, model: MODEL,
input: prompt, messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
],
},
}); });
const rawResponse = await result.getText(); const rawResponse = extractContentFromChatResponse(response);
return { rawResponse }; return { rawResponse };
}; };

View File

@@ -13,6 +13,7 @@ import { CalendarIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -36,6 +37,15 @@ interface QuickShortcutResult {
nextValue: string; nextValue: string;
} }
function getInputValue(value: string, allDay: boolean): string {
const parsed = parseValue(value);
if (!parsed) return "";
return allDay
? format(parsed, "yyyy-MM-dd")
: format(parsed, "yyyy-MM-dd'T'HH:mm");
}
/** Parse the incoming ISO / date string into a Date object, or return undefined. */ /** Parse the incoming ISO / date string into a Date object, or return undefined. */
function parseValue(value: string): Date | undefined { function parseValue(value: string): Date | undefined {
if (!value) return undefined; if (!value) return undefined;
@@ -128,7 +138,6 @@ export function DateTimePicker({
value, value,
allDay, allDay,
); );
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [visibleMonth, setVisibleMonth] = React.useState(() => const [visibleMonth, setVisibleMonth] = React.useState(() =>
getCalendarMonthForValue(value, new Date()), getCalendarMonthForValue(value, new Date()),
@@ -168,52 +177,58 @@ export function DateTimePicker({
return ( return (
<div className={cn("w-full", className)}> <div className={cn("w-full", className)}>
{/* Date popover trigger */} <div className="flex items-center gap-2">
<Popover open={open} onOpenChange={setOpen}> <Input
<PopoverTrigger asChild> type={allDay ? "date" : "datetime-local"}
<Button value={getInputValue(value, allDay)}
type="button" onChange={(event) => {
variant="outline" onChange(event.target.value);
className={cn( }}
"flex flex-1 px-3 py-1 font-normal justify-start", placeholder={placeholder}
!displayLabel && "text-muted-foreground", className="flex-1"
)} />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className={cn(!displayLabel && "text-muted-foreground")}
aria-label={displayLabel ?? placeholder}
>
<CalendarIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[min(22rem,calc(100vw-2rem))] p-0"
align="start"
sideOffset={8}
> >
<CalendarIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" /> <div className="flex flex-wrap gap-1 border-b border-border px-3 py-2">
<span className="flex-1 truncate text-left"> {quickOptions.map((label) => (
{displayLabel ?? placeholder} <Button
</span> key={label}
</Button> type="button"
</PopoverTrigger> variant="ghost"
<PopoverContent size="sm"
className="w-[min(22rem,calc(100vw-2rem))] p-0" onClick={() => handleQuickSelect(label)}
align="start" className="h-auto px-2 py-1 text-xs text-muted-foreground"
sideOffset={8} >
> {label}
<div className="flex flex-wrap gap-1 border-b border-border px-3 py-2"> </Button>
{quickOptions.map((label) => ( ))}
<Button </div>
key={label} <Calendar
type="button" mode="single"
variant="ghost" month={visibleMonth}
size="sm" onMonthChange={setVisibleMonth}
onClick={() => handleQuickSelect(label)} selected={parsed}
className="h-auto px-2 py-1 text-xs text-muted-foreground" onSelect={handleDaySelect}
> initialFocus
{label} />
</Button> </PopoverContent>
))} </Popover>
</div> </div>
<Calendar
mode="single"
month={visibleMonth}
onMonthChange={setVisibleMonth}
selected={parsed}
onSelect={handleDaySelect}
initialFocus
/>
</PopoverContent>
</Popover>
</div> </div>
); );
} }

View File

@@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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",
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -723,7 +723,7 @@ function TimePickerInputGroup(props: DivProps) {
data-invalid={invalid ? "" : undefined} data-invalid={invalid ? "" : undefined}
{...inputGroupProps} {...inputGroupProps}
className={cn( className={cn(
"flex h-10 w-full cursor-text items-center gap-0.5 rounded-md border border-input bg-background px-3 py-2 shadow-xs outline-none transition-shadow", "flex h-10 w-full cursor-text items-center gap-0.5 rounded-md border border-input bg-background pl-3 pr-2 py-2 shadow-xs outline-none transition-shadow",
"has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50", "has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50",
invalid && "border-destructive ring-destructive/20", invalid && "border-destructive ring-destructive/20",
disabled && "cursor-not-allowed opacity-50", disabled && "cursor-not-allowed opacity-50",
@@ -1468,11 +1468,11 @@ function TimePickerTrigger(props: ButtonProps) {
ref={composedRef} ref={composedRef}
{...triggerProps} {...triggerProps}
className={cn( className={cn(
"ml-auto size-7 text-muted-foreground hover:text-foreground", "ml-auto size-7 shrink-0 text-muted-foreground hover:text-foreground",
className, className,
)} )}
> >
{children ?? <Clock className="size-4" />} {children ?? <Clock aria-hidden="true" className="size-4" />}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
); );
@@ -2149,7 +2149,7 @@ interface TimePickerSeparatorProps extends React.ComponentProps<"span"> {
} }
function TimePickerSeparator(props: TimePickerSeparatorProps) { function TimePickerSeparator(props: TimePickerSeparatorProps) {
const { asChild, children, ...separatorProps } = props; const { asChild, children, className, ...separatorProps } = props;
const SeparatorPrimitive = asChild ? SlotPrimitive.Slot : "span"; const SeparatorPrimitive = asChild ? SlotPrimitive.Slot : "span";
@@ -2157,6 +2157,7 @@ function TimePickerSeparator(props: TimePickerSeparatorProps) {
<SeparatorPrimitive <SeparatorPrimitive
aria-hidden="true" aria-hidden="true"
data-slot="time-picker-separator" data-slot="time-picker-separator"
className={cn("text-sm text-muted-foreground", className)}
{...separatorProps} {...separatorProps}
> >
{children ?? ":"} {children ?? ":"}