feat(ui): normalize recipe dialogs + chip/category UX polish

This commit is contained in:
Shine1i 2026-02-26 10:13:01 +01:00
commit 48f7d40e87
6 changed files with 52 additions and 30 deletions

View file

@ -1,7 +1,15 @@
import { Button } from "@/components/ui/button";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type KeyboardEvent, type ReactElement, useId, useMemo, useState } from "react";
import {
type KeyboardEvent,
type ReactElement,
useEffect,
useId,
useMemo,
useRef,
useState,
} from "react";
type ChipInputProps = {
values: string[];
@ -19,12 +27,28 @@ export function ChipInput({
suggestions,
}: ChipInputProps): ReactElement {
const [draft, setDraft] = useState("");
const [isWrapped, setIsWrapped] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const listId = useId();
const suggestionSet = useMemo(
() => new Set((suggestions ?? []).map((value) => value.trim())),
[suggestions],
);
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
const syncWrapped = () => {
setIsWrapped(element.clientHeight > 44);
};
syncWrapped();
const observer = new ResizeObserver(syncWrapped);
observer.observe(element);
return () => observer.disconnect();
}, [values.length, draft]);
function addValue(rawValue: string, allowAny: boolean): void {
const trimmed = rawValue.trim();
if (!trimmed) {
@ -55,7 +79,10 @@ export function ChipInput({
}
return (
<div className="bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-4xl border bg-clip-padding px-1.5 py-1.5 text-sm transition-colors focus-within:ring-[3px]">
<div
ref={containerRef}
className={`bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-wrap items-center gap-1.5 border bg-clip-padding px-1.5 py-1.5 text-sm transition-colors focus-within:ring-[3px] ${isWrapped ? "corner-squircle rounded-xl" : "rounded-4xl"}`}
>
{values.map((value, index) => (
<span
key={`${value}-${index}`}

View file

@ -50,7 +50,7 @@ export function ImportDialog({
position="absolute"
overlayPosition="absolute"
overlayClassName="bg-transparent"
className="corner-squircle max-h-[650px] overflow-auto sm:max-w-2xl"
className="corner-squircle max-h-[650px] overflow-auto sm:max-w-2xl shadow-border"
>
<DialogHeader>
<DialogTitle>Import recipe</DialogTitle>

View file

@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
@ -6,8 +5,6 @@ import {
} from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ArrowDown01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactElement, useState } from "react";
import type { ModelProviderConfig } from "../../types";
import { FieldLabel } from "../shared/field-label";
@ -70,21 +67,13 @@ export function ModelProviderDialog({
</div>
<Collapsible open={optionalOpen} onOpenChange={setOptionalOpen}>
<CollapsibleTrigger asChild={true}>
<Button
<button
type="button"
variant="outline"
size="sm"
className="w-full justify-between"
className="flex w-full items-center justify-between text-left text-xs text-muted-foreground"
>
Optional
<HugeiconsIcon
icon={ArrowDown01Icon}
strokeWidth={2}
className={
optionalOpen ? "rotate-180 transition-transform" : "transition-transform"
}
/>
</Button>
<span className="font-semibold uppercase">Optional</span>
<span>{optionalOpen ? "Hide" : "Show"}</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-4">
<div className="grid gap-2">

View file

@ -285,7 +285,7 @@ export function RunDialog({
position="absolute"
overlayPosition="absolute"
overlayClassName="bg-transparent"
className="corner-squircle sm:max-w-2xl"
className="corner-squircle sm:max-w-2xl shadow-border"
>
<DialogHeader>
<DialogTitle>{kindLabel} settings</DialogTitle>
@ -294,7 +294,7 @@ export function RunDialog({
</p>
</DialogHeader>
<div className="flex items-center justify-between rounded-xl border bg-muted/20 px-3 py-2 text-sm">
<div className="flex items-center justify-between text-sm">
<span className="font-medium text-foreground">Preview mode</span>
<Switch
checked={kind === "preview"}

View file

@ -68,7 +68,7 @@ export function ProcessorsDialog({
position="absolute"
overlayPosition="absolute"
overlayClassName="bg-transparent"
className="corner-squircle max-h-[650px] overflow-auto sm:max-w-2xl"
className="corner-squircle max-h-[650px] overflow-auto sm:max-w-2xl shadow-border"
>
<VisuallyHidden.Root>
<DialogTitle>Processors</DialogTitle>

View file

@ -133,12 +133,15 @@ export function CategoryDialog({
Add values first, then set optional weights.
</p>
) : (
<div className="grid gap-2">
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{(config.values ?? []).map((value, index) => (
<div key={`${value}-weight`} className="flex items-center gap-3">
<span className="max-w-20 truncate text-xs text-muted-foreground">
<div key={`${value}-weight`} className="space-y-1">
<p
className="truncate text-xs text-muted-foreground"
title={value}
>
{value}
</span>
</p>
<Input
type="number"
className="nodrag w-full"
@ -236,15 +239,18 @@ export function CategoryDialog({
<p className="text-xs font-semibold uppercase text-muted-foreground">
Rule weights (optional)
</p>
<div className="grid gap-2">
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{(params.values ?? []).map((value, index) => (
<div
key={`${condition}-${value}-${index}-weight`}
className="flex items-center gap-3"
className="space-y-1"
>
<span className="w-28 truncate text-xs text-muted-foreground">
<p
className="truncate text-xs text-muted-foreground"
title={value}
>
{value}
</span>
</p>
<Input
type="number"
className="nodrag"