| Commit message (Collapse) | Author | Age | Files | Lines |
| | |
|
| |
|
|
|
|
|
|
|
|
|
| |
Introduce --shadow-card, --shadow-card-emphasized, --shadow-cta,
--scrim, --scrim-soft, and --scrim-banner. Migrate 14 inline
literals (card.css, input.css, Notification, MediaRoulette, the
two Landing CTA buttons, the popup overlay, the palette and
roulette scrims, three identity banners, the user profile cover
art shadow) onto the tokens, with light/dark adaptation handled
by the existing prefers-color-scheme blocks instead of duplicated
inline. Two single-use Landing demo-focus values stay inline.
|
| |
|
|
|
|
|
|
|
|
|
| |
Strip backdrop-filter: blur(4px) from the base .card so the ~50
in-flow surfaces (landing panels, schedule rows, tool cards, user
profile cards, badge wall, etc.) stop paying for a blur they don't
need. Introduce .card-glass for the surfaces that actually float over
other content: the sticky page header, header nav dropdowns, command
palette, title-attribute tooltips, LinkedTooltip, and HoverCover.
Popup, MediaRoulette, and Notification keep their existing
parent-overlay or solid-background treatments.
|
| |
|
|
|
|
|
|
|
| |
Replace generic alt="Avatar" / alt="Character" with empty alts on
images that visually duplicate a name already present as adjacent text
(Wrapped activity avatar, Hololive stream icon, three Birthdays
covers); screen readers stop announcing "Avatar" / "Character" twice.
Also swap the em dash in the landing subheadline for a comma per the
project copy rule.
|
| |
|
|
|
|
|
|
|
| |
Replace the * { transition: color/bg/border/shadow } rule with a
:where() list of the surfaces that actually receive state changes
(anchors, buttons, form controls, cards, details/summary, role=button,
role=menuitem, role=option). Every element no longer pays the
transition (especially box-shadow, the costly one) on theme switch or
parent re-paint.
|
| |
|
|
|
|
|
|
|
| |
Add loading=lazy and decoding=async to the 16 <img> elements that
weren't already deferring across Tools/Wrapped, Events, EasterEvent,
Hololive, and the rate-limited fallback. Also drop the
backdrop-filter: blur(160px) the dropdown items were paying on every
hover; the background-color change already gives sufficient feedback,
and the parent card's own blur stays.
|
| |
|
|
|
|
|
|
| |
Wrap the palette in role=dialog with aria-modal, mark the overlay
aria-hidden, and turn the search input into a labeled combobox driving
a listbox of role=option results via aria-activedescendant. Trap Tab on
the input, preventDefault on Escape, and restore focus to the
previously-focused element when the palette closes.
|
| |
|
|
|
| |
The scoped :focus-visible rule on dropdown menu items no longer needs
its own border-radius now that the global rule sets it.
|
| |
|
|
|
|
| |
Add border-radius: 4px to the global :focus-visible rule so the ring
looks consistent on bare anchors and header items, not just on
elements that already carry their own radius.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Wire Enter/Space/ArrowDown/ArrowUp/Escape on the toggle and
ArrowDown/ArrowUp/Home/End/Escape on each item so the Schedule and
Profile menus are reachable without a mouse. Add aria-haspopup,
aria-expanded, aria-controls, and role=menu/menuitem; give each
instance a unique toggle/menu id so the two header dropdowns no longer
collide. Close the menu on item activation so preventDefault items
(e.g. Log Out) don't leave it hanging open. Focus moves via
`await tick()` so :focus-visible matches reliably, with a scoped
fallback outline tuned to var(--base0D) for the menu items.
|
| |
|
|
|
|
|
| |
Add a global reduced-motion media query that collapses animation and
transition durations and disables smooth scroll. Skip Lenis init when
the same preference is set so the JS-driven smooth scroll falls back to
the browser default, which the spec already honours.
|
| |
|
|
|
|
|
| |
Replace `a:focus { outline: none }` with `:focus { outline: none }` +
`:focus-visible { outline: 2px solid var(--base0D); outline-offset: 2px }`
so keyboard users see focus on every interactive element while mouse
clicks stay clean.
|
| |
|
|
| |
Add --ease-out-quart, --ease-in-out-quart, --duration-fast, --duration-slow in motion.css and migrate the global anchor, header, and theme-switch transitions to use them. Establishes a shared motion vocabulary for future polish.
|
| |
|
|
| |
Replace transform 0.3s ease with 0.4s cubic-bezier(0.22, 1, 0.36, 1) so the header settles instead of snapping, pairing with Lenis-smoothed scroll.
|
| |
|
|
| |
Replace transition: all with explicit color, opacity, and text-decoration-color so unrelated property changes (transform, background) do not get incidentally tweened.
|
| |
|
|
| |
Bump Root fly duration/delay from 100ms to 180ms so the built-in opacity fade is perceptible and the slide reads against Lenis-smoothed scroll.
|
| |
|
|
| |
Lenis disables native CSS smooth scroll, so window.scrollTo({behavior:"smooth"}) jumps instantly. Expose the Lenis instance via a store and scroll through it, falling back to native when unavailable.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
| |
Adds 13 reactive quick toggles (24h time, animations, blur adult, cover
modes, hover cover, schedule list, reverse sort, data saver, notifications,
language, title format cycle, outbound link target cycle), three sync
actions (push/pull/disable), and login/logout entries gated on auth state.
Names reflect current state so the palette doubles as a status surface.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The tooltip was appended to body with no top/left, so its static-flow position
extended body.scrollWidth/scrollHeight during measurement. Fix by setting
initial top/left to -9999px so the node never lands in the flow.
Previously this slid the tooltip in from off-screen because the top/left 0.3s
transition was already live. Now the transition starts as opacity-only at
creation time; after the first updateTooltipPosition snaps into place, a
requestAnimationFrame re-enables the top/left easing so subsequent mousemove
updates keep the original smooth glide and 100ms debounce behavior.
LinkedTooltip's measurement div is parked the same way plus visibility:hidden,
since it exists only to read offsetWidth for the Svelte-rendered tooltip.
|
| |
|
|
| |
This reverts commit 28860bb88da4c08e3ba383adc9c23ae3689310b6.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Body grew on hover because the absolutely-positioned tooltip was appended to
document.body without top/left, so its static-flow position extended
body.scrollWidth/scrollHeight during layout measurement.
Switch both the use:tooltip directive and LinkedTooltip's measurement div to
position:fixed, which is relative to the viewport and does not contribute to
the body's scroll area. The directive now also uses clientX/clientY (dropping
the scrollY offset for the pin branch), removes the 100ms mousemove debounce,
and drops the top/left transition so the tooltip tracks the cursor without
feeling laggy or sliding in from the page top.
|
| |
|
|
| |
This reverts commit 6865cae76cb88aa78d7c297c637557468fdce8fc.
|
| |
|
|
|
|
| |
sliding"
This reverts commit 6e780fdb76155669a72692c64fd51c0da2c932d2.
|
| |
|
|
|
|
|
| |
The opacity+top+left transition combined with the off-screen parking caused
tooltips to animate from -9999px into place ("pops down from the top") and
lagged cursor tracking in the non-pinned variant used by birthdays. Keep only
the opacity fade; position updates snap.
|
| |
|
|
|
|
| |
Absolute-positioned tooltips were appended to document.body without initial
left/top, so their static-flow position extended body scrollWidth/scrollHeight
during layout, briefly enlarging the page on hover.
|
| |
|
|
|
|
| |
new Date(Date.now()) is identical to new Date() and was called twice
back-to-back to seed currentYear and selectedYear to the same value.
Call new Date() once for currentYear and reuse it for selectedYear.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
CLAUDE.md prohibits abbreviations like ctx in identifiers. Rename the
Cloudflare Worker ExecutionContext parameter to executionContext in the
proxy worker (handleMangaChapterCounts, fetch, scheduled), and alias
Trigger.dev's destructured { ctx } to taskContext in the notifications
scheduled task. The external property name on Trigger.dev's params
object is library-defined and remains ctx on the wire.
Verified: proxy worker still boots under local wrangler dev and all
routes (OPTIONS, POST /manga/chapter-counts, forwardProxyRequest) still
respond identically.
|
| |
|
|
| |
This reverts commit 13226aaeb7c4dc1ce01074ef1ba1eeb87b53d5f5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Earlier fix added .catch() handlers to both fetch chains, but the
outer .then body called response.json().then(...) without returning
the inner promise. That meant rejections from response.json() (e.g.
non-JSON response) or from the inner handler body (e.g. a throw in
isEqualsJson or data access) floated unhandled.
Return the inner promise so its rejection reaches the outer .catch.
Also short-circuit early on !response.ok to keep the happy path at
the top indentation level.
Verified via a standalone promise-graph reproducer: all seven paths
(outer fetch reject, non-ok response, json reject, handler throw,
push fetch reject, push handler throw, happy path) are caught by
their intended handler.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
LinkedTooltip appends its tooltip div directly to document.body in
createTooltip and only removes it from hideTooltip. If the component
was destroyed while the tooltip was visible (e.g. parent navigated
mid-hover), the div leaked and stayed in the DOM forever.
Add an onDestroy handler that clears the pending hide/debounce timers
and, if the tooltip div is still parented to document.body, removes
it. Guarding on parentNode avoids double-removal when hideTooltip has
already run.
|
| |
|
|
|
|
|
|
|
| |
onMount scheduled a setTimeout to auto-dismiss the notification after
notification.duration ms but never stored the handle. If the component
was destroyed before the timer fired (parent unmount, navigation), the
timer still ran and called remove() on a stale closure.
Capture the timer id and clear it from the onMount cleanup callback.
|
| |
|
|
|
|
|
|
|
|
|
| |
The settings store's subscribe callback kicked off a fetch chain to
pull/push settings with no .catch() on either promise chain. A network
failure or non-ok PUT from /api/configuration would surface as an
unhandled promise rejection.
Attach a .catch() that logs a descriptive error on both the pull and
push chains so sync failures become observable without crashing the
page.
|
| |
|
|
|
|
|
| |
setShadowHidden is async and hits Supabase. The PUT handler called it
without await, so the handler could respond before the database write
landed (and any error was silently lost). Add the missing await so the
response only goes out after the update settles.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The refresh endpoint accepted a ?redirect query param and, when
present, called redirect(303, "/") instead of returning the refreshed
token as JSON. The target was hardcoded to "/" regardless of the
param's value, so the feature was dead — and the pattern of reading
a "redirect" param invited future open-redirect bugs if someone wired
the value through to redirect() directly.
The sole in-tree caller (feeds/activity-notifications) reads the JSON
response, so always return JSON and drop the redirect import.
|
| |
|
|
|
|
|
| |
The onMount cleanup removed resize and scroll listeners that were
never actually registered — the only live side effect is the
updatePosition interval. Drop the two dead removeEventListener calls
so the cleanup only matches what the component actually sets up.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The public variant of mediaListCollection chained data.MediaListCollection
.lists with no null check, so any AniList error response (rate limit,
private list, nonexistent user) threw a TypeError.
Mirror the guard used by the authenticated sibling: if any of data,
MediaListCollection, or lists is missing, return an empty array.
The function currently has no in-tree callers; noting that it may be
a candidate for removal in a follow-up.
|
| |
|
|
|
|
|
|
|
|
| |
The ParallaxImage alternativeText prop was passed as the literal
string "selectedBadge.description" (quoted attribute syntax) instead
of the expression, so every badge image rendered with that identifier
name as its alt text.
Switch to the expression form with a nullish-coalescing fallback so
badges without a description render empty alt rather than the literal.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The seasonal schedule collector pushed page 1 outside the while-loop,
then re-pushed the current page on every loop iteration. That meant
every multi-page season duplicated its first page's media, and the
same bug ran a second time when includeLastSeason was set.
Restructure into a shared collectAllSchedulePages helper with a single
push-then-check-hasNextPage loop, so each page is pushed exactly once.
Verified against AniList for SPRING 2026 with includeLastSeason=true:
210 media, 0 duplicates.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The PUT ?incrementClickCount path ran before any auth guard, letting
unauthenticated callers spam-increment arbitrary badges. Require the
request Origin to match appOrigin() so legitimate in-browser clicks
(authenticated or not) still count while direct scripted calls are
rejected.
Also convert the shared `unauthorised` Response singleton into a
factory. The singleton's body was consumed on first use, so subsequent
401 paths returned a `Response body is locked` error instead of the
intended "Unauthorised" body.
|
| |
|
|
|
|
|
| |
The `tz` query value was interpolated raw into the upstream URL, letting
callers append arbitrary query segments (e.g. `tz=foo&f=hax`). Wrap the
value in encodeURIComponent and rename the local variable away from the
banned `tz` abbreviation.
|
| | |
|
| |
|
|
|
|
| |
Showing "0" in the list title next to a "No anime/manga to display"
empty-state message duplicates information. Treat an empty media list
the same as dummy mode and suppress the count.
|
| |
|
|
|
|
|
|
| |
If a user's persisted filter references a custom list that no longer
exists on any media (renamed or deleted on AniList), the select falls
back to showing "All" but the underlying state still filters to an
empty set. Detect the stale value and reset it, clearing the StateBin
entry so the fix survives a reload.
|
| |
|
|
|
|
| |
Previously the link rendered "Toggle follow for ..." and was clickable
with an empty submission. Only render once the input has content so
there is no dead affordance.
|