Nerd stats open with a tap

On a touch screen a nerd-stats tooltip showed only through the browser faking hover, unreliably, and couldn't be
scrolled. Where the pointer can't hover (AppStatusService.CanHover), a tap on a NerdTip now opens its rows in a sheet
from the bottom; the trigger is focusable, says it opens a dialog, and is at least 44px on touch. A closed tooltip takes
no room any more (daisyUI only made it transparent, and one past a card's edge made a phone's feed scroll sideways),
and on touch it never shows. The rows are a component of their own (NerdRows), at 12px on phones. A card's More menu
has a Details entry on phones that gathers the post's, its author's, its place's or CDN's and its counts' rows in one
sheet. A picture's tip doesn't open on a tap, the picture does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 23:36:03 +02:00
1 parent 765220059d
commit 89cef25def
8 files changed
+134 -23

No files matched your search

+35 -1
View File
@@ -325,6 +325,10 @@
<i aria-hidden="true" class="ion-md-bookmark text-base @(status.Bookmarked ? "text-warning" : null)"></i> <i aria-hidden="true" class="ion-md-bookmark text-base @(status.Bookmarked ? "text-warning" : null)"></i>
<span>@Localizer["Bookmark"]</span> <span>@Localizer["Bookmark"]</span>
</DButton> </DButton>
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap md:hidden" OnClick="OpenDetails" data-details>
<i aria-hidden="true" class="ion-md-stats text-base"></i>
<span>@Localizer["Details"]</span>
</DButton>
@if (status.Url is { Length: > 0 } url) @if (status.Url is { Length: > 0 } url)
{ {
<a class="btn btn-sm justify-start whitespace-nowrap" href="@url" target="_blank" rel="noopener noreferrer"> <a class="btn btn-sm justify-start whitespace-nowrap" href="@url" target="_blank" rel="noopener noreferrer">
@@ -416,6 +420,29 @@
<PhoneSheet IsOpen="isEditing" OnClose="() => isEditing = false" Heading="@Localizer["Edit"]"> <PhoneSheet IsOpen="isEditing" OnClose="() => isEditing = false" Heading="@Localizer["Edit"]">
<Composer Editing="status" OnPublished="Edited" /> <Composer Editing="status" OnPublished="Edited" />
</PhoneSheet> </PhoneSheet>
@* a phone's way to the nerd stats the card's corners hold on hover, all in one sheet *@
@if (isDetailsOpen)
{
<DModal Placement="DModalPlacement.ResponsiveBottom" IsOpen="true" IsOpenChanged="open => isDetailsOpen = open" CloseText="@Localizer["Close"]"
data-details-sheet>
<Heading>@Localizer["Details"]</Heading>
<ChildContent>
<div class="flex flex-col gap-4">
<NerdRows Heading="@Localizer["Post"]" Rows="PostRows()" Pending="isReadingProvenance" />
<NerdRows Heading="@Localizer["Author"]" Rows="AuthorRows()" Pending="isReadingServer" />
@if (Post.Location is { Source: LocationSource.Cdn } detailsCdn)
{
<NerdRows Heading="@Localizer["The server is behind {0}, so its place is unknown.", detailsCdn.Label]" Rows="CdnRows()" Pending="isReadingServer" />
}
else if (Post.Location is not null)
{
<NerdRows Heading="@Localizer["Place"]" Rows="LocationRows()" />
}
<NerdRows Heading="@Localizer["Counts"]" Rows="CountRows(status)" />
</div>
</ChildContent>
</DModal>
}
@if (isReporting && status.Account is { } reported) @if (isReporting && status.Account is { } reported)
{ {
<ReportDialog Account="reported" Post="status" OnClosed="@(() => isReporting = false)" /> <ReportDialog Account="reported" Post="status" OnClosed="@(() => isReporting = false)" />
@@ -497,6 +524,7 @@
[Parameter] public FilterContext? FilterIn { get; set; } [Parameter] public FilterContext? FilterIn { get; set; }
bool isReplying; bool isReplying;
bool isDetailsOpen;
bool isQuoting; bool isQuoting;
bool isEditing; bool isEditing;
bool isActing; bool isActing;
@@ -581,7 +609,7 @@
<div class="grid gap-2 @(status.MediaAttachments.Count > 1 ? "grid-cols-2" : "grid-cols-1")"> <div class="grid gap-2 @(status.MediaAttachments.Count > 1 ? "grid-cols-2" : "grid-cols-1")">
@foreach (var media in status.MediaAttachments) @foreach (var media in status.MediaAttachments)
{ {
<NerdTip Heading="@Localizer["Media"]" Rows="MediaRows(media)" Class="block w-full"> <NerdTip Heading="@Localizer["Media"]" Rows="MediaRows(media)" Class="block w-full" TapOpens="false">
@if ((status.Sensitive || IsBlurred) && !revealedMedia.Contains(media.Id)) @if ((status.Sensitive || IsBlurred) && !revealedMedia.Contains(media.Id))
{ {
<button type="button" class="flex items-center justify-center w-full aspect-video rounded-lg neo-inset neo-2xs text-sm" <button type="button" class="flex items-center justify-center w-full aspect-video rounded-lg neo-inset neo-2xs text-sm"
@@ -1008,6 +1036,12 @@
await OnReplied.InvokeAsync(reply); await OnReplied.InvokeAsync(reply);
} }
async Task OpenDetails()
{
isDetailsOpen = true;
await Task.WhenAll(ReadProvenance(), ReadServer());
}
async Task ReadProvenance() async Task ReadProvenance()
{ {
if (Status is null || Post.Located) if (Status is null || Post.Located)
+30
View File
@@ -0,0 +1,30 @@
@inherits LocalizableComponentBase
@* A block of nerd stats: a monospace list of label/value rows under its heading, rows with no value left out; in a
NerdTip's tooltip, in the sheet a tap opens on a touch screen, and in a card's Details on a phone. *@
<div class="nerd-stats flex flex-col gap-1 text-left font-mono text-[11px] max-md:text-xs leading-tight max-w-80">
@if (Heading is { Length: > 0 })
{
<p class="font-bold opacity-80">@Heading</p>
}
<dl class="grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5">
@foreach (var row in Rows.Where(row => !string.IsNullOrEmpty(row.Value)))
{
<dt class="opacity-60 whitespace-nowrap">@row.Label</dt>
<dd class="break-all">@row.Value</dd>
}
</dl>
@if (Pending)
{
<p class="opacity-60">@Localizer["loading"]…</p>
}
</div>
@code {
[Parameter] public string Heading { get; set; }
[Parameter] public IEnumerable<NerdRow> Rows { get; set; } = [];
/// <summary>Shown under the rows while the stats being read on demand arrive.</summary>
[Parameter] public bool Pending { get; set; }
protected override void OnInitialized() => IsLoading = false;
}
+38 -21
View File
@@ -1,10 +1,32 @@
@inherits LocalizableComponentBase @inherits LocalizableComponentBase
@* Nerd stats on hover or focus: a DTooltip whose content is a monospace list of label/value rows. OnOpen runs on the @* Nerd stats on hover or focus: a DTooltip whose content is the rows (NerdRows). A touch screen has no hover, so there
first hover or focus, for stats read on demand (a post's provenance); rows with no value are left out. *@ a tap opens the same rows in a sheet from the bottom (TapOpens; not for a trigger that is a link itself, the
<DTooltip Placement="Placement" Align="Align" class="@Class" TipContent="Body" @onmouseenter="Open" @onfocusin="Open"> pictures). OnOpen runs on the first hover, focus or tap, for stats read on demand (a post's provenance). *@
@if (TapOpens)
{
@* (on touch the trigger is at least 44px: daisyUI's tooltip draws its arrow with ::after, so padding-free min sizes) *@
<DTooltip Placement="Placement" Align="Align" class="@($"{Class} pointer-coarse:inline-flex pointer-coarse:items-center pointer-coarse:min-h-11 pointer-coarse:min-w-11")"
TipContent="Body" @onmouseenter="Open" @onfocusin="Open" @onclick="Tap"
tabindex="0" role="button" aria-haspopup="dialog" aria-label="@Heading">
@ChildContent @ChildContent
</DTooltip> </DTooltip>
}
else
{
<DTooltip Placement="Placement" Align="Align" class="@Class" TipContent="Body" @onmouseenter="Open" @onfocusin="Open">
@ChildContent
</DTooltip>
}
@if (isSheetOpen)
{
<DModal Placement="DModalPlacement.ResponsiveBottom" IsOpen="true" IsOpenChanged="open => isSheetOpen = open" CloseText="@Localizer["Close"]" data-nerd-sheet>
<Heading>@Heading</Heading>
<ChildContent>
<NerdRows Rows="Rows" Pending="Pending" />
</ChildContent>
</DModal>
}
@code { @code {
[Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public RenderFragment ChildContent { get; set; }
@@ -16,8 +38,19 @@
[Parameter] public DTooltipPlacement Placement { get; set; } [Parameter] public DTooltipPlacement Placement { get; set; }
[Parameter] public DTooltipAlign Align { get; set; } [Parameter] public DTooltipAlign Align { get; set; }
[Parameter] public string Class { get; set; } [Parameter] public string Class { get; set; }
/// <summary>A tap opens the rows in a sheet where the screen has no hover.</summary>
[Parameter] public bool TapOpens { get; set; } = true;
bool opened; bool opened;
bool isSheetOpen;
async Task Tap()
{
if (CascadingState.Status.CanHover())
return;
isSheetOpen = true;
await Open();
}
protected override void OnInitialized() => IsLoading = false; protected override void OnInitialized() => IsLoading = false;
@@ -29,21 +62,5 @@
await OnOpen.InvokeAsync(); await OnOpen.InvokeAsync();
} }
RenderFragment Body => @<div class="nerd-stats flex flex-col gap-1 text-left font-mono text-[11px] leading-tight max-w-80"> RenderFragment Body => @<NerdRows Heading="@Heading" Rows="Rows" Pending="Pending" />;
@if (Heading is { Length: > 0 })
{
<p class="font-bold opacity-80">@Heading</p>
}
<dl class="grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5">
@foreach (var row in Rows.Where(row => !string.IsNullOrEmpty(row.Value)))
{
<dt class="opacity-60 whitespace-nowrap">@row.Label</dt>
<dd class="break-all">@row.Value</dd>
}
</dl>
@if (Pending)
{
<p class="opacity-60">@Localizer["loading"]…</p>
}
</div>;
} }
+3
View File
@@ -1856,4 +1856,7 @@
<data name="Feeds" xml:space="preserve"> <data name="Feeds" xml:space="preserve">
<value>Feed</value> <value>Feed</value>
</data> </data>
<data name="Details" xml:space="preserve">
<value>Dettagli</value>
</data>
</root> </root>
+3
View File
@@ -1856,4 +1856,7 @@
<data name="Feeds" xml:space="preserve"> <data name="Feeds" xml:space="preserve">
<value>Feeds</value> <value>Feeds</value>
</data> </data>
<data name="Details" xml:space="preserve">
<value>Details</value>
</data>
</root> </root>
+3
View File
@@ -42,6 +42,9 @@ public class AppStatusService(IJSRuntime jsRuntime)
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia"); public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
// whether the pointer can hover (a mouse); a touch screen can't, so what shows on hover there opens on a tap
public bool CanHover() => JsSyncRuntime.Invoke<bool>("canHover");
// a sideways-scrolling strip of tabs shows its selected tab // a sideways-scrolling strip of tabs shows its selected tab
public void RevealActive(ElementReference strip) => JsSyncRuntime.InvokeVoid("revealActive", strip); public void RevealActive(ElementReference strip) => JsSyncRuntime.InvokeVoid("revealActive", strip);
+13 -1
View File
@@ -62,13 +62,25 @@
padding-inline-start: 1.25rem; padding-inline-start: 1.25rem;
} }
/* daisyUI's tooltip is a pill; a block of nerd stats needs the corners of a box. */ /* daisyUI's tooltip is a pill; a block of nerd stats needs the corners of a box. Hidden, it takes no room (daisyUI
only makes it transparent, and one past a card's edge made a phone's feed scroll sideways), and on a touch screen
it never shows: a tap opens the rows in a sheet instead (NerdTip). */
.tooltip-content:has(> .nerd-stats) { .tooltip-content:has(> .nerd-stats) {
border-radius: var(--radius-box); border-radius: var(--radius-box);
padding: 0.5rem 0.65rem; padding: 0.5rem 0.65rem;
max-width: min(22rem, 90vw); max-width: min(22rem, 90vw);
} }
.tooltip:not(:hover, :focus-within, .tooltip-open) > .tooltip-content:has(> .nerd-stats) {
display: none;
}
@media (hover: none) {
.tooltip-content:has(> .nerd-stats) {
display: none;
}
}
.float-tooltip-kap { .float-tooltip-kap {
padding: 0 !important; padding: 0 !important;
background: transparent !important; background: transparent !important;
+9
View File
@@ -123,6 +123,15 @@ window.clearAppBadge = () => {
window.logFromJs(e.message, "clearAppBadge") window.logFromJs(e.message, "clearAppBadge")
} }
} }
window.canHover = () => {
try {
return window.matchMedia("(hover: hover)").matches
} catch (e) {
console.error(e)
window.logFromJs(e.message, "canHover")
return true
}
}
// the selected tab of a sideways-scrolling strip, brought into its middle // the selected tab of a sideways-scrolling strip, brought into its middle
window.revealActive = strip => { window.revealActive = strip => {
try { try {