feat(dashboard): move This Week stats into the library bar as a popover
Release / build-and-push (push) Successful in 3m11s
Release / build-and-push (push) Successful in 3m11s
The "This Week" reading-stats strip leaves its full-width row below the subbar and now lives inside the library bar itself, between the switcher and the customize/refresh actions. Expanding it opens an absolutely positioned popover that FLOATS over the collections — opening the stats no longer shifts the page down. - LibrarySwitcher gains an optional `center` component slot rendered between the left group and the actions group (nil = unused). Series and collections call sites pass nil and render byte-identical output. - DashboardWeekWidget redesigned for the slot: compact inline summary (icon, "This Week", minutes; pages/sessions/books/streak reveal at md/lg/xl) and a card popover panel with the labeled grid, sync note, and the Full stats link moved in from the old summary row. - Panel anchoring is responsive: below `sm` it anchors to the sticky bar itself (details goes static; bar-anchored left-4/right-4, i.e. screen-centered); at `sm`+ the details is relative again and the panel centers on the widget (w-[30rem], translate-x centering). - On phones the summary collapses to the chart icon + chevron (bar row width is scarce); "This Week"/minutes reveal at `sm`, and the "Your Library" title hides below `sm` to keep the row from overlapping. Icon-only toggle carries an aria-label. - web/src/dashboard.ts: click-outside and Escape close the popover, re-querying #week-widget per event because htmx replaces it on library switches. Rendering untouched (behavior-only island). - id="week-widget" and all htmx wiring unchanged: week-refresh refetch + the /dashboard/week-widget fragment still drive library-scoped refreshes behind the switch fade; stats-off suppression now yields an empty slot via weekWidgetComponent instead of a hidden strip. - web/static/style.css rebuilt for the new utilities. Known behavior (unchanged or deliberate): the bar is sticky, so stats stay visible while scrolling; a library switch re-renders the details closed; scopes with no current-week reading show zeros (e.g. Manga).
This commit is contained in:
1 parent
f6a73ab5aa
commit
8c97f59453
10 files changed
+299
-234
No files matched your search
+53
-38
@@ -18,7 +18,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
</head>
|
||||
<body x-data="dashboard" x-init="initDashboard()" class={ "theme-" + user.Theme }>
|
||||
@Header(user, "/dashboard")
|
||||
@LibrarySwitcher(libData, currentLibraryID, "Your Library", DashboardActions())
|
||||
@LibrarySwitcher(libData, currentLibraryID, "Your Library", weekWidgetComponent(week, activeLibType), DashboardActions())
|
||||
<!-- OUTSIDE #collections-container on purpose: the switcher's
|
||||
re-render wipes that container's innerHTML, and anything
|
||||
server-rendered inside it dies on the first library
|
||||
@@ -41,11 +41,6 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
if week != nil {
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 pt-6">
|
||||
@DashboardWeekWidget(*week, activeLibType)
|
||||
</div>
|
||||
}
|
||||
<main id="collections-container" class="w-full px-4 sm:px-6 lg:px-8 pt-5 pb-8">
|
||||
for _, section := range sections {
|
||||
@CollectionCarousel(section)
|
||||
@@ -317,41 +312,56 @@ type BookStatsData struct {
|
||||
FirstSession string
|
||||
}
|
||||
|
||||
// DashboardWeekWidget renders the collapsible "This Week" strip. The
|
||||
// collapsed summary row reads at a glance on every screen size; the
|
||||
// expanded body holds the full grid. The details element re-fetches
|
||||
// itself through htmx when the body sees a `week-refresh` event
|
||||
// (dispatched by the library switcher), carrying the CURRENT
|
||||
// #library-select value, so the widget tracks the switched library.
|
||||
// weekWidgetComponent adapts the optional week stats into the
|
||||
// LibrarySwitcher's center slot. nil (stats unavailable or the stats
|
||||
// service is off) = the slot stays empty, exactly as when the widget
|
||||
// used to be suppressed entirely.
|
||||
func weekWidgetComponent(week *WeekStatsData, activeLibType string) templ.Component {
|
||||
if week == nil {
|
||||
return nil
|
||||
}
|
||||
return DashboardWeekWidget(*week, activeLibType)
|
||||
}
|
||||
|
||||
// DashboardWeekWidget renders the "This Week" stats that live in the
|
||||
// library bar's center slot (LibrarySwitcher's `center` component).
|
||||
// The summary is a compact inline row whose numbers reveal themselves
|
||||
// as width allows; the expanded body is an absolutely positioned
|
||||
// popover that FLOATS over the page content — opening it never shifts
|
||||
// the collections below. The details element re-fetches itself through
|
||||
// htmx when the body sees a `week-refresh` event (dispatched by the
|
||||
// library switcher), carrying the CURRENT #library-select value, so
|
||||
// the widget tracks the switched library. Visible at every width — on
|
||||
// narrow screens the summary collapses to the chart icon + chevron
|
||||
// (tap target, aria-labeled; "This Week" + minutes reveal at `sm`,
|
||||
// pages/sessions/books at md/lg/xl). The panel anchors to the
|
||||
// BAR (full-width under it) below `sm` — the widget is too far left on
|
||||
// phones for widget-centered anchoring — and centers on the widget at
|
||||
// `sm` and up.
|
||||
templ DashboardWeekWidget(week WeekStatsData, activeLibType string) {
|
||||
<details
|
||||
id="week-widget"
|
||||
class="card group"
|
||||
class="group sm:relative"
|
||||
hx-get="/dashboard/week-widget"
|
||||
hx-trigger="week-refresh from:body"
|
||||
hx-include="#library-select"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<summary class="px-4 md:px-6 py-3 flex flex-wrap items-center gap-x-5 gap-y-1 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
|
||||
<span class="flex items-center gap-2 font-bold tracking-tight" style="color: var(--text-primary)">
|
||||
<span class="grid place-items-center h-6 w-6 rounded-md" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("chart", "h-4 w-4")
|
||||
</span>
|
||||
This Week
|
||||
<summary class="flex items-center gap-2 cursor-pointer select-none list-none rounded-lg px-2 py-1 hover:bg-surface-hover [&::-webkit-details-marker]:hidden" aria-label="Reading stats for this week">
|
||||
<span class="grid place-items-center h-6 w-6 rounded-md shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("chart", "h-4 w-4")
|
||||
</span>
|
||||
<span class="text-sm font-semibold" style="color: var(--text-primary)">{ fmtReadingMinutes(week.WeekMinutes) }</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ fmt.Sprintf("%d pages", week.WeekPages) }</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ fmt.Sprintf("%d sessions", week.WeekSessions) }</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">{ fmt.Sprintf("%d books", week.WeekBooks) }</span>
|
||||
<span class="text-sm font-bold tracking-tight whitespace-nowrap hidden sm:inline" style="color: var(--text-primary)">This Week</span>
|
||||
<span class="text-sm font-semibold whitespace-nowrap hidden sm:inline" style="color: var(--text-primary)">{ fmtReadingMinutes(week.WeekMinutes) }</span>
|
||||
<span class="text-xs whitespace-nowrap hidden md:inline" style="color: var(--text-secondary)">{ fmt.Sprintf("%d pages", week.WeekPages) }</span>
|
||||
<span class="text-xs whitespace-nowrap hidden lg:inline" style="color: var(--text-secondary)">{ fmt.Sprintf("%d sessions", week.WeekSessions) }</span>
|
||||
<span class="text-xs whitespace-nowrap hidden xl:inline" style="color: var(--text-secondary)">{ fmt.Sprintf("%d books", week.WeekBooks) }</span>
|
||||
if week.CurrentStreak > 0 {
|
||||
<span class="text-sm font-medium" style="color: var(--accent)">🔥 { fmt.Sprintf("%d", week.CurrentStreak) }-day streak</span>
|
||||
<span class="text-xs font-medium whitespace-nowrap hidden lg:inline" style="color: var(--accent)">🔥 { fmt.Sprintf("%d", week.CurrentStreak) }-day streak</span>
|
||||
}
|
||||
<span class="ml-auto flex items-center gap-3">
|
||||
<a href="/stats" class="text-xs font-medium hover:underline" style="color: var(--accent);" @click.stop>Full stats</a>
|
||||
@Icon("chevron-down", "h-4 w-4 transition-transform group-open:rotate-180")
|
||||
</span>
|
||||
@Icon("chevron-down", "h-4 w-4 shrink-0 transition-transform group-open:rotate-180")
|
||||
</summary>
|
||||
<div class="px-4 md:px-6 pb-4 pt-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);">
|
||||
<div class="card absolute top-full left-4 right-4 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 mt-2 sm:w-[30rem] max-w-[calc(100vw-2rem)] z-40 p-4 md:px-6 md:py-5" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
if activeLibType == "audiobooks" {
|
||||
<div>
|
||||
@@ -387,16 +397,21 @@ templ DashboardWeekWidget(week WeekStatsData, activeLibType string) {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<p class="text-xs mt-3" style="color: var(--text-secondary)">
|
||||
Synced from your reading devices
|
||||
if week.CurrentStreak > 0 {
|
||||
if week.ActiveToday {
|
||||
{ fmt.Sprintf(" · 🔥 %d-day streak — keep it going today", week.CurrentStreak) }
|
||||
} else {
|
||||
{ fmt.Sprintf(" · 🔥 %d-day streak — read today to keep it", week.CurrentStreak) }
|
||||
<div class="flex items-end justify-between gap-4 mt-3 pt-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);">
|
||||
<p class="text-xs" style="color: var(--text-secondary)">
|
||||
Synced from your reading devices
|
||||
if week.CurrentStreak > 0 {
|
||||
if week.ActiveToday {
|
||||
{ fmt.Sprintf(" · 🔥 %d-day streak — keep it going today", week.CurrentStreak) }
|
||||
} else {
|
||||
{ fmt.Sprintf(" · 🔥 %d-day streak — read today to keep it", week.CurrentStreak) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</p>
|
||||
</p>
|
||||
<a href="/stats" class="inline-flex items-center gap-1 text-xs font-medium hover:underline shrink-0 whitespace-nowrap" style="color: var(--accent);">Full stats
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user