Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import clsx from "clsx";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Radix disallows an empty-string Item value (it's reserved to mean "no
|
||||
// selection" internally) - but several call sites here use "" to mean "all"
|
||||
// / "no filter". This sentinel maps "" <-> a real string at the Radix
|
||||
// boundary only, so callers can keep using "" as usual.
|
||||
const EMPTY_VALUE_SENTINEL = "__select-empty__";
|
||||
|
||||
/**
|
||||
* Custom dropdown replacing native <select> everywhere in the app. Native
|
||||
* select option lists are rendered by the OS/browser itself and cannot be
|
||||
* animated with CSS/JS in any browser - this is a real component (Radix's
|
||||
* accessible, keyboard-navigable primitive) so the open/close transition
|
||||
* (see select-in/select-out in tailwind.config.ts) actually applies.
|
||||
*/
|
||||
export function Select({
|
||||
value,
|
||||
onValueChange,
|
||||
options,
|
||||
placeholder,
|
||||
name,
|
||||
ariaLabel,
|
||||
triggerClassName,
|
||||
}: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
name?: string;
|
||||
ariaLabel?: string;
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Root
|
||||
value={value === "" ? EMPTY_VALUE_SENTINEL : value}
|
||||
onValueChange={(v) => onValueChange(v === EMPTY_VALUE_SENTINEL ? "" : v)}
|
||||
name={name}
|
||||
>
|
||||
<SelectPrimitive.Trigger
|
||||
aria-label={ariaLabel}
|
||||
className={clsx(
|
||||
"focus-ring flex items-center justify-between gap-2 rounded-md border border-slate-300 bg-white text-left shadow-sm outline-none data-[placeholder]:text-slate-400",
|
||||
triggerClassName ?? "px-3 py-2 text-sm",
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
<SelectPrimitive.Icon>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-slate-400" aria-hidden />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
position="popper"
|
||||
sideOffset={4}
|
||||
className="z-50 overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg data-[state=closed]:animate-select-out data-[state=open]:animate-select-in"
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{options.map((option) => (
|
||||
<SelectPrimitive.Item
|
||||
key={option.value}
|
||||
value={option.value === "" ? EMPTY_VALUE_SENTINEL : option.value}
|
||||
className="relative flex cursor-pointer select-none items-center rounded px-2 py-1.5 pl-7 text-sm text-slate-700 outline-none data-[highlighted]:bg-brand-50 data-[highlighted]:text-brand-700"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-2 inline-flex items-center">
|
||||
<Check className="h-3.5 w-3.5" aria-hidden />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
</SelectPrimitive.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user