Skip to content

refactor: solid header (@miodec)#7564

Open
Miodec wants to merge 74 commits intomasterfrom
solid-nav
Open

refactor: solid header (@miodec)#7564
Miodec wants to merge 74 commits intomasterfrom
solid-nav

Conversation

@Miodec
Copy link
Member

@Miodec Miodec commented Mar 2, 2026

No description provided.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the frontend header from legacy HTML/DOM-driven code to SolidJS components, introducing new signals/hooks to drive header UI state (account spinner, notifications, XP bar) from application logic.

Changes:

  • Replace <load src="html/header.html" /> with a Solid-mounted <Header /> and add new header/layout components.
  • Introduce signal-based state for header elements (notification bubble, account spinner, XP bar events) and wire parts of test/config/db flows into them.
  • Add common UI primitives (e.g., Bar, new anime helpers) used by the new header and profile UI.

Reviewed changes

Copilot reviewed 51 out of 52 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pnpm-lock.yaml Lockfile update for new frontend dependency (motion).
frontend/package.json Adds motion dependency.
packages/oxlint-config/rules/jsx.jsonc Allows router-link JSX attribute in lint config.
frontend/src/index.html Mounts Solid header instead of loading legacy header HTML.
frontend/src/html/header.html Partially comments out legacy nav links (legacy header content).
frontend/src/styles/nav.scss Comments out legacy header/nav SCSS (migration away from old DOM structure).
frontend/src/styles/media-queries-purple.scss Comments out legacy header/nav responsive overrides.
frontend/src/styles/media-queries-green.scss Comments out legacy header/nav responsive overrides.
frontend/src/styles/media-queries-gray.scss Comments out legacy header/nav responsive overrides.
frontend/src/styles/media-queries-brown.scss Comments out legacy header/nav responsive overrides.
frontend/src/styles/media-queries-blue.scss Comments out legacy header/nav responsive overrides.
frontend/src/ts/utils/levels.ts Extends XP detail calculations with float/progress percent fields.
frontend/src/ts/test/test-ui.ts Adds signal-based XP breakdown skipping (but retains legacy call).
frontend/src/ts/test/test-logic.ts Shifts account spinner to header signal; stops using legacy XPBar update path.
frontend/src/ts/test/focus.ts Removes header from focus-class toggling selector list.
frontend/src/ts/stores/snapshot.ts Exports MiniSnapshot; unwraps Solid stores before cloning/reconciling.
frontend/src/ts/signals/xp-bar.ts New XP bar data + skip event signals.
frontend/src/ts/signals/user.ts New Solid store for authenticated user state.
frontend/src/ts/signals/loader-bar.ts Refactors loader bar signal using createSignalWithSetters.
frontend/src/ts/signals/header.ts New header signals (notification bubble + account spinner).
frontend/src/ts/signals/animated-level.ts New signal for animated level display.
frontend/src/ts/hooks/useSwapAnimation.ts New hook for swapping visibility with animations.
frontend/src/ts/hooks/useRefWithUtils.ts Refactors ref hook to use a signal instead of a mutable local.
frontend/src/ts/hooks/createSignalWithSetters.ts New helper to create signals with mapped setter functions.
frontend/src/ts/hooks/createEvent.ts New “event signal” helper (counter-based).
frontend/src/ts/elements/alerts.ts Uses header notification bubble signal; renames show to showAlerts.
frontend/src/ts/elements/account-button.ts No-ops legacy account button DOM manipulation (migration).
frontend/src/ts/db.ts Emits XP bar signal data from snapshot updates / XP changes.
frontend/src/ts/config.ts Drives account spinner signal during config saves.
frontend/src/ts/components/utils/Transition.tsx Adds placeholder Transition component.
frontend/src/ts/components/pages/profile/UserDetails.tsx Uses new Bar component for level progress UI.
frontend/src/ts/components/pages/AboutPage.tsx Comments out hover prefetch wiring (legacy DOM behavior).
frontend/src/ts/components/mount.tsx Registers new header mount target.
frontend/src/ts/components/modals/DevOptionsModal.tsx Updates XP bar dev test to use db.addXp signal path.
frontend/src/ts/components/layout/header/Header.tsx New Solid header wrapper.
frontend/src/ts/components/layout/header/Logo.tsx New Solid logo component with router-link integration.
frontend/src/ts/components/layout/header/Nav.tsx New Solid nav (alerts, account menu, XP bar).
frontend/src/ts/components/layout/header/AccountMenu.tsx New Solid account dropdown.
frontend/src/ts/components/layout/header/AccountXpBar.tsx New Solid XP bar animation + breakdown UI.
frontend/src/ts/components/common/anime/index.ts Updates anime exports to new helper components.
frontend/src/ts/components/common/anime/AnimeShow.tsx Renames + tweaks default duration and adds class passthrough.
frontend/src/ts/components/common/anime/AnimeSwitch.tsx Adds animated Switch wrapper with shared defaults.
frontend/src/ts/components/common/anime/AnimeMatch.tsx Adds Match wrapper that consumes AnimeSwitch defaults.
frontend/src/ts/components/common/anime/AnimeConditional.tsx Adds convenience animated if/then/else wrapper.
frontend/src/ts/components/common/anime/AnimeGroupTest.tsx Expands doc comment for test/demo component.
frontend/src/ts/components/common/anime/Anime.tsx Adds ref passthrough to underlying DOM element.
frontend/src/ts/components/common/UserFlags.tsx Adds optional class passthrough for flag icons/badges.
frontend/src/ts/components/common/User.tsx Refactors user display; adds animated level + spinner/avatar swap.
frontend/src/ts/components/common/NotificationBubble.tsx New notification bubble component.
frontend/src/ts/components/common/DiscordAvatar.tsx Adds configurable fallback icon rendering.
frontend/src/ts/components/common/Button.tsx Adds mouse enter/leave props; relaxes onClick typing (anchor/button).
frontend/src/ts/components/common/Bar.tsx New bar primitive used by profile + header XP UI.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)

