Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Once the package is installed, simply add the reference to the Bootstrap Italia
<head>
<!-- other css imports -->
<link rel="stylesheet" href="_content/BitBlazor/bootstrap-italia/css/bootstrap-italia.min.css" />
<!-- optional: include Bootstrap Italia default fonts (Titillium Web, Lora, Roboto Mono) -->
<link rel="stylesheet" href="_content/BitBlazor/css/fonts.css" />
</head>
```

Expand Down
208 changes: 208 additions & 0 deletions docs/components/modal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# BitModal

The `BitModal` component provides a [modal dialog using Bootstrap Italia styles](https://italia.github.io/bootstrap-italia/docs/componenti/modale/).

## Namespace

```csharp
BitBlazor.Components
```

## Description

The Modal component renders an accessible dialog that overlays the page. Visibility is controlled entirely through Blazor state using the `IsVisible` / `@bind-IsVisible` pattern — no JavaScript interop is required.

## Parameters

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `IsVisible` | `bool` | ✗ | `false` | Controls whether the modal is displayed. Use `@bind-IsVisible` for two-way binding. |
| `IsVisibleChanged` | `EventCallback<bool>` | ✗ | - | Callback invoked when the modal requests a visibility change (close). Wired automatically by `@bind-IsVisible`. |
| `BodyContent` | `RenderFragment` | ✓ | - | The main body content of the modal. |
| `Title` | `string?` | ✗ | `null` | Text displayed in the header as an `h2` element. When set, the modal uses `aria-labelledby` for accessibility. |
| `TitleId` | `string?` | ✗ | `null` | Override for the `id` attribute of the title `h2` element. Defaults to `"{modal-id}-title"`. |
| `AriaLabel` | `string?` | ✗ | `null` | Sets the `aria-label` on the modal element. Use when `Title` is not set. |
| `AriaDescribedById` | `string?` | ✗ | `null` | Sets the `aria-describedby` attribute, pointing to a description element inside the modal. |
| `HeaderContent` | `RenderFragment?` | ✗ | `null` | Fully custom header content, replacing the default `Title`-based header. |
| `FooterContent` | `RenderFragment?` | ✗ | `null` | Footer content. When `null`, no footer element is rendered. |
| `ShowCloseButton` | `bool` | ✗ | `true` | Whether to show the close (×) button in the header. |
| `CloseButtonAriaLabel` | `string` | ✗ | `"Chiudi"` | The `aria-label` for the close button. |
| `Size` | `ModalSize` | ✗ | `Default` | The size of the modal dialog. |
| `Position` | `ModalPosition` | ✗ | `Default` | The positioning of the modal dialog. |
| `Type` | `ModalType` | ✗ | `Default` | The visual type variant of the modal. |
| `Backdrop` | `ModalBackdrop` | ✗ | `Default` | Controls backdrop visibility and whether clicking it closes the modal. |
| `ScrollableContent` | `bool` | ✗ | `false` | Enables internal body scrolling, keeping header and footer always visible. |
| `FooterShadow` | `bool` | ✗ | `false` | Adds a top shadow to the footer element (`modal-footer-shadow`). |
| `Fade` | `bool` | ✗ | `true` | Enables the CSS fade animation. Set to `false` for instant show/hide. |
| `OnClose` | `EventCallback` | ✗ | - | Callback invoked just before the modal is closed. |

## Enumerations

### ModalSize

| Value | CSS class on `.modal-dialog` | Description |
|-------|------------------------------|-------------|
| `Default` | *(none)* | Standard size |
| `Small` | `modal-sm` | Small dialog |
| `Large` | `modal-lg` | Large dialog |
| `ExtraLarge` | `modal-xl` | Extra-large dialog |

### ModalPosition

| Value | Effect | Description |
|-------|--------|-------------|
| `Default` | *(none)* | Top-center (default Bootstrap Italia) |
| `Centered` | `modal-dialog-centered` on dialog | Vertically centered |
| `Left` | `modal-dialog-left` on dialog + `it-dialog-scrollable` on modal | Full-height, left-aligned |
| `Right` | `modal-dialog-right` on dialog + `it-dialog-scrollable` on modal | Full-height, right-aligned |

### ModalType

| Value | CSS class on `.modal` | Description |
|-------|----------------------|-------------|
| `Default` | *(none)* | Standard modal |
| `Alert` | `alert-modal` | Alert modal, used with an icon in the header |
| `LinkList` | `it-dialog-link-list` | Optimised for navigation link lists |
| `Popconfirm` | `popconfirm-modal` | Compact confirmation dialog |

### ModalBackdrop

| Value | Description |
|-------|-------------|
| `Default` | Backdrop is shown; clicking it closes the modal |
| `Static` | Backdrop is shown; clicking it does **not** close the modal |
| `None` | No backdrop is rendered |

## Usage Examples

### Basic modal with two-way binding

```razor
<button class="btn btn-primary" @onclick="() => isOpen = true">
Apri la modale
</button>

