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:
1 parent
765220059d
commit
89cef25def
8 files changed
+135
-24
No files matched your search
@@ -325,6 +325,10 @@
|
||||
<i aria-hidden="true" class="ion-md-bookmark text-base @(status.Bookmarked ? "text-warning" : null)"></i>
|
||||
<span>@Localizer["Bookmark"]</span>
|
||||
</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)
|
||||
{
|
||||
<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"]">
|
||||
<Composer Editing="status" OnPublished="Edited" />
|
||||
</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)
|
||||
{
|
||||
<ReportDialog Account="reported" Post="status" OnClosed="@(() => isReporting = false)" />
|
||||
@@ -497,6 +524,7 @@
|
||||
[Parameter] public FilterContext? FilterIn { get; set; }
|
||||
|
||||
bool isReplying;
|
||||
bool isDetailsOpen;
|
||||
bool isQuoting;
|
||||
bool isEditing;
|
||||
bool isActing;
|
||||
@@ -581,7 +609,7 @@
|
||||
<div class="grid gap-2 @(status.MediaAttachments.Count > 1 ? "grid-cols-2" : "grid-cols-1")">
|
||||
@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))
|
||||
{
|
||||
<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);
|
||||
}
|
||||
|
||||
async Task OpenDetails()
|
||||
{
|
||||
isDetailsOpen = true;
|
||||
await Task.WhenAll(ReadProvenance(), ReadServer());
|
||||
}
|
||||
|
||||
async Task ReadProvenance()
|
||||
{
|
||||
if (Status is null || Post.Located)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,10 +1,32 @@
|
||||
@inherits LocalizableComponentBase
|
||||
|
||||
@* Nerd stats on hover or focus: a DTooltip whose content is a monospace list of label/value rows. OnOpen runs on the
|
||||
first hover or focus, for stats read on demand (a post's provenance); rows with no value are left out. *@
|
||||
<DTooltip Placement="Placement" Align="Align" class="@Class" TipContent="Body" @onmouseenter="Open" @onfocusin="Open">
|
||||
@ChildContent
|
||||
</DTooltip>
|
||||
@* Nerd stats on hover or focus: a DTooltip whose content is the rows (NerdRows). A touch screen has no hover, so there
|
||||
a tap opens the same rows in a sheet from the bottom (TapOpens; not for a trigger that is a link itself, the
|
||||
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
|
||||
</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 {
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
@@ -16,8 +38,19 @@
|
||||
[Parameter] public DTooltipPlacement Placement { get; set; }
|
||||
[Parameter] public DTooltipAlign Align { 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 isSheetOpen;
|
||||
|
||||
async Task Tap()
|
||||
{
|
||||
if (CascadingState.Status.CanHover())
|
||||
return;
|
||||
isSheetOpen = true;
|
||||
await Open();
|
||||
}
|
||||
|
||||
protected override void OnInitialized() => IsLoading = false;
|
||||
|
||||
@@ -29,21 +62,5 @@
|
||||
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">
|
||||
@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>;
|
||||
RenderFragment Body => @<NerdRows Heading="@Heading" Rows="Rows" Pending="Pending" />;
|
||||
}
|
||||
@@ -1856,4 +1856,7 @@
|
||||
<data name="Feeds" xml:space="preserve">
|
||||
<value>Feed</value>
|
||||
</data>
|
||||
<data name="Details" xml:space="preserve">
|
||||
<value>Dettagli</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1856,4 +1856,7 @@
|
||||
<data name="Feeds" xml:space="preserve">
|
||||
<value>Feeds</value>
|
||||
</data>
|
||||
<data name="Details" xml:space="preserve">
|
||||
<value>Details</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -42,6 +42,9 @@ public class AppStatusService(IJSRuntime jsRuntime)
|
||||
|
||||
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
|
||||
public void RevealActive(ElementReference strip) => JsSyncRuntime.InvokeVoid("revealActive", strip);
|
||||
|
||||
|
||||
+13
-1
@@ -62,13 +62,25 @@
|
||||
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) {
|
||||
border-radius: var(--radius-box);
|
||||
padding: 0.5rem 0.65rem;
|
||||
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 {
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
|
||||
@@ -123,6 +123,15 @@ window.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
|
||||
window.revealActive = strip => {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user