style: refine settings taxonomy UI (field cells, icon headers, 2-col grid)

- PoolManager: defined field cells with value count, refined chips,
  inline ghost + button, focus ring, pending/empty states
- TaxonomyManager: icon-badged section headers, ordered 2-col grid,
  sync explainer note
- Settings page: wider container (max-w-4xl) for the grid to breathe

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:46:13 +02:00
parent e80c95f838
commit e1b3e8c3d5
3 changed files with 76 additions and 41 deletions
+2 -2
View File
@@ -19,8 +19,8 @@ export default async function ImpostazioniPage() {
<div> <div>
<h1 className="text-2xl font-bold text-[#1a1a1a] mb-6">Impostazioni</h1> <h1 className="text-2xl font-bold text-[#1a1a1a] mb-6">Impostazioni</h1>
<div className="space-y-6 max-w-2xl"> <div className="space-y-6 max-w-4xl">
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6"> <div className="bg-white rounded-xl border border-[#e5e7eb] p-6 max-w-md">
<h2 className="text-base font-semibold text-[#1a1a1a] mb-4">Analytics Profittabilità</h2> <h2 className="text-base font-semibold text-[#1a1a1a] mb-4">Analytics Profittabilità</h2>
<form action={handleSave} className="space-y-4"> <form action={handleSave} className="space-y-4">
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { X } from "lucide-react"; import { X, Plus } from "lucide-react";
export function PoolManager({ export function PoolManager({
label, label,
@@ -18,6 +18,8 @@ export function PoolManager({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const canAdd = input.trim().length > 0;
function handleAdd() { function handleAdd() {
const trimmed = input.trim(); const trimmed = input.trim();
if (!trimmed) return; if (!trimmed) return;
@@ -48,52 +50,68 @@ export function PoolManager({
} }
return ( return (
<div className="flex-1 min-w-[180px]"> <div
<p className="text-sm font-medium text-[#1a1a1a] mb-2">{label}</p> className={`rounded-lg border border-[#ececee] bg-[#fafafa] p-3.5 transition-opacity ${
isPending ? "opacity-60" : ""
<div className="flex flex-wrap gap-1.5 mb-3 min-h-[28px]"> }`}
{pool.length === 0 && ( >
<span className="text-xs text-[#a1a1aa]">Nessun valore</span> <div className="flex items-center justify-between mb-2.5">
<p className="text-sm font-medium text-[#1a1a1a]">{label}</p>
{pool.length > 0 && (
<span className="text-[11px] text-[#a1a1aa] tabular-nums">{pool.length}</span>
)} )}
{pool.map((v) => ( </div>
<div className="flex flex-wrap gap-1.5 mb-3 min-h-[26px]">
{pool.length === 0 ? (
<span className="text-xs text-[#a1a1aa] italic">Nessun valore</span>
) : (
pool.map((v) => (
<span <span
key={v} key={v}
className="inline-flex items-center gap-1 bg-[#f4f4f5] text-[#1a1a1a] text-xs px-2 py-0.5 rounded-full" className="group inline-flex items-center gap-1 rounded-md border border-[#e5e7eb] bg-white pl-2.5 pr-1 py-1 text-xs text-[#27272a] shadow-[0_1px_1px_rgba(0,0,0,0.02)]"
> >
{v} {v}
<button <button
type="button" type="button"
onClick={() => handleRemove(v)} onClick={() => handleRemove(v)}
disabled={isPending} disabled={isPending}
className="text-[#71717a] hover:text-red-500 transition-colors disabled:opacity-50" className="rounded p-0.5 text-[#c4c4c8] transition-colors hover:bg-red-50 hover:text-red-500 disabled:opacity-50"
aria-label={`Rimuovi ${v}`} aria-label={`Elimina ${v}`}
title={`Elimina "${v}" ovunque`}
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</button> </button>
</span> </span>
))} ))
)}
</div> </div>
<div className="flex gap-1.5"> <div className="relative">
<input <input
type="text" type="text"
value={input} value={input}
onChange={(e) => { setInput(e.target.value); setError(null); }} onChange={(e) => {
setInput(e.target.value);
setError(null);
}}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
placeholder="Aggiungi..." placeholder="Aggiungi valore"
disabled={isPending} disabled={isPending}
className="border border-[#e5e7eb] rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20 flex-1 min-w-0 disabled:opacity-50" aria-label={`Aggiungi valore a ${label}`}
className="h-8 w-full rounded-md border border-[#e5e7eb] bg-white pl-3 pr-9 text-sm text-[#1a1a1a] placeholder:text-[#a1a1aa] transition-shadow focus:border-[#1A463C]/30 focus:outline-none focus:ring-2 focus:ring-[#1A463C]/15 disabled:opacity-50"
/> />
<button <button
type="button" type="button"
onClick={handleAdd} onClick={handleAdd}
disabled={isPending || !input.trim()} disabled={isPending || !canAdd}
className="bg-[#1A463C] text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors disabled:opacity-40" aria-label={`Conferma aggiunta a ${label}`}
className="absolute right-1 top-1/2 -translate-y-1/2 grid h-6 w-6 place-items-center rounded text-[#1A463C] transition-colors hover:bg-[#1A463C]/10 disabled:text-[#c4c4c8] disabled:hover:bg-transparent"
> >
+ <Plus className="h-4 w-4" />
</button> </button>
</div> </div>
{error && <p className="text-xs text-red-500 mt-1">{error}</p>} {error && <p className="mt-1.5 text-xs text-red-500">{error}</p>}
</div> </div>
); );
} }
@@ -1,5 +1,6 @@
"use client"; "use client";
import { Tag, BookOpen } from "lucide-react";
import { PoolManager } from "./PoolManager"; import { PoolManager } from "./PoolManager";
import { addTaxonomyValue, removeTaxonomyValue } from "@/app/admin/impostazioni/actions"; import { addTaxonomyValue, removeTaxonomyValue } from "@/app/admin/impostazioni/actions";
@@ -19,11 +20,13 @@ const CATALOG_FIELDS: FieldDef[] = [
]; ];
function Section({ function Section({
icon,
title, title,
subtitle, subtitle,
fields, fields,
pools, pools,
}: { }: {
icon: React.ReactNode;
title: string; title: string;
subtitle: string; subtitle: string;
fields: FieldDef[]; fields: FieldDef[];
@@ -31,9 +34,17 @@ function Section({
}) { }) {
return ( return (
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6"> <div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
<h2 className="text-base font-semibold text-[#1a1a1a] mb-1">{title}</h2> <div className="flex items-start gap-3 mb-5">
<p className="text-xs text-[#71717a] mb-5">{subtitle}</p> <div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[#1A463C]/8 text-[#1A463C]">
<div className="flex flex-wrap gap-6"> {icon}
</div>
<div>
<h2 className="text-base font-semibold text-[#1a1a1a] leading-tight">{title}</h2>
<p className="text-xs text-[#71717a] mt-0.5">{subtitle}</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{fields.map((f) => ( {fields.map((f) => (
<PoolManager <PoolManager
key={f.id} key={f.id}
@@ -50,19 +61,25 @@ function Section({
export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) { export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) {
return ( return (
<> <div className="space-y-6">
<Section <Section
icon={<Tag className="h-[18px] w-[18px]" />}
title="Tassonomie Offerte" title="Tassonomie Offerte"
subtitle="Valori disponibili nei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte." subtitle="Valori dei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte."
fields={OFFER_FIELDS} fields={OFFER_FIELDS}
pools={pools} pools={pools}
/> />
<Section <Section
icon={<BookOpen className="h-[18px] w-[18px]" />}
title="Tassonomie Catalogo" title="Tassonomie Catalogo"
subtitle="Valori disponibili nei campi Fase, Offerta e Pacchetto del catalogo servizi." subtitle="Valori dei campi Fase, Offerta e Pacchetto del catalogo servizi."
fields={CATALOG_FIELDS} fields={CATALOG_FIELDS}
pools={pools} pools={pools}
/> />
</> <p className="text-xs text-[#a1a1aa] leading-relaxed">
I valori si sincronizzano ovunque: creandone uno dall&apos;editor offerte o dal catalogo
comparirà qui automaticamente. Eliminando un valore da qui viene rimosso ovunque sia usato.
</p>
</div>
); );
} }