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
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ concurrency:

permissions:
contents: read
pull-requests: write

jobs:
markup-lint:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pkg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: [main]
workflow_dispatch: {}

permissions:
contents: read

jobs:
git-check:
name: Git
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<PropertyGroup>
<!-- Tip: Edit this value to change the current MAJOR.MINOR.PATCH version -->
<VersionPrefix>2.4.2</VersionPrefix>
<VersionPrefix>2.4.3</VersionPrefix>
</PropertyGroup>

<Choose>
Expand Down
80 changes: 40 additions & 40 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AwesomeAssertions" Version="9.6.0" />
<PackageVersion Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageVersion Include="Bogus" Version="35.6.5" />
<PackageVersion Include="ClosedXML" Version="0.105.1" />
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<PackageVersion Include="FirebaseAdmin" Version="3.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.13" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.11.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.13" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="2.13.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.63.0" />
<PackageVersion Include="Microsoft.Playwright.Xunit.v3" Version="1.63.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.12.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.5.1" />
<PackageVersion Include="MongoDB.Bson" Version="3.12.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.12.0" />
<PackageVersion Include="Moq" Version="4.21.0" />
<PackageVersion Include="Riok.Mapperly" Version="4.3.1" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.18.0" />
<PackageVersion Include="SharpCompress" Version="0.50.6" />
<PackageVersion Include="System.CodeDom" Version="10.0.13" />
<PackageVersion Include="System.Management" Version="10.0.13" />
<PackageVersion Include="SystemTextJson.JsonDiffPatch" Version="2.0.0" />
<PackageVersion Include="Withywoods.AspNetCore.Mvc.Testing" Version="2.0.5" />
<PackageVersion Include="Withywoods.Configuration" Version="2.0.5" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="4.0.2" />
</ItemGroup>
</Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AwesomeAssertions" Version="9.6.0" />
<PackageVersion Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageVersion Include="Bogus" Version="35.6.5" />
<PackageVersion Include="ClosedXML" Version="0.105.1" />
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<PackageVersion Include="FirebaseAdmin" Version="3.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.14" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.14" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.14" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.11.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.14" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="2.13.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.64.1" />
<PackageVersion Include="Microsoft.Playwright.Xunit.v3" Version="1.64.1" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.12.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.5.1" />
<PackageVersion Include="MongoDB.Bson" Version="3.13.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.13.0" />
<PackageVersion Include="Moq" Version="4.21.0" />
<PackageVersion Include="Riok.Mapperly" Version="4.3.1" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.18.3" />
<PackageVersion Include="SharpCompress" Version="0.50.6" />
<PackageVersion Include="System.CodeDom" Version="10.0.14" />
<PackageVersion Include="System.Management" Version="10.0.14" />
<PackageVersion Include="SystemTextJson.JsonDiffPatch" Version="2.0.0" />
<PackageVersion Include="Withywoods.AspNetCore.Mvc.Testing" Version="2.0.5" />
<PackageVersion Include="Withywoods.Configuration" Version="2.0.5" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="4.0.2" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions docs/blazor-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ A missing item is a 404 at every layer:
- **Enter takes the highlight, Tab only one reached with the arrow keys**, since completing on Tab turns "Dr Kim" into "Dr Kimura".
Escape drops the highlight, and there is no `preventDefault` on keydown.

**`DeleteItemControl`** deletes a media item from its detail page, with the same confirmation as a list row.
It replaces the history entry with the list, so browser back skips the deleted item.
The admin unlink beside it uses the broken-chain icon, since the trash means deleting the item.

## Razor gotchas

- **A `string`-typed component `[Parameter]` needs the `@` prefix.**
Expand All @@ -100,6 +104,14 @@ Sonar reports every `!important` (`css:S9423`), and an `!important` theme also s

`wwwroot/Keeptrack.BlazorApp.lib.module.js` is loaded automatically by name (never a manual `<script>` tag), and is where `blazor.addEventListener('enhancedload', ...)` re-applies client-side DOM state.

### Responsive widths

Phone below 768px, laptop up to 1400px, and the large-screen block last in `app.css` above it, so its overrides win the cascade.
From 1400px, the content column and the detail measures widen.
From 1600px, list rows go two per line instead of stretching.
From 1920px, the root font size grows.
The px measures (covers, grid columns, thumbnails) are widened alongside it, since rem does not reach them.

