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
+134
-23
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>
|
<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)
|
||||||
|
|||||||
@@ -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
|
@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>;
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user