Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 107 additions & 46 deletions docs/UI_DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,107 @@ when-to-read: prima di creare o modificare qualsiasi componente visivo
- **Hover inattivo:** `hover:border-editorial-accent/40 hover:text-editorial-accent`
- **Disabled:** `disabled:opacity-40 disabled:cursor-not-allowed`
- **Focus:** `focus:outline-none focus-visible:ring-2 focus-visible:ring-editorial-accent`
- **Tooltip obbligatorio:** ogni pulsante icon-only deve avere `title` + `aria-label`
- **Tooltip obbligatorio:** ogni pulsante icon-only usa `<IconButton>` — il tooltip è incluso automaticamente.
- **Nessuna variante locale:** i componenti feature non devono reintrodurre button/dot/label custom. Usa sempre le primitive condivise.

---

## Componenti
## Primitive condivise (`src/components/ui/`)

### Label di sezione
### IconButton — pulsanti icon-only (OBBLIGATORIO)

Ogni sezione ha intestazione icona + etichetta uppercase:
Ogni controllo icon-only **deve** usare `<IconButton>`. Non usare `<button>` raw per controlli visivi nell'app.

```tsx
<div className="flex items-center gap-1.5">
<IconName size={11} className="text-editorial-accent shrink-0" />
<p className="text-[10px] font-sans uppercase tracking-[0.35em] text-editorial-muted">
{t('chiave.etichetta')}
</p>
</div>
import { IconButton } from '../ui';

// Azione semplice
<IconButton size="md" tone="default" onClick={handler} title={t('chiave.tooltip')}>
<PlusIcon size={13} />
</IconButton>

// Toggle (stato attivo comunicato via tone + ariaPressed)
<IconButton
size="md"
tone={isActive ? 'accent' : 'default'}
onClick={() => setActive(!isActive)}
title={t('chiave.tooltip')}
ariaPressed={isActive}
>
<SomeIcon size={14} />
</IconButton>

// Tab ARIA (usa role/aria-selected/aria-controls invece di ariaPressed)
<IconButton
size="lg"
tone={activeTab === 'foo' ? 'accent' : 'default'}
onClick={() => setTab('foo')}
title={t('chiave.tab')}
id="panel-tab-foo"
role="tab"
aria-selected={activeTab === 'foo'}
aria-controls="panel-panel-foo"
>
<SomeIcon size={16} />
</IconButton>
```

**Varianti tone:** `default | accent | success | charcoal | muted | running`
**Varianti size:** `sm | md | lg`

Regole:
- Usa `ariaPressed` per toggle (on/off). Usa `aria-selected` per tab ARIA.
- Non usare `ariaPressed` e `aria-selected` insieme sullo stesso pulsante.
- Aggiungi `className="shrink-0"` se il button è in una flex row con elementi `w-full`.

---

### Tooltip — tooltip canonico (OBBLIGATORIO per controlli icon-only)

`IconButton` integra già `<Tooltip>` internamente — non aggiungere tooltip separati sui pulsanti.

Per tooltip su altri elementi (testo troncato, badge, etichette):

```tsx
import { Tooltip } from '../ui';

<Tooltip label="Testo del tooltip" side="bottom">
<span className="truncate">{longText}</span>
</Tooltip>
```

**Non usare** l'attributo HTML `title` per tooltip visivi sui controlli interattivi. `title` è accettabile solo per elementi non interattivi dove il tooltip nativo è sufficiente.

---

### StatusDot — indicatore stato compatto

Per indicatori di stato non interattivi (stage pipeline, stato chunk, ecc.):

```tsx
import { StatusDot } from '../ui';

<StatusDot tone="success" />
<StatusDot tone="running" />
<StatusDot tone="accent" />
```

**Tone disponibili:** gli stessi di `IconButton` — usa solo token `editorial-*`. Non usare classi Tailwind dirette come `bg-emerald-500`, `bg-amber-400`, ecc.

---

### SectionLabel — intestazione sezione con icona

Per intestazioni di sezione con icona + etichetta uppercase:

```tsx
import { SectionLabel } from '../ui';

<SectionLabel icon={SomeIcon} label={t('chiave.sezione')} />
```

Non reintrodurre il pattern `<div className="flex items-center gap-1.5">` manuale dove `SectionLabel` è sufficiente.

---

### Pulsanti pill (selezione tra opzioni)
Expand All @@ -77,54 +159,33 @@ Componente: `src/components/ui/PillButton.tsx`

---

### Pulsanti azione (icon-only)

Sempre **solo icona**, mai testo + icona nei pannelli libreria/configurazione:

```tsx
<button onClick={handler} title={t('...')} aria-label={t('...')}
className="rounded-full border border-editorial-border p-2 text-editorial-muted transition-colors
hover:border-editorial-accent/60 hover:text-editorial-accent
focus:outline-none focus-visible:ring-2 focus-visible:ring-editorial-accent">
<PlusIcon size={13} />
</button>
```

Componente: `src/components/ui/IconButton.tsx`

---

### Barra navigazione filtri (pattern LibraryPanel — OBBLIGATORIO)

Ogni gruppo di filtri/tab deve usare **esattamente** questo pattern:
Ogni gruppo di filtri/tab usa `<IconButton>` con separatore e label corsiva:

```tsx
<div className="flex items-center gap-2">
{OPTIONS.map((opt) => {
const isActive = current === opt;
return (
<button key={opt} onClick={() => setCurrent(opt)}
title={label(opt)} aria-label={label(opt)}
className={`rounded-full border p-2 transition-colors
focus:outline-none focus-visible:ring-2 focus-visible:ring-editorial-accent ${
isActive
? 'border-editorial-accent bg-editorial-accent text-white'
: 'border-editorial-border text-editorial-muted hover:border-editorial-accent/40 hover:text-editorial-accent'
}`}>
<SomeIcon size={14} />
</button>
);
})}
{OPTIONS.map((opt) => (
<IconButton
key={opt}
size="md"
tone={current === opt ? 'accent' : 'default'}
onClick={() => setCurrent(opt)}
title={label(opt)}
ariaPressed={current === opt}
>
<SomeIcon size={14} />
</IconButton>
))}
<span className="mx-1 h-4 w-px self-center bg-editorial-border/70" aria-hidden="true" />
<span className="self-center font-display text-sm italic text-editorial-ink">{label(current)}</span>
</div>
```

Regole:
- Pulsanti **solo icona** (descrizione in `title`/`aria-label`)
- Separatore: `span w-px h-4 bg-editorial-border/70`
- Label corsiva: `font-display text-sm italic text-editorial-ink`
- Hover inattivo: `hover:border-editorial-accent/40` (non `/60`)
- Hover inattivo: `hover:border-editorial-accent/40` (non `/60`) — gestito dal tone `default` di `IconButton`

---

Expand All @@ -134,7 +195,7 @@ Regole:

Prima di aggiungere un nuovo pulsante, tab, o filtro:
1. Cerca nell'app un componente analogo
2. Replica **esattamente** lo stesso stile e struttura
2. Usa la primitiva condivisa corrispondente (`IconButton`, `StatusDot`, `SectionLabel`, `Tooltip`)
3. Deviazioni richiedono approvazione esplicita dell'utente

Colori da **non usare** fuori dai componenti UI (`StyleGuide.tsx` per riferimento):
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-sql": "^2.4.0",
"@vitejs/plugin-react": "^5.0.4",
"class-variance-authority": "^0.7.1",
"diff": "^9.0.0",
"i18next": "^25.1.0",
"lucide-react": "^0.546.0",
Expand Down
Loading
Loading