### Icons

**Icons are Unicode with text presentation** (`◈ ✓ ✕ ★ ▶ ↻ ⌂ ⚙ ♪ ◼ ▭ ▬ ◆`), never a codepoint with `Emoji_Presentation=Yes` (`⭐`, `👁`), never with U+FE0F.
Expand Down
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Gherkin was rejected as twice the code for the same coverage.
- **`ExploreSmokeTest` seeds `explore_catalogue` directly** (`End2EndFixture.SeedExploreCatalogueAsync`, self-hosted only), since the e2e host never runs the refresh.
It seeds every ranking `ExploreRankings.Rankings` declares for a domain (the one read follows a stored admin setting), uses real provider ids only for the two `Add` cases (TMDB 278, IGDB 72),
and its `[Theory]`s cover movies and video games, which differ in tab gating, ranking and id space.
- `MobileScreenshotTest` is an assertion-free visual harness behind `E2E_MOBILE_CHECK=true`, capturing every page at a phone viewport for review.
- `MobileScreenshotTest` is an assertion-free visual harness behind `E2E_MOBILE_CHECK=true`, capturing every page at a phone viewport, then a sample at 1280px and at 2560px, for review.

**`PageBase.WaitForReadyAsync` reloads once and re-asserts** (`ExpectWithReloadAsync`, also behind `ListPage.ExpectRowThumbnailAsync`), for two cases where waiting never helps:

Expand Down
1 change: 1 addition & 0 deletions src/BlazorApp/Components/Inventory/Pages/AlbumDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ else
<ReferenceMatchControls TDto="AlbumDto" Api="Api" ItemId="@(Album.Id!)" ReferenceId="@Album.ReferenceId" ReferenceTitle="@Reference?.Title"
ReferenceDataDescription="cover, tracklist, artist"
OnChecked="OnReferenceCheckedAsync" OnUnlinked="LoadAsync"/>
<DeleteItemControl TDto="AlbumDto" Api="Api" ItemId="@(Album.Id!)" ItemName="album" ListRoute="/albums"/>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm kt-toggle-btn @(Album.IsFavorite ? "active" : "")" @onclick="ToggleFavoriteAsync">Favorites</button>
Expand Down
1 change: 1 addition & 0 deletions src/BlazorApp/Components/Inventory/Pages/BookDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ else
<ReferenceMatchControls TDto="BookDto" Api="Api" ItemId="@(Book.Id!)" ReferenceId="@Book.ReferenceId" ReferenceTitle="@Reference?.Title"
ReferenceDataDescription="cover, synopsis, genre/language/ISBN autofill"
OnChecked="OnReferenceCheckedAsync" OnUnlinked="LoadAsync"/>
<DeleteItemControl TDto="BookDto" Api="Api" ItemId="@(Book.Id!)" ItemName="book" ListRoute="/books"/>
</div>
@* "Read" is not here but on the card below as a .kt-corner-flag (see MovieDetail). *@
<div class="d-flex gap-2 flex-wrap">
Expand Down
1 change: 1 addition & 0 deletions src/BlazorApp/Components/Inventory/Pages/MovieDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ else
<ReferenceMatchControls TDto="MovieDto" Api="Api" ItemId="@(Movie.Id!)" ReferenceId="@Movie.ReferenceId" ReferenceTitle="@Reference?.Title"
ReferenceDataDescription="cover, synopsis, cast/genres"
OnChecked="OnReferenceCheckedAsync" OnUnlinked="LoadAsync"/>
<DeleteItemControl TDto="MovieDto" Api="Api" ItemId="@(Movie.Id!)" ItemName="movie" ListRoute="/movies"/>
</div>
@* "Watched" is not here but on the card below as a .kt-corner-flag, since it outranks these three flags,
and its success green says "finished" where their accent blue says "flagged". *@
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<ReferenceMatchControls TDto="TvShowDto" Api="Api" ItemId="@(_show.Id!)" ReferenceId="@_show.ReferenceId" ReferenceTitle="@_reference?.Title"
ReferenceDataDescription="cover, synopsis, cast/genres, episode guide"
OnChecked="OnReferenceCheckedAsync" OnUnlinked="LoadAsync"/>
<DeleteItemControl TDto="TvShowDto" Api="Api" ItemId="@(_show.Id!)" ItemName="TV show" ListRoute="/tv-shows"/>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm kt-toggle-btn @(_show.IsFavorite ? "active" : "")" @onclick="ToggleFavoriteAsync">Favorites</button>
Expand Down Expand Up @@ -210,9 +211,9 @@

