feat: pagina Impostazioni redesign Quiet Luxury — sezioni tokenizzate + PoolManager tassonomie dual-mode

- impostazioni/page.tsx: sezione Analytics con input tariffa bordato (€/h, font-mono) + header uppercase
- TaxonomyManager: card sezione tokenizzata, griglie Offerte (2col) / Catalogo (3col)
- PoolManager: box bg-muted, badge conteggio mono, chip valori, add su Enter, delete cascade confermata
- DESIGN-SYSTEM.md: note pagina Impostazioni

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 12:14:52 +02:00
parent d444bd6064
commit dd2d148457
5 changed files with 502 additions and 100 deletions
+35 -32
View File
@@ -17,50 +17,53 @@ export default async function ImpostazioniPage() {
}
return (
<div className="space-y-6">
<PageHeader title="Impostazioni" />
<div className="space-y-8">
<PageHeader
title="Impostazioni"
subtitle="Configura le tariffe target e gestisci le tassonomie globali del sistema"
/>
<div className="space-y-6 max-w-4xl">
<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>
<section className="rounded-xl border border-border-light bg-card p-6 shadow-card">
<div className="max-w-2xl">
<h2 className="mb-1 text-xs font-bold uppercase tracking-wider text-foreground">
Analytics Profittabilità
</h2>
<p className="mb-6 text-xs text-tertiary">
Tariffa oraria target (/h). Usata per calcolare il costo ideale e il delta
profitto/perdita per ogni progetto.
</p>
<form action={handleSave} className="space-y-4">
<div>
<label
htmlFor="target_hourly_rate"
className="block text-sm font-medium text-[#1a1a1a] mb-1"
>
Tariffa oraria target (/h)
</label>
<p className="text-xs text-[#71717a] mb-2">
Usata per calcolare il costo ideale e il delta profitto/perdita per ogni progetto.
</p>
<div className="flex items-center gap-2">
<span className="text-sm text-[#71717a]"></span>
<input
id="target_hourly_rate"
name="target_hourly_rate"
type="number"
step="0.01"
min="0"
defaultValue={targetRate.toFixed(2)}
className="border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20 w-32"
/>
<span className="text-sm text-[#71717a]">/h</span>
</div>
<form action={handleSave} className="flex items-center gap-3">
<div className="relative rounded-lg border border-border transition-all duration-150 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary">
<span className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-xs font-medium text-tertiary">
</span>
<input
id="target_hourly_rate"
name="target_hourly_rate"
type="number"
step="0.01"
min="0"
defaultValue={targetRate.toFixed(2)}
aria-label="Tariffa oraria target"
className="w-36 bg-transparent py-2.5 pl-7 pr-12 font-mono text-xs font-semibold text-foreground focus:outline-none"
/>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-[11px] font-medium text-tertiary">
/h
</span>
</div>
<button
type="submit"
className="bg-[#1A463C] text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors"
className="rounded-lg bg-primary px-5 py-2.5 text-xs font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
Salva
</button>
</form>
</div>
</section>
<TaxonomyManager pools={pools} />
</div>
<TaxonomyManager pools={pools} />
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useState, useTransition } from "react";
import { X, Plus } from "lucide-react";
import { X } from "lucide-react";
export function PoolManager({
label,
@@ -18,8 +18,6 @@ export function PoolManager({
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const canAdd = input.trim().length > 0;
function handleAdd() {
const trimmed = input.trim();
if (!trimmed) return;
@@ -51,67 +49,62 @@ export function PoolManager({
return (
<div
className={`rounded-lg border border-[#ececee] bg-[#fafafa] p-3.5 transition-opacity ${
className={`flex flex-col justify-between rounded-xl border border-border-light bg-muted/40 p-5 transition-opacity ${
isPending ? "opacity-60" : ""
}`}
>
<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>
)}
</div>
<div>
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{label}
</span>
<span className="rounded border border-border-light bg-card px-1.5 py-0.5 font-mono text-[10px] font-bold tabular-nums text-tertiary">
{pool.length}
</span>
</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
key={v}
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}
<button
type="button"
onClick={() => handleRemove(v)}
disabled={isPending}
className="rounded p-0.5 text-[#c4c4c8] transition-colors hover:bg-red-50 hover:text-red-500 disabled:opacity-50"
aria-label={`Elimina ${v}`}
title={`Elimina "${v}" ovunque`}
<div className="mb-4 flex flex-wrap gap-1.5">
{pool.length === 0 ? (
<div className="w-full py-6 text-center text-xs italic text-tertiary">
Nessun valore inserito
</div>
) : (
pool.map((v) => (
<span
key={v}
className="inline-flex items-center gap-1 rounded-md border border-border/60 bg-card py-1 pl-2.5 pr-1.5 text-[11px] font-semibold text-foreground shadow-sm"
>
<X className="h-3 w-3" />
</button>
</span>
))
)}
{v}
<button
type="button"
onClick={() => handleRemove(v)}
disabled={isPending}
className="rounded p-0.5 text-tertiary transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-50"
aria-label={`Elimina ${v}`}
title={`Elimina "${v}" ovunque`}
>
<X className="h-3 w-3" />
</button>
</span>
))
)}
</div>
</div>
<div className="relative">
<input
type="text"
value={input}
onChange={(e) => {
setInput(e.target.value);
setError(null);
}}
onKeyDown={onKeyDown}
placeholder="Aggiungi valore"
disabled={isPending}
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
type="button"
onClick={handleAdd}
disabled={isPending || !canAdd}
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>
</div>
{error && <p className="mt-1.5 text-xs text-red-500">{error}</p>}
<input
type="text"
value={input}
onChange={(e) => {
setInput(e.target.value);
setError(null);
}}
onKeyDown={onKeyDown}
placeholder="Aggiungi valore..."
disabled={isPending}
aria-label={`Aggiungi valore a ${label}`}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-xs text-foreground transition-colors placeholder:text-tertiary focus:border-primary focus:outline-none disabled:opacity-50"
/>
{error && <p className="mt-1.5 text-xs text-destructive">{error}</p>}
</div>
);
}
@@ -25,26 +25,28 @@ function Section({
subtitle,
fields,
pools,
gridClass,
}: {
icon: React.ReactNode;
title: string;
subtitle: string;
fields: FieldDef[];
pools: Record<string, string[]>;
gridClass: string;
}) {
return (
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
<div className="flex items-start gap-3 mb-5">
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[#1A463C]/8 text-[#1A463C]">
<section className="rounded-xl border border-border-light bg-card p-6 shadow-card">
<div className="mb-6 flex items-start gap-3">
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-border-light bg-muted text-muted-foreground">
{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>
<h2 className="text-xs font-bold uppercase tracking-wider text-foreground">{title}</h2>
<p className="mt-0.5 text-xs text-tertiary">{subtitle}</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className={`grid gap-6 border-t border-border-light pt-4 ${gridClass}`}>
{fields.map((f) => (
<PoolManager
key={f.id}
@@ -55,28 +57,30 @@ function Section({
/>
))}
</div>
</div>
</section>
);
}
export function TaxonomyManager({ pools }: { pools: Record<string, string[]> }) {
return (
<div className="space-y-6">
<div className="space-y-8">
<Section
icon={<Tag className="h-[18px] w-[18px]" />}
icon={<Tag className="h-5 w-5" />}
title="Tassonomie Offerte"
subtitle="Valori dei campi Categoria, Ticket, Tipo e Obiettivo dell'editor offerte."
fields={OFFER_FIELDS}
pools={pools}
gridClass="grid-cols-1 md:grid-cols-2"
/>
<Section
icon={<BookOpen className="h-[18px] w-[18px]" />}
icon={<BookOpen className="h-5 w-5" />}
title="Tassonomie Catalogo"
subtitle="Valori dei campi Fase, Offerta e Pacchetto del catalogo servizi."
fields={CATALOG_FIELDS}
pools={pools}
gridClass="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
/>
<p className="text-xs text-[#a1a1aa] leading-relaxed">
<p className="max-w-4xl px-2 text-xs leading-relaxed text-tertiary">
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>