<BitModal @bind-IsVisible="isOpen"
Id="example-modal"
Title="Intestazione modale"
AriaDescribedById="example-modal-desc">
<BodyContent>
<p id="example-modal-desc">Descrizione scopo della modale.</p>
<p>Contenuto della modale.</p>
</BodyContent>
<FooterContent>
<button class="btn btn-outline-primary btn-sm" type="button" @onclick="() => isOpen = false">Annulla</button>
<button class="btn btn-primary btn-sm" type="button" @onclick="() => isOpen = false">Conferma</button>
</FooterContent>
</BitModal>

@code {
private bool isOpen = false;
}
```

### Modal without header

```razor
<BitModal @bind-IsVisible="isOpen"
AriaLabel="Finestra di dialogo"
ShowCloseButton="false">
<BodyContent>
<p>Questa modale non ha intestazione.</p>
</BodyContent>
<FooterContent>
<button class="btn btn-primary btn-sm" @onclick="() => isOpen = false">Chiudi</button>
</FooterContent>
</BitModal>
```

### Centered modal with scrollable content

```razor
<BitModal @bind-IsVisible="isOpen"
Title="Contenuto lungo"
Position="ModalPosition.Centered"
ScrollableContent="true"
FooterShadow="true">
<BodyContent>
@* Long content that scrolls inside the modal body *@
@for (int i = 1; i <= 20; i++)
{
<p>Paragrafo @i lorem ipsum dolor sit amet.</p>
}
</BodyContent>
<FooterContent>
<button class="btn btn-primary btn-sm" @onclick="() => isOpen = false">OK</button>
</FooterContent>
</BitModal>
```

### Static backdrop (does not close on click)

```razor
<BitModal @bind-IsVisible="isOpen"
Title="Conferma obbligatoria"
Backdrop="ModalBackdrop.Static"
ShowCloseButton="false">
<BodyContent>
<p>Devi fare una scelta per continuare.</p>
</BodyContent>
<FooterContent>
<button class="btn btn-outline-primary btn-sm" @onclick="() => isOpen = false">Annulla</button>
<button class="btn btn-primary btn-sm" @onclick="ConfirmAsync">Conferma</button>
</FooterContent>
</BitModal>
```

### Alert modal with icon

```razor
<BitModal @bind-IsVisible="isOpen"
Type="ModalType.Alert"
AriaLabel="Avviso importante">
<HeaderContent>
<div class="d-flex align-items-center">
<BitIcon IconName="@Icons.ItWarningCircle" CssClass="me-2" />
<h2 class="modal-title h5 mb-0">Avviso importante</h2>
</div>
</HeaderContent>
<BodyContent>
<p>Il documento verrà eliminato definitivamente.</p>
</BodyContent>
<FooterContent>
<button class="btn btn-outline-primary btn-sm" @onclick="() => isOpen = false">Annulla</button>
<button class="btn btn-primary btn-sm" @onclick="DeleteAsync">Elimina</button>
</FooterContent>
</BitModal>
```

### Popconfirm modal

```razor
<BitModal @bind-IsVisible="isOpen"
Type="ModalType.Popconfirm"
AriaLabel="Sei sicuro?"
ShowCloseButton="false">
<BodyContent>
<p>Questa operazione non può essere annullata.</p>
</BodyContent>
<FooterContent>
<button class="btn btn-outline-primary btn-sm" @onclick="() => isOpen = false">No</button>
<button class="btn btn-primary btn-sm" @onclick="ProceedAsync">Sì, procedi</button>
</FooterContent>
</BitModal>
```

## Accessibility

- The modal element always carries `role="dialog"` and `aria-modal="true"`.
- The dialog element carries `role="document"`.
- When `Title` is set (and `HeaderContent` is not overriding the header), `aria-labelledby` points automatically to the title `h2` element.
- When there is no `Title`, set `AriaLabel` to provide an accessible name for screen readers.
- Use `AriaDescribedById` to reference a descriptive paragraph inside the `BodyContent` for a richer screen-reader experience.
- The close button always has an `aria-label` (defaults to `"Chiudi"`). Customise it with `CloseButtonAriaLabel`.

## References

- [Bootstrap Italia — Finestre modali](https://italia.github.io/bootstrap-italia/docs/componenti/modale/)
- [WAI-ARIA Authoring Practices — Dialog Modal](https://www.w3.org/TR/wai-aria-practices/#dialog_modal)
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
@page "/comunicazioni"
@rendermode InteractiveAuto

@using BitBlazor.Components
@using BitBlazor.Utilities

<PageTitle>Comunicazioni — Comune di Bitopoli</PageTitle>

<BitBreadcrumb Items="@breadcrumbItems" />

<div class="mt-3 mb-4">
<h1 class="h3 fw-bold mb-1">Comunicazioni e Avvisi</h1>
<p class="text-muted mb-0">
Tutte le comunicazioni istituzionali, avvisi urgenti e notifiche del Comune di Bitopoli.
</p>
</div>

<!-- Filtri per categoria -->
<div class="d-flex flex-wrap gap-2 mb-4 align-items-center">
<span class="small fw-semibold text-muted me-1">Filtra per:</span>
<BitBadge Text="@($"Tutti ({visibleCount})")" BackgroundColor="Color.Primary" Rounded="true" />
<BitBadge Text="Urgenti" BackgroundColor="Color.Danger" Rounded="true" />
<BitBadge Text="Informativi" BackgroundColor="Color.Primary" Rounded="true" />
<BitBadge Text="Successo" BackgroundColor="Color.Success" Rounded="true" />
</div>

<!-- Notifiche interattive (dismissibili) -->
<div class="d-flex flex-column gap-3 mb-5">

@foreach (var item in visibleItems)
{
<BitCard Bordered="true" Shadow="CardShadow.Small">
<CardBody>
<div class="d-flex justify-content-between align-items-start gap-3 mb-2">
<div class="d-flex align-items-center gap-2">
<BitBadge Text="@item.Category" BackgroundColor="@item.BadgeColor" Rounded="true" />
<span class="text-muted small">@item.Date.ToString("dd MMMM yyyy")</span>
</div>
<BitAvatar Text="@item.AvatarText" Size="Size.Small" BackgroundColor="@item.AvatarColor" />
</div>
<BitAlert Type="@item.AlertType" Title="@item.Title" Dismissible="true"
CloseButtonAriaLabel="Chiudi avviso"
OnClosed="@(() => DismissItem(item.Id))">
<p class="mb-0">@item.Body</p>
</BitAlert>
</CardBody>
</BitCard>
}

@if (!visibleItems.Any())
{
<BitCard Bordered="true" Shadow="CardShadow.Small">
<CardBody>
<CardText>
<div class="text-center py-4 text-muted">
<BitIcon IconName="@Icons.ItCheckCircle" Size="IconSize.ExtraLarge" Color="IconColor.Success" />
<p class="mt-3 mb-0">Nessuna comunicazione da visualizzare.<br />Hai chiuso tutti gli avvisi attivi.</p>
</div>
</CardText>
</CardBody>
</BitCard>
}
</div>

@if (dismissed.Any())
{
<div class="d-flex align-items-center gap-3 mb-4">
<span class="text-muted small">@dismissed.Count avvisi chiusi</span>
<BitButton Color="Color.Secondary" Variant="Variant.Outline" Size="Size.Small" OnClick="RestoreAll">
Ripristina tutti
</BitButton>
</div>
}

<!-- Storico comunicazioni (statiche) -->
<h2 class="h5 fw-bold mb-3">Archivio comunicazioni</h2>
<div class="row g-3">
@foreach (var arch in archive)
{
<div class="col-12 col-md-6">
<BitCard Bordered="true" Shadow="CardShadow.Small" FullHeight="true">
<CardBody>
<div class="d-flex justify-content-between align-items-start mb-2">
<BitBadge Text="@arch.Category" BackgroundColor="Color.Secondary" Rounded="true" />
<span class="text-muted small">@arch.Date.ToString("dd/MM/yyyy")</span>
</div>
<CardTitle>@arch.Title</CardTitle>
<CardText>
<p class="text-muted small mb-2">@arch.Summary</p>
</CardText>
<CardFooter>
<BitButton Color="Color.Primary" Variant="Variant.Outline" Size="Size.Small">
Leggi tutto
</BitButton>
</CardFooter>
</CardBody>
</BitCard>
</div>
}
</div>

@code {
private List<int> dismissed = new();

private int visibleCount => visibleItems.Count;

private List<ComunicazioneItem> allItems = new()
{
new(1, "Urgente", AlertType.Danger, "Allerta meteo — Codice Arancione",
"La Protezione Civile ha emesso un'allerta meteo arancione per il comune di Bitopoli valida dalle 18:00 di oggi alle 06:00 di domani. Si raccomanda la massima prudenza.",
new DateOnly(2026, 3, 14), "PC", Color.Danger, Color.Danger),
new(2, "Lavori", AlertType.Warning, "Interruzione acqua in Via Mazzini",
"Giovedì 19 marzo dalle 08:00 alle 16:00 sarà sospesa l'erogazione idrica in Via Mazzini, Via Garibaldi e vie limitrofe per interventi di manutenzione straordinaria.",
new DateOnly(2026, 3, 13), "AM", Color.Warning, Color.Warning),
new(3, "Informativo", AlertType.Info, "Apertura sportello PagoPA",
"A partire dal 17 marzo 2026 sarà attivo il nuovo sportello PagoPA per il pagamento online di tutti i tributi e le sanzioni comunali, incluse IMU, TARI e infrazioni stradali.",
new DateOnly(2026, 3, 12), "PA", Color.Primary, Color.Primary),
new(4, "Avviso", AlertType.Info, "Consultazione pubblica PNRR",
"È aperta fino al 31 marzo 2026 la consultazione pubblica sul Piano Nazionale di Ripresa e Resilienza — progetti del Comune di Bitopoli. Tutti i cittadini possono partecipare.",
new DateOnly(2026, 3, 10), "PN", Color.Secondary, Color.Secondary),
new(5, "Servizi", AlertType.Success, "Nuovo CIE Point operativo",
"Da oggi è operativo il nuovo punto di rilascio CIE (Carta d'Identità Elettronica) presso la sede municipale. Tempi di attesa ridotti e prenotazione online disponibile.",
new DateOnly(2026, 3, 8), "CI", Color.Success, Color.Success),
};

private List<ComunicazioneItem> visibleItems =>
allItems.Where(i => !dismissed.Contains(i.Id)).ToList();

private List<ArchivioItem> archive = new()
{
new("Notizie", "Approvato il bilancio preventivo 2026",
"Il Consiglio Comunale ha approvato il bilancio preventivo per l'anno 2026 con 14 voti favorevoli.",
new DateOnly(2026, 2, 28)),
new("Turismo", "Calendario eventi primaverili 2026",
"Pubblicato il programma completo degli eventi culturali e turistici del Comune per la stagione primaverile.",
new DateOnly(2026, 2, 20)),
new("Sociale", "Bando contributi affitto 2026",
"Aperto il bando per la concessione di contributi a fondo perduto per il sostegno al pagamento dei canoni di locazione.",
new DateOnly(2026, 2, 15)),
new("Ambiente", "Piano rifiuti: nuovi orari raccolta",
"A partire dal 1° marzo 2026 variano gli orari e i giorni della raccolta differenziata porta a porta. Consulta il nuovo calendario.",
new DateOnly(2026, 2, 10)),
};

private void DismissItem(int id) => dismissed.Add(id);

private void RestoreAll() => dismissed.Clear();

private IReadOnlyList<BitBreadcrumbItem> breadcrumbItems = new List<BitBreadcrumbItem>
{
new BitBreadcrumbItem { Text = "Home", Link = "/" },
new BitBreadcrumbItem { Text = "Comunicazioni" }
};

private record ComunicazioneItem(
int Id, string Category, AlertType AlertType, string Title, string Body,
DateOnly Date, string AvatarText, Color AvatarColor, Color BadgeColor);

private record ArchivioItem(string Category, string Title, string Summary, DateOnly Date);
}
Loading
Loading