protected override InventoryApiClientBase<TvShowDto> Api => TvShowApi;

protected override TvShowDto? Item { get => _show; set => _show = value; }

Check warning on line 214 in src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Make this an auto-implemented property and remove its backing field.

Check warning on line 214 in src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Make this an auto-implemented property and remove its backing field.

protected override TvShowReferenceDto? ItemReference { get => _reference; set => _reference = value; }

Check warning on line 216 in src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Make this an auto-implemented property and remove its backing field.

Check warning on line 216 in src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Make this an auto-implemented property and remove its backing field.

protected override Task<TvShowReferenceDto?> GetReferenceAsync(string referenceId) => ReferenceDataApi.GetTvShowAsync(referenceId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ else
<ReferenceMatchControls TDto="VideoGameDto" Api="Api" ItemId="@(Game.Id!)" ReferenceId="@Game.ReferenceId" ReferenceTitle="@Reference?.Title"
ReferenceDataDescription="cover, synopsis, genres/platforms"
OnChecked="OnReferenceCheckedAsync" OnUnlinked="LoadAsync"/>
<DeleteItemControl TDto="VideoGameDto" Api="Api" ItemId="@(Game.Id!)" ItemName="video game" ListRoute="/video-games"/>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm kt-toggle-btn @(Game.IsWishlisted ? "active" : "")" @onclick="ToggleWishlistedAsync"><Icon Name="wishlist" Size="13"/> Wishlist</button>
Expand Down
47 changes: 47 additions & 0 deletions src/BlazorApp/Components/Inventory/Shared/DeleteItemControl.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@typeparam TDto where TDto : Keeptrack.Common.System.IHasId
@inject NavigationManager Navigation
@* A detail page's own delete, with the same confirmation as a list row's. *@

@if (_error is not null)
{
<span class="kt-inline-toast danger">@_error</span>
}
<button class="kt-icon-btn" title="Delete this @ItemName" aria-label="Delete" @onclick="() => _pending = true">
<Icon Name="trash" Size="15"/>
</button>
<ConfirmModal Show="_pending"
Title="Delete this item?"
Message="@($"This will permanently delete this {ItemName}. This can't be undone.")"
OnConfirm="DeleteAsync" OnCancel="() => _pending = false"/>

@code {
[Parameter, EditorRequired] public required InventoryApiClientBase<TDto> Api { get; set; }

[Parameter, EditorRequired] public required string ItemId { get; set; }

/// <summary>Lower case, as it reads in the confirmation, for example "movie".</summary>
[Parameter, EditorRequired] public required string ItemName { get; set; }

[Parameter, EditorRequired] public required string ListRoute { get; set; }

private bool _pending;
private string? _error;

private async Task DeleteAsync()
{
_pending = false;
_error = null;
try
{
await Api.DeleteAsync(ItemId);
}
catch (Exception ex)
{
_error = $"Error: {ex.Message}";
return;
}

// Replaces the history entry, so browser back skips the page of an item that no longer exists.
Navigation.NavigateTo(ListRoute, replace: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<Authorized>
<button class="kt-icon-btn" title="Unlink and remove this reference" aria-label="Unlink reference"
@onclick="() => _pendingUnlink = true">
<Icon Name="trash" Size="15"/>
<Icon Name="unlink" Size="15"/>
</button>
</Authorized>
</AuthorizeView>
Expand All @@ -31,7 +31,7 @@
OnConfirm="UnlinkAsync" OnCancel="() => _pendingUnlink = false"/>

@code {
private static readonly TimeSpan MessageDuration = TimeSpan.FromSeconds(4);

Check warning on line 34 in src/BlazorApp/Components/Inventory/Shared/ReferenceMatchControls.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

A static field in a generic type is not shared among instances of different close constructed types.

Check warning on line 34 in src/BlazorApp/Components/Inventory/Shared/ReferenceMatchControls.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

A static field in a generic type is not shared among instances of different close constructed types.

[Parameter, EditorRequired] public required InventoryApiClientBase<TDto> Api { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<div class="kt-page-header">
<h1>Reference data</h1>
<span class="kt-app-version" title="Running build">v@(AppVersion.Current)</span>
</div>

@* Linking unresolved titles is the frequent admin task, so it comes first, and the occasional sync, export and import sit below it. *@
Expand Down Expand Up @@ -233,7 +234,7 @@
so the row always offers a way to ask differently. *@
<div class="d-flex gap-2 align-items-center mb-2" style="max-width:640px">
<input class="form-control form-control-sm" placeholder="@($"Search {_reconciliation.ProviderDisplayName} with different text, or paste the game's page URL")"
@bind="_candidateQuery" @bind:event="oninput" @onkeydown="@(e => { if (e.Key == "Enter") return SearchCandidatesAsync(gap); return Task.CompletedTask; })"/>

Check warning on line 237 in src/BlazorApp/Components/ReferenceDataAdmin/ReferenceDataAdminPage.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

This line will not be executed conditionally; only the first line of this 1-line block will be. The rest will execute unconditionally.

Check warning on line 237 in src/BlazorApp/Components/ReferenceDataAdmin/ReferenceDataAdminPage.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

This line will not be executed conditionally; only the first line of this 1-line block will be. The rest will execute unconditionally.
<button class="btn btn-sm btn-secondary" @onclick="() => SearchCandidatesAsync(gap)" disabled="@(_loadingCandidates || string.IsNullOrWhiteSpace(_candidateQuery))">Search</button>
</div>
@if (_loadingCandidates)
Expand Down Expand Up @@ -675,7 +676,7 @@
// Buffered rather than handed over as a live stream, since an IBrowserFile stream is pulled down the SignalR circuit in small chunks.
// Doing that during the POST would count the whole upload against the request's timeout.
using var buffer = new MemoryStream();
await using (var stream = e.File.OpenReadStream(MaxImportFileSize))

Check warning on line 679 in src/BlazorApp/Components/ReferenceDataAdmin/ReferenceDataAdminPage.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Limit the content length of HTTP requests.

Check warning on line 679 in src/BlazorApp/Components/ReferenceDataAdmin/ReferenceDataAdminPage.razor

View workflow job for this annotation

GitHub Actions / Code / Quality

Limit the content length of HTTP requests.
{
await stream.CopyToAsync(buffer);
}
Expand Down
28 changes: 28 additions & 0 deletions src/BlazorApp/Components/Shared/AppVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Reflection;

namespace Keeptrack.BlazorApp.Components.Shared;

/// <summary>
/// The running build's version, from the informational version that <c>Directory.Build.props</c> and the SDK's source revision produce.
/// </summary>
public static class AppVersion
{
private const int ShortHashLength = 7;

public static string Current { get; } =
Format(typeof(AppVersion).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion);

public static string Format(string? informationalVersion)
{
if (string.IsNullOrEmpty(informationalVersion))
{
return "unknown";
}

// The SDK appends the full 40-character commit hash, which is unreadable in a header and as identifying at git's usual 7.
var plus = informationalVersion.IndexOf('+');
return plus >= 0 && informationalVersion.Length - plus - 1 > ShortHashLength
? informationalVersion[..(plus + 1 + ShortHashLength)]
: informationalVersion;
}
}
13 changes: 11 additions & 2 deletions src/BlazorApp/Components/Shared/Icon.razor
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
break;

case "movies":
<rect x="3" y="4.5" width="18" height="15" rx="2"/>
<path d="M3 9.5h18M3 14.5h18M8 4.5v15M16 4.5v15"/>
<circle cx="6.5" cy="6.5" r="3"/>
<circle cx="13.5" cy="6.5" r="3"/>
<rect x="3" y="10.5" width="13" height="8.5" rx="1.5"/>
<path d="M16 13.4 21 11v7.5l-5-2.4"/>
break;

case "tv-shows":
Expand Down Expand Up @@ -139,6 +141,13 @@
<path d="M10.2 10.5v7M13.8 10.5v7"/>
break;

@* A broken chain, distinct from "trash": it removes the link to the reference, never the item itself. *@
case "unlink":
<path d="M13 6.4 14.4 5a3.8 3.8 0 0 1 5.4 5.4l-1.4 1.4"/>
<path d="M11 17.6 9.6 19a3.8 3.8 0 0 1-5.4-5.4l1.4-1.4"/>
<path d="M8 3v2.6M3 8h2.6M16 21v-2.6M21 16h-2.6"/>
break;

case "external-link":
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<path d="M15 3h6v6"/>
Expand Down
46 changes: 45 additions & 1 deletion src/BlazorApp/wwwroot/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,8 @@ div.content,
/* Inventory rows in list view only, added by InventoryList, never to the grid.
A .kt-item-row clusters its content left and pushes its delete button to the far edge, so across the full 1100px column the title and its delete icon sit ~900px apart.
Capping the measure keeps a row readable as one object.
The grid, the admin queues and the dense car and health journals take the full width. */
The grid, the admin queues and the dense car and health journals take the full width.
On a large screen the cap lifts and the rows go two per line instead (see Large screens). */
.kt-table-wrap-list { max-width: 920px; margin-inline: auto; }

.kt-table-header {
Expand Down Expand Up @@ -699,6 +700,8 @@ a.kt-item-row { color: inherit; text-decoration: none; }
}
.kt-page-header h1 { font-size: 2rem; color: var(--kt-text); margin: 0; line-height: 1; }
.kt-page-header h1 span { color: var(--kt-accent); }
/* The running build on the admin page, so a deployment can be checked from the browser. */
.kt-app-version { color: var(--kt-text-muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; }

/* ── Quick Add ─────────────────────────────────────────────────────── */
.kt-quickadd-back {
Expand Down Expand Up @@ -1872,3 +1875,44 @@ a.kt-item-row { color: inherit; text-decoration: none; }
background: var(--kt-surface);
}
.kt-dropzone-icon { font-size: 2.25rem; display: block; margin-bottom: 1rem; color: var(--kt-text-subtle); }

/* ── Large screens ─────────────────────────────────────────────────── */
/* Last in the file so these overrides win the cascade over every base rule they widen.
The base measures suit a laptop, and on a large monitor they leave the content as a narrow strip between two empty margins.
Text grows with the root font size, while px measures (covers, grid columns) are widened explicitly since rem does not reach them. */
@media (min-width: 1400px) {
article.content, div.content, .content { max-width: 1440px; padding: 2.5rem 3.5rem; }
.kt-search-input-wrap { max-width: 480px; }
.kt-detail-hero { max-width: 1100px; }
.kt-form-card > .row { max-width: 1100px; }
.kt-game-page { max-width: 1100px; }
}

/* Rows go two per line rather than stretching, which keeps each row's title and delete button close together without leaving the sides empty. */
@media (min-width: 1600px) {
.kt-table-wrap-list { max-width: none; }
.kt-item-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kt-item-list > .kt-item-row:nth-child(odd) { border-right: 1px solid var(--kt-border); }
}

@media (min-width: 1920px) {
html, body { font-size: 16px; }
article.content, div.content, .content { max-width: 1800px; padding: 3rem 4rem; }
.sidebar { width: 250px; }
.kt-search-input-wrap { max-width: 560px; }
.kt-sort-select { width: 96px; }
.kt-item-thumb { width: 52px; height: 78px; }
.kt-item-thumb.square { width: 64px; height: 64px; }
.kt-item-thumb.wide { width: 112px; height: 63px; }
.kt-item-grid { grid-template-columns: repeat(auto-fill, minmax(175px, 1fr)); gap: 1.5rem 1.25rem; }
.kt-item-grid.square { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.kt-item-grid.wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.kt-detail-hero { max-width: 1300px; --kt-hero-cover-width: 280px; }
.kt-detail-hero.square { --kt-hero-cover-width: 320px; }
.kt-form-card > .row { max-width: 1300px; }
.kt-game-page { max-width: 1300px; }
}

@media (min-width: 2400px) {
html, body { font-size: 17px; }
}
Loading
Loading