frontend/src/ts/components/common/Button.tsx:104

  • BaseProps now allows onClick even when rendering an anchor (href present), but the <a> branch never attaches onClick. This breaks callers like the new header nav (start-test button passes href + onClick to restart). Either wire props.onClick onto the <a> element or reintroduce the type restriction so onClick can’t be provided with href.
type BaseProps = {
  text?: string;
  fa?: FaProps;
  class?: string;
  classList?: JSX.HTMLAttributes<HTMLButtonElement>["classList"];
  type?: "text" | "button";
  children?: JSXElement;
  ariaLabel?:
    | string
    | { text: string; position: "up" | "down" | "left" | "right" };
  "router-link"?: true;
  onClick?: () => void;
  onMouseEnter?: () => void;
  onMouseLeave?: () => void;
};

type ButtonProps = BaseProps & {
  href?: never;
  sameTarget?: true;
  active?: boolean;
  disabled?: boolean;
};

type AnchorProps = BaseProps & {
  href: string;
  // onClick?: never;
  disabled?: never;
};

export function Button(props: ButtonProps | AnchorProps): JSXElement {
  const isAnchor = "href" in props;
  const isActive = (): boolean => (!isAnchor && props.active) ?? false;

  const content = (
    <>
      <Show when={props.fa !== undefined}>
        <Fa {...(props.fa as FaProps)} />
      </Show>
      <Show when={props.text !== undefined}>{props.text}</Show>
      {props.children}
    </>
  );

  const ariaLabel = (): object => {
    if (props.ariaLabel === undefined) return {};
    if (typeof props.ariaLabel === "string") {
      return { "aria-label": props.ariaLabel, "data-balloon-pos": "up" };
    }
    return {
      "aria-label": props.ariaLabel.text,
      "data-balloon-pos": props.ariaLabel.position,
    };
  };

  const getClasses = (): string => {
    return cn(
      "inline-flex h-min cursor-pointer appearance-none items-center justify-center gap-[0.5em] rounded border-0 p-[0.5em] text-center leading-[1.25] text-text transition-[color,background,opacity] duration-125 ease-in-out select-none",
      "focus-visible:shadow-[0_0_0_0.1rem_var(--bg-color),_0_0_0_0.2rem_var(--text-color)] focus-visible:outline-none",
      {
        "bg-sub-alt hover:bg-text hover:text-bg": props.type !== "text",
        "bg-transparent text-sub hover:text-text": props.type === "text",
        [props.class ?? ""]: props.class !== undefined,
        "bg-main text-bg hover:bg-text": isActive(),

        ...props.classList,
      },
      {
        "opacity-[0.33]": props.disabled,
        "bg-text text-bg": isActive() && props.disabled,
      },
    );
  };

  return (
    <Conditional
      if={isAnchor}
      then={
        <a
          class={getClasses()}
          href={props.href}
          target={
            props["router-link"] || props.href?.startsWith("#")
              ? undefined
              : "_blank"
          }
          rel={
            props["router-link"] || props.href?.startsWith("#")
              ? undefined
              : "noreferrer noopener"
          }
          {...ariaLabel()}
          {...(props["router-link"] ? { "router-link": "" } : {})}
          onMouseEnter={() => props.onMouseEnter?.()}
          onMouseLeave={() => props.onMouseLeave?.()}
        >
          {content}
        </a>
      }

frontend/src/ts/components/common/anime/AnimeShow.tsx:15

  • Doc mismatch: the comment says duration default is 250ms, but the implementation defaults to 125ms. Update the doc to match the new default (or revert the default) so consumers aren’t misled.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Continuous integration check(s) failed. Please review the failing check's logs and make the necessary changes.

@github-actions github-actions bot added waiting for update Pull requests or issues that require changes/comments before continuing and removed waiting for review Pull requests that require a review before continuing labels Mar 2, 2026
@github-actions github-actions bot removed the waiting for update Pull requests or issues that require changes/comments before continuing label Mar 2, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Continuous integration check(s) failed. Please review the failing check's logs and make the necessary changes.

@github-actions github-actions bot added the waiting for update Pull requests or issues that require changes/comments before continuing label Mar 2, 2026
@github-actions github-actions bot removed the waiting for update Pull requests or issues that require changes/comments before continuing label Mar 2, 2026
@Miodec Miodec requested a review from Copilot March 2, 2026 22:08
@github-actions github-actions bot added the waiting for review Pull requests that require a review before continuing label Mar 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 58 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines 85 to 105
<a
class={getClasses()}
href={props.href}
target={
props["router-link"] || props.href?.startsWith("#")
? undefined
: "_blank"
}
rel={
props["router-link"] || props.href?.startsWith("#")
? undefined
: "noreferrer noopener"
}
{...ariaLabel()}
{...(props["router-link"] ? { "router-link": "" } : {})}
onClick={() => props.onClick?.()}
onMouseEnter={() => props.onMouseEnter?.()}
onMouseLeave={() => props.onMouseLeave?.()}
{...props.dataset}
>
{content}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button now allows onClick in BaseProps, and callers (e.g. nav start button) pass onClick while also providing href (anchor mode). The anchor branch never binds onClick, so the handler is silently ignored (restart-on-click etc. breaks). Either wire onClick onto the <a> element too, or disallow onClick when href is present (restore the onClick?: never typing + remove from BaseProps).

Copilot uses AI. Check for mistakes.
Comment on lines +1214 to +1218
setAccountButtonSpinner(true);

const response = await Ape.results.add({ body: { result } });

AccountButton.loading(false);
setAccountButtonSpinner(false);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setAccountButtonSpinner(false) runs only after await Ape.results.add(...) resolves. If the request throws/rejects (network error, etc.), the spinner will stay stuck on. Wrap the await in try { ... } finally { setAccountButtonSpinner(false) } (and consider handling thrown errors similarly to non-200 responses).

Copilot uses AI. Check for mistakes.
Comment on lines 4 to +24
@@ -11,17 +11,17 @@ import {
import { getConfig } from "../../signals/config";
import { getActivePage } from "../../signals/core";
import { showModal } from "../../stores/modals";
import { qsr } from "../../utils/dom";
// import { qsr } from "../../utils/dom";
import { getNumberWithMagnitude } from "../../utils/numbers";
import AsyncContent from "../common/AsyncContent";
import { Button } from "../common/Button";
import { ChartJs } from "../common/ChartJs";
import { Fa } from "../common/Fa";
import { H2, H3 } from "../common/Headers";

qsr("nav .view-about").on("mouseenter", () => {
prefetch();
});
// qsr("nav .view-about").on("mouseenter", () => {
// prefetch();
// });
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now contains commented-out imports and a fully commented prefetch handler/function. If prefetch-on-hover is intentionally removed, delete the dead code; if it's still desired, re-enable it in a Solid-friendly way (e.g. attach onMouseEnter in the header/nav component and call queryClient.prefetchQuery(...)).

Copilot uses AI. Check for mistakes.
Comment on lines 171 to 184
it("saves to localstorage if nosave=false", async () => {
//GIVEN
replaceConfig({ numbers: false });

//WHEN
Config.setConfig("numbers", true);

//THEN
//wait for debounce
await vi.advanceTimersByTimeAsync(2500);

//show loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(1, true);

//save
expect(dbSaveConfigMock).toHaveBeenCalledWith({ numbers: true });

//hide loading
expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false);
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config tests previously asserted the account spinner/loading toggling during debounced saves. That coverage was removed when migrating off AccountButton.loading(...). Consider replacing it with an assertion on the new mechanism (e.g. spy on setAccountButtonSpinner from signals/header) so regressions in spinner behavior are still caught.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend User interface or web stuff packages Changes in local packages waiting for review Pull requests that require a review before continuing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants