Avvisi in Home: componente condiviso, più leggibili; turno palloni ridisegnato
Certificati e turno palloni usano lo stesso componente Avviso: icona propria per ogni caso (il colore non è l'unico segnale), testo più grande, elenchi di persone in righe con dettaglio a destra e conteggio, freccia sulle card cliccabili, comparsa graduale. Il turno palloni ha la pillola «Oggi»/«In arrivo» e i messaggi in riquadri. Solo aspetto: testi e regole di comparsa invariati. Test del componente, CHANGELOG, ARCHITECTURE e test/README aggiornati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7ae2c1884
commit
519f9aaff4
7 files changed
+258
-74
No files matched your search
@@ -85,7 +85,7 @@ quando il database non risponde.
|
||||
## UI
|
||||
|
||||
Componenti condivisi in `src/components/crapp/` (`ui-bits.tsx` per `Card`, `PageHeader`,
|
||||
`Section`, `StatTile`; i drawer di dettaglio `BadgeDrawer` e `ObiettivoDrawer`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche
|
||||
`Section`, `StatTile`; `Avviso.tsx` per gli avvisi colorati in Home; i drawer di dettaglio `BadgeDrawer` e `ObiettivoDrawer`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche
|
||||
schermate, pochi click.
|
||||
|
||||
In `src/components/ui/` restano solo le due primitive shadcn davvero usate, `drawer` (vaul) e
|
||||
|
||||
@@ -25,6 +25,10 @@ versioni `0.y.z` erano pre-release.
|
||||
|
||||
### Modificato
|
||||
|
||||
- **Avvisi in Home più leggibili** — certificati e turno palloni usano lo stesso componente (`Avviso`): icona
|
||||
propria per ogni caso (non solo il colore), titolo e testo più grandi, elenchi di persone in righe con il dettaglio
|
||||
a destra e il conteggio, freccia dove la card porta ai documenti, comparsa graduale. Solo aspetto: testi e regole
|
||||
di quando compaiono non cambiano. Nessuna migration.
|
||||
- **Tessera CSI tolta dalla dashboard** — in Profili spariscono il blocco «Tesseramento CSI» (numero e data
|
||||
tessera), il badge «Tesserato»/«Da tesserare» e il riquadro «Tesserati» della tab Squadra. L'export CSV non
|
||||
cambia. Le colonne a database restano, non più usate (DD-048). Nessuna migration.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||
import { Reveal } from "@/components/motion/Reveal";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Gravità di un avviso in Home. Il colore non è l'unico segnale: ogni avviso ha anche un'icona
|
||||
* sua e un titolo, così si distingue anche senza vedere i colori.
|
||||
*/
|
||||
export type TonoAvviso = "critico" | "attenzione" | "scaduto" | "azione";
|
||||
|
||||
const toni: Record<TonoAvviso, string> = {
|
||||
critico: "bg-destructive text-destructive-foreground",
|
||||
attenzione: "bg-warning text-warning-foreground",
|
||||
scaduto: "bg-primary text-primary-foreground",
|
||||
azione: "bg-accent-grad text-accent-foreground",
|
||||
};
|
||||
|
||||
/** Una riga di un elenco: a sinistra chi, a destra il dettaglio (es. la scadenza). */
|
||||
export type RigaAvviso = { chi: string; cosa: string };
|
||||
|
||||
type ProprietaCorpo = {
|
||||
tono: TonoAvviso;
|
||||
icona: LucideIcon;
|
||||
titolo: string;
|
||||
/** Frasi di spiegazione, una per paragrafo. */
|
||||
testi?: string[];
|
||||
/** Elenco di persone con il loro dettaglio; con più di una riga compare il conteggio. */
|
||||
elenco?: RigaAvviso[];
|
||||
/** Mostra la freccia: tutta la card porta a un'altra schermata. */
|
||||
cliccabile?: boolean;
|
||||
/** Parola breve accanto al titolo (es. «Oggi»), per dire subito quando riguarda. */
|
||||
etichetta?: string;
|
||||
/** I testi in riquadri propri, uno per messaggio: per chi deve fare una cosa precisa. */
|
||||
riquadri?: boolean;
|
||||
};
|
||||
|
||||
/** Contenuto dell'avviso, senza margini né link: è la parte che si può verificare nei test. */
|
||||
export function CorpoAvviso({
|
||||
tono,
|
||||
icona: Icona,
|
||||
titolo,
|
||||
testi = [],
|
||||
elenco = [],
|
||||
cliccabile = false,
|
||||
etichetta,
|
||||
riquadri = false,
|
||||
}: ProprietaCorpo) {
|
||||
return (
|
||||
<div className={cn("rounded-3xl p-5 shadow-pop", toni[tono])}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
aria-hidden
|
||||
className="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-current/15"
|
||||
>
|
||||
<Icona className="h-6 w-6" />
|
||||
</span>
|
||||
<p className="min-w-0 flex-1 font-display text-lg uppercase leading-tight tracking-wide">
|
||||
{titolo}
|
||||
</p>
|
||||
{etichetta ? (
|
||||
<span className="rounded-full bg-black/25 px-3 py-1 text-[13px] font-bold uppercase tracking-wide">
|
||||
{etichetta}
|
||||
</span>
|
||||
) : null}
|
||||
{elenco.length > 1 ? (
|
||||
<span
|
||||
aria-label={`${elenco.length} persone`}
|
||||
className="rounded-full bg-current/15 px-3 py-1 text-[13px] font-bold tabular-nums"
|
||||
>
|
||||
{elenco.length}
|
||||
</span>
|
||||
) : null}
|
||||
{cliccabile ? <ChevronRight aria-hidden className="h-6 w-6 shrink-0" /> : null}
|
||||
</div>
|
||||
|
||||
{testi.map((t) => (
|
||||
<p
|
||||
key={t}
|
||||
className={cn(
|
||||
"mt-3 text-[15px] leading-snug",
|
||||
riquadri && "rounded-2xl bg-black/15 p-3.5 font-semibold",
|
||||
)}
|
||||
>
|
||||
{t}
|
||||
</p>
|
||||
))}
|
||||
|
||||
{elenco.length > 0 ? (
|
||||
<ul className="mt-3 divide-y divide-current/15 border-t border-current/15">
|
||||
{elenco.map((r) => (
|
||||
<li
|
||||
key={`${r.chi}-${r.cosa}`}
|
||||
className="flex items-baseline justify-between gap-3 py-2.5 text-[15px] leading-snug"
|
||||
>
|
||||
<span className="min-w-0 truncate font-semibold">{r.chi}</span>
|
||||
<span className="shrink-0 text-right tabular-nums">{r.cosa}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avviso in Home: comparsa graduale, margini della colonna e, se serve, link ai documenti del
|
||||
* profilo. Tutti gli avvisi (certificati, turno palloni) passano da qui per avere lo stesso aspetto.
|
||||
*/
|
||||
export function Avviso({ indice = 0, ...corpo }: ProprietaCorpo & { indice?: number }) {
|
||||
const card = <CorpoAvviso {...corpo} />;
|
||||
return (
|
||||
<Reveal indice={indice} className="mx-5 mt-4">
|
||||
{corpo.cliccabile ? (
|
||||
<Link
|
||||
to="/profilo"
|
||||
search={{ tab: "documenti" }}
|
||||
className="premi block rounded-3xl focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
||||
>
|
||||
{card}
|
||||
</Link>
|
||||
) : (
|
||||
<div role="status">{card}</div>
|
||||
)}
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { CalendarClock, FileClock, FileX } from "lucide-react";
|
||||
import { Avviso, type RigaAvviso } from "@/components/crapp/Avviso";
|
||||
import { useProfili } from "@/lib/profili";
|
||||
import {
|
||||
avvisiCertificatiUtente,
|
||||
@@ -12,45 +12,6 @@ import { oggiISO } from "@/lib/palloni-core";
|
||||
import { useIsAdmin, useRuoliPronti } from "@/lib/ruoli";
|
||||
import { useGiocatoreBase } from "@/lib/user-store";
|
||||
|
||||
function Card({
|
||||
colore,
|
||||
titolo,
|
||||
righe,
|
||||
cliccabile,
|
||||
}: {
|
||||
colore: "warning" | "primary" | "destructive";
|
||||
titolo: string;
|
||||
righe: string[];
|
||||
cliccabile?: boolean;
|
||||
}) {
|
||||
const classi = `mx-5 mt-4 block rounded-3xl p-4 shadow-pop ${
|
||||
colore === "warning"
|
||||
? "bg-warning text-warning-foreground"
|
||||
: colore === "destructive"
|
||||
? "bg-destructive text-destructive-foreground"
|
||||
: "bg-primary text-primary-foreground"
|
||||
} ${cliccabile ? "premi" : ""}`;
|
||||
const contenuto = (
|
||||
<>
|
||||
<p className="flex items-center gap-2 font-display text-lg uppercase leading-none">
|
||||
<AlertCircle className="h-5 w-5" /> {titolo}
|
||||
</p>
|
||||
{righe.map((r) => (
|
||||
<p key={r} className="mt-2 text-xs leading-snug opacity-90">
|
||||
{r}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return cliccabile ? (
|
||||
<Link to="/profilo" search={{ tab: "documenti" }} className={classi}>
|
||||
{contenuto}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={classi}>{contenuto}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avviso certificati mancanti/scaduti/in scadenza (DD-035, DD-041, DD-046): calcolato al volo, niente push.
|
||||
* Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7.
|
||||
@@ -72,28 +33,31 @@ export function AvvisoCertificati() {
|
||||
base,
|
||||
});
|
||||
const cardPersonale = personaleMancante ? (
|
||||
<Card
|
||||
colore="destructive"
|
||||
<Avviso
|
||||
tono="critico"
|
||||
icona={FileX}
|
||||
titolo="Certificato mancante"
|
||||
cliccabile
|
||||
righe={["Non hai ancora caricato il certificato medico: caricalo nei documenti"]}
|
||||
testi={["Non hai ancora caricato il certificato medico: caricalo nei documenti"]}
|
||||
/>
|
||||
) : personale ? (
|
||||
personale.giorni < 0 ? (
|
||||
<Card
|
||||
colore="primary"
|
||||
<Avviso
|
||||
tono="scaduto"
|
||||
icona={FileClock}
|
||||
titolo="Certificato scaduto"
|
||||
cliccabile
|
||||
righe={[
|
||||
testi={[
|
||||
`Il tuo certificato medico è scaduto il ${formatDataBreve(personale.scadenza)}: caricane uno nuovo`,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Card
|
||||
colore="warning"
|
||||
<Avviso
|
||||
tono="attenzione"
|
||||
icona={CalendarClock}
|
||||
titolo="Certificato in scadenza"
|
||||
cliccabile
|
||||
righe={[`Il tuo certificato medico ${testoScadenza(personale.giorni)}`]}
|
||||
testi={[`Il tuo certificato medico ${testoScadenza(personale.giorni)}`]}
|
||||
/>
|
||||
)
|
||||
) : null;
|
||||
@@ -104,30 +68,33 @@ export function AvvisoCertificati() {
|
||||
<>
|
||||
{cardPersonale}
|
||||
{mancanti.length > 0 ? (
|
||||
<Card
|
||||
colore="destructive"
|
||||
<Avviso
|
||||
tono="critico"
|
||||
icona={FileX}
|
||||
titolo="Certificati mancanti"
|
||||
righe={mancanti.map((m) => `${m.nome} ${m.cognome} — non caricato`)}
|
||||
elenco={mancanti.map((m) => ({ chi: `${m.nome} ${m.cognome}`, cosa: "non caricato" }))}
|
||||
/>
|
||||
) : null}
|
||||
{scaduti.length > 0 ? (
|
||||
<Card
|
||||
colore="primary"
|
||||
<Avviso
|
||||
tono="scaduto"
|
||||
icona={FileClock}
|
||||
titolo="Certificati scaduti"
|
||||
righe={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
||||
elenco={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
||||
/>
|
||||
) : null}
|
||||
{inScadenza.length > 0 ? (
|
||||
<Card
|
||||
colore="warning"
|
||||
<Avviso
|
||||
tono="attenzione"
|
||||
icona={CalendarClock}
|
||||
titolo="Certificati in scadenza"
|
||||
righe={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))}
|
||||
elenco={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function rigaStaff(a: AvvisoCertificato, dettaglio: string): string {
|
||||
return `${a.nome} ${a.cognome} — ${dettaglio}`;
|
||||
function rigaStaff(a: AvvisoCertificato, dettaglio: string): RigaAvviso {
|
||||
return { chi: `${a.nome} ${a.cognome}`, cosa: dettaglio };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Volleyball } from "lucide-react";
|
||||
import { Avviso } from "@/components/crapp/Avviso";
|
||||
import { formatData } from "@/lib/crapp-data";
|
||||
import { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core";
|
||||
import { useTurniPalloni } from "@/lib/palloni";
|
||||
@@ -17,6 +18,8 @@ export function PromemoriaPalloni() {
|
||||
|
||||
const oggi = oggiISO();
|
||||
const messaggi: string[] = [];
|
||||
// «Oggi» solo se c'è davvero qualcosa da fare oggi; altrimenti è un semplice promemoria.
|
||||
let oggiTocca = true;
|
||||
|
||||
for (const evento of lista) {
|
||||
if (evento.data !== oggi) continue;
|
||||
@@ -36,6 +39,7 @@ export function PromemoriaPalloni() {
|
||||
}
|
||||
|
||||
if (messaggi.length === 0) {
|
||||
oggiTocca = false;
|
||||
const prossimo = lista.find((e) => e.data >= oggi && turni[e.id] === io.id);
|
||||
if (!prossimo) return null;
|
||||
messaggi.push(
|
||||
@@ -44,15 +48,13 @@ export function PromemoriaPalloni() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-5 mt-4 rounded-3xl bg-accent-grad p-4 text-accent-foreground shadow-pop">
|
||||
<p className="flex items-center gap-2 font-display text-lg uppercase leading-none">
|
||||
<AlertCircle className="h-5 w-5" /> Turno palloni
|
||||
</p>
|
||||
{messaggi.map((m) => (
|
||||
<p key={m} className="mt-2 text-xs leading-snug opacity-90">
|
||||
{m}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<Avviso
|
||||
tono="azione"
|
||||
icona={Volleyball}
|
||||
titolo="Turno palloni"
|
||||
etichetta={oggiTocca ? "Oggi" : "In arrivo"}
|
||||
testi={messaggi}
|
||||
riquadri
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
|
||||
|
||||
| Cartella | Cosa verifica | Serve rete? |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
|
||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), avvisi in Home, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
|
||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
|
||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Check del componente degli avvisi in Home: `bun test/unit/avviso.test.ts`. */
|
||||
import assert from "node:assert/strict";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { CalendarClock, FileX } from "lucide-react";
|
||||
import { CorpoAvviso, type TonoAvviso } from "@/components/crapp/Avviso";
|
||||
|
||||
const html = (p: Parameters<typeof CorpoAvviso>[0]) =>
|
||||
renderToStaticMarkup(createElement(CorpoAvviso, p));
|
||||
|
||||
// --- ogni tono ha il suo colore, e l'icona è decorativa (il titolo dice già tutto) ---
|
||||
const classi: Record<TonoAvviso, string> = {
|
||||
critico: "bg-destructive",
|
||||
attenzione: "bg-warning",
|
||||
scaduto: "bg-primary",
|
||||
azione: "bg-accent-grad",
|
||||
};
|
||||
for (const [tono, classe] of Object.entries(classi) as [TonoAvviso, string][]) {
|
||||
const h = html({ tono, icona: FileX, titolo: "Titolo" });
|
||||
assert.ok(h.includes(classe), `${tono}: classe colore ${classe}`);
|
||||
assert.ok(h.includes("Titolo"), `${tono}: titolo`);
|
||||
assert.ok(h.includes('aria-hidden="true"'), `${tono}: icona nascosta ai lettori di schermo`);
|
||||
}
|
||||
|
||||
// --- testi: un paragrafo per frase ---------------------------------------------------
|
||||
const conTesti = html({
|
||||
tono: "attenzione",
|
||||
icona: CalendarClock,
|
||||
titolo: "Certificato in scadenza",
|
||||
testi: ["Prima frase.", "Seconda frase."],
|
||||
});
|
||||
assert.equal((conTesti.match(/<p /g) ?? []).length, 3, "titolo + due paragrafi");
|
||||
assert.ok(!conTesti.includes("<ul"), "senza elenco niente lista");
|
||||
|
||||
// --- elenco: righe chi/cosa, conteggio solo con più di una riga ----------------------
|
||||
const una = html({
|
||||
tono: "critico",
|
||||
icona: FileX,
|
||||
titolo: "Certificati mancanti",
|
||||
elenco: [{ chi: "Mario Rossi", cosa: "non caricato" }],
|
||||
});
|
||||
assert.ok(una.includes("Mario Rossi") && una.includes("non caricato"), "riga con chi e cosa");
|
||||
assert.ok(!una.includes("persone"), "una sola riga: nessun conteggio");
|
||||
|
||||
const tre = html({
|
||||
tono: "critico",
|
||||
icona: FileX,
|
||||
titolo: "Certificati mancanti",
|
||||
elenco: [
|
||||
{ chi: "Mario Rossi", cosa: "non caricato" },
|
||||
{ chi: "Luca Bianchi", cosa: "non caricato" },
|
||||
{ chi: "Anna Verdi", cosa: "non caricato" },
|
||||
],
|
||||
});
|
||||
assert.equal((tre.match(/<li /g) ?? []).length, 3, "una riga per persona");
|
||||
assert.ok(tre.includes('aria-label="3 persone"'), "conteggio accessibile");
|
||||
|
||||
// --- freccia solo se la card porta altrove -------------------------------------------
|
||||
const base = { tono: "scaduto", icona: FileX, titolo: "t" } as const;
|
||||
assert.ok(!html(base).includes("lucide-chevron-right"), "senza link niente freccia");
|
||||
assert.ok(
|
||||
html({ ...base, cliccabile: true }).includes("lucide-chevron-right"),
|
||||
"con link la freccia",
|
||||
);
|
||||
|
||||
// --- etichetta e riquadri (turno palloni) --------------------------------------------
|
||||
const palloni = html({
|
||||
tono: "azione",
|
||||
icona: FileX,
|
||||
titolo: "Turno palloni",
|
||||
etichetta: "Oggi",
|
||||
testi: ["Porta i palloni."],
|
||||
riquadri: true,
|
||||
});
|
||||
assert.ok(palloni.includes("Oggi"), "etichetta mostrata");
|
||||
assert.ok(palloni.includes("bg-black/15"), "il messaggio è in un riquadro");
|
||||
assert.ok(palloni.includes("bg-black/25"), "etichetta in pillola");
|
||||
assert.ok(!html({ ...base }).includes("bg-black/25"), "senza etichetta niente pillola");
|
||||
assert.ok(
|
||||
!html({ ...base, testi: ["x"] }).includes("bg-black/15"),
|
||||
"senza riquadri testo semplice",
|
||||
);
|
||||
|
||||
console.log("avviso: ok");
|
||||
Reference in new issue
Block a user