feat(ui): normalize recipe dialogs + chip/category UX polish
This commit is contained in:
parent
1de4b73244
commit
48f7d40e87
6 changed files with 52 additions and 30 deletions
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue