This commit is contained in:
2026-03-27 16:20:46 +03:00
commit 13b56d4960
260 changed files with 111131 additions and 0 deletions
@@ -0,0 +1,160 @@
"use client"
import { useRef } from "react"
import type { AsyncOption, BaseFieldControlProps } from "../types"
import { Loader2 } from "lucide-react"
import {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxEmpty,
} from "@/shared/components/ui/combobox"
const defaultGetOptionValue = (opt: any) => opt.value
const defaultGetOptionLabel = (opt: any) => opt.label
function defaultGetOptionKey(opt: any): string {
const v = defaultGetOptionValue(opt)
if (typeof v === "string" || typeof v === "number") return String(v)
return String(opt.id ?? JSON.stringify(v))
}
// ── Single-select ──
export type AsyncSelectFieldProps<TOption = AsyncOption> = BaseFieldControlProps<any> & {
options: TOption[]
loading?: boolean
onInputValueChange?: (value: string) => void
placeholder?: string
getOptionValue?: (option: TOption) => any
getOptionLabel?: (option: TOption) => string
getOptionKey?: (option: TOption) => string
}
export function AsyncSelectField<TOption = AsyncOption>({
value,
onChange,
onBlur,
disabled,
invalid,
options,
loading,
onInputValueChange,
placeholder = "Search...",
getOptionValue = defaultGetOptionValue,
getOptionLabel = defaultGetOptionLabel,
getOptionKey = defaultGetOptionKey,
}: AsyncSelectFieldProps<TOption>) {
const anchorRef = useRef<HTMLDivElement>(null)
return (
<div ref={anchorRef}>
<Combobox
value={value}
onValueChange={(val) => onChange(val)}
disabled={disabled}
onInputValueChange={(val, { reason }) => {
if (reason === "input-change") {
onInputValueChange?.(val)
}
}}
>
<ComboboxInput
placeholder={placeholder}
showClear={!!value}
onBlur={onBlur}
aria-invalid={invalid || undefined}
/>
<ComboboxContent anchor={anchorRef}>
<ComboboxList>
{loading && (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{!loading &&
options.map((opt) => (
<ComboboxItem key={getOptionKey(opt)} value={getOptionValue(opt)}>
{getOptionLabel(opt)}
</ComboboxItem>
))}
{!loading && options.length === 0 && (
<ComboboxEmpty>No results found</ComboboxEmpty>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}
// ── Multi-select ──
export type AsyncMultiSelectFieldProps<TOption = AsyncOption> = BaseFieldControlProps<any[]> & {
options: TOption[]
loading?: boolean
onInputValueChange?: (value: string) => void
placeholder?: string
getOptionValue?: (option: TOption) => any
getOptionLabel?: (option: TOption) => string
getOptionKey?: (option: TOption) => string
}
export function AsyncMultiSelectField<TOption = AsyncOption>({
value,
onChange,
onBlur,
disabled,
invalid,
options,
loading,
onInputValueChange,
placeholder = "Search...",
getOptionValue = defaultGetOptionValue,
getOptionLabel = defaultGetOptionLabel,
getOptionKey = defaultGetOptionKey,
}: AsyncMultiSelectFieldProps<TOption>) {
const anchorRef = useRef<HTMLDivElement>(null)
return (
<div ref={anchorRef}>
<Combobox
multiple
value={value ?? []}
onValueChange={(val) => onChange(val as any[])}
disabled={disabled}
onInputValueChange={(val, { reason }) => {
if (reason === "input-change") {
onInputValueChange?.(val)
}
}}
>
<ComboboxInput
placeholder={placeholder}
showClear={value && value.length > 0}
onBlur={onBlur}
aria-invalid={invalid || undefined}
/>
<ComboboxContent anchor={anchorRef}>
<ComboboxList>
{loading && (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{!loading &&
options.map((opt) => (
<ComboboxItem key={getOptionKey(opt)} value={getOptionValue(opt)}>
{getOptionLabel(opt)}
</ComboboxItem>
))}
{!loading && options.length === 0 && (
<ComboboxEmpty>No results found</ComboboxEmpty>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}
@@ -0,0 +1,28 @@
"use client"
import type { BaseFieldControlProps } from "../types"
import { Switch } from "@/shared/components/ui/switch"
export type CheckboxFieldProps = BaseFieldControlProps<boolean> & {
label?: string
}
export function CheckboxField({
value,
onChange,
onBlur,
name,
disabled,
invalid,
}: CheckboxFieldProps) {
return (
<Switch
checked={value}
onCheckedChange={(checked) => onChange(checked === true)}
onBlur={onBlur}
name={name}
disabled={disabled}
aria-invalid={invalid || undefined}
/>
)
}
@@ -0,0 +1,28 @@
import type { BaseFieldControlProps } from "../types"
import { Input } from "@/shared/components/ui/input"
export type FileInputFieldProps = BaseFieldControlProps<File | null> & {
accept?: string
}
export function FileInputField({
// value intentionally unused — file inputs cannot be controlled
onBlur,
name,
disabled,
invalid,
accept,
onChange,
}: FileInputFieldProps) {
return (
<Input
type="file"
accept={accept}
onBlur={onBlur}
name={name}
disabled={disabled}
aria-invalid={invalid || undefined}
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
/>
)
}
@@ -0,0 +1,45 @@
"use client"
import type { BaseFieldControlProps, SelectOption } from "../types"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
export type SelectFieldProps = BaseFieldControlProps<string> & {
placeholder?: string
options: SelectOption[]
}
export function SelectField({
value,
onChange,
onBlur,
name,
disabled,
invalid,
placeholder,
options,
}: SelectFieldProps) {
return (
<Select value={value} onValueChange={onChange} disabled={disabled} name={name}>
<SelectTrigger
className="w-full"
aria-invalid={invalid || undefined}
onBlur={onBlur}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
@@ -0,0 +1,31 @@
import type { BaseFieldControlProps } from "../types"
import { Input } from "@/shared/components/ui/input"
export type TextInputFieldProps = BaseFieldControlProps<string> & {
placeholder?: string
type?: React.HTMLInputTypeAttribute
}
export function TextInputField({
value,
onChange,
onBlur,
name,
disabled,
invalid,
placeholder,
type = "text",
}: TextInputFieldProps) {
return (
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
name={name}
disabled={disabled}
aria-invalid={invalid || undefined}
placeholder={placeholder}
type={type}
/>
)
}
@@ -0,0 +1,31 @@
import type { BaseFieldControlProps } from "../types"
import { Textarea } from "@/shared/components/ui/textarea"
export type TextareaFieldProps = BaseFieldControlProps<string> & {
placeholder?: string
rows?: number
}
export function TextareaField({
value,
onChange,
onBlur,
name,
disabled,
invalid,
placeholder,
rows,
}: TextareaFieldProps) {
return (
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
name={name}
disabled={disabled}
aria-invalid={invalid || undefined}
placeholder={placeholder}
rows={rows}
/>
)
}