diff --git a/.changeset/mobile-settings-star-count.md b/.changeset/mobile-settings-star-count.md
new file mode 100644
index 0000000000..119286eb74
--- /dev/null
+++ b/.changeset/mobile-settings-star-count.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Settings on mobile now keeps showing the GitHub star count.
+category: fix
+dev: Removed the ≤768px `display:none` on `.settings-github-star-btn__count` in SettingsModal.css (FN-7848).
diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css
index ac88ca2ce0..5e8c12e2f6 100644
--- a/packages/dashboard/app/components/SettingsModal.css
+++ b/packages/dashboard/app/components/SettingsModal.css
@@ -45,11 +45,10 @@
margin-right: var(--space-sm);
}
- /* Keep the mobile title readable by reducing promotional header actions to their already-labeled icons. */
- .settings-header-actions .settings-github-star-btn__count {
- display: none;
- }
-
+ /*
+ FNXC:Settings 2026-07-11-00:00:
+ FN-7848: Mobile Settings must still show the GitHub star count. FN-4375 kept the header in one row by collapsing promotional action labels; keep the count visible and rely on .settings-modal-heading min-width: 0 to truncate the title before the pill wraps. Only the redundant "Star" word stays collapsed to save horizontal room.
+ */
.settings-header-actions .settings-github-star-btn__action,
.settings-header-actions .settings-header-discord-btn {
width: 40px;
diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx
index 20fa713bb0..42936ff867 100644
--- a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx
+++ b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi } from "vitest";
+import { beforeEach, describe, it, expect, vi } from "vitest";
import type { ComponentProps } from "react";
import { render, screen, fireEvent, waitFor, within, act, cleanup } from "@testing-library/react";
import path from "path";
@@ -203,6 +203,10 @@ vi.mock("../FileBrowser", () => ({
describe("SettingsModal", () => {
installSettingsModalEnv();
+ beforeEach(() => {
+ localStorage.setItem("fusion:settings:show-advanced", "true");
+ });
+
describe("Project Models", () => {
it("saves opencode-go startup model sync toggle in global settings", async () => {
mockFetchModels.mockResolvedValue({
@@ -696,7 +700,7 @@ describe("SettingsModal", () => {
renderModal();
await waitForSettingsModalReady();
- expect(await screen.findByText("Version 1.2.3")).toBeInTheDocument();
+ expect(await screen.findByText("v1.2.3")).toBeInTheDocument();
expect(mockFetchDashboardHealth).toHaveBeenCalledTimes(1);
expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument();
@@ -864,9 +868,9 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
const inlineButton = screen.getByRole("button", { name: "Check for updates" });
- expect(within(inlineButton).getByText("Version 1.2.3")).toBeInTheDocument();
+ expect(within(inlineButton).getByText("v1.2.3")).toBeInTheDocument();
- await settingsModalUser.click(within(inlineButton).getByText("Version 1.2.3"));
+ await settingsModalUser.click(within(inlineButton).getByText("v1.2.3"));
await waitFor(() => {
expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
index d2f0c6f58f..3a0256b132 100644
--- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
@@ -43,6 +43,7 @@ const defaultSettings = {
vi.mock("../../api", () => ({
fetchProjects: vi.fn(() => Promise.resolve([])),
fetchGitRemotes: vi.fn(() => Promise.resolve({ remotes: [] })),
+ fetchGitRemotesDetailed: vi.fn(() => Promise.resolve([])),
fetchGitBranches: vi.fn(() => Promise.resolve([])),
fetchSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
fetchSettingsByScope: vi.fn(() => Promise.resolve({ global: { ...defaultSettings }, project: {} })),
@@ -179,6 +180,37 @@ function expectMobileRule(css: string, selector: string, declaration: string): v
expect(pattern.test(css)).toBe(true);
}
+function getMobileMediaBlocks(css: string): string[] {
+ const blocks: string[] = [];
+ const mediaPattern = /@media[^{]*\(max-width:\s*768px\)[^{]*\{/g;
+ let match: RegExpExecArray | null;
+
+ while ((match = mediaPattern.exec(css)) !== null) {
+ let depth = 0;
+ let end = match.index;
+ for (; end < css.length; end += 1) {
+ if (css[end] === "{") depth += 1;
+ if (css[end] === "}") depth -= 1;
+ if (depth === 0 && end > match.index) {
+ end += 1;
+ break;
+ }
+ }
+ blocks.push(css.slice(match.index, end));
+ mediaPattern.lastIndex = end;
+ }
+
+ return blocks;
+}
+
+function expectNoMobileRule(css: string, selector: string, declaration: string): void {
+ const pattern = new RegExp(
+ `${escapeRegExp(selector)}\\s*\\{[^}]*${escapeRegExp(declaration)}`,
+ );
+ const offendingBlock = getMobileMediaBlocks(css).find((block) => pattern.test(block));
+ expect(offendingBlock).toBeUndefined();
+}
+
function expectBaseRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`${escapeRegExp(selector)}\\s*\\{[^}]*${escapeRegExp(declaration)}`,
@@ -189,6 +221,9 @@ function expectBaseRule(css: string, selector: string, declaration: string): voi
describe("SettingsModal mobile adaptations", () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.removeItem("fusion_github_star_count");
+ localStorage.removeItem("fusion:github-star-clicked");
+ localStorage.setItem("fusion:settings:show-advanced", "true");
mockSettingsViewport(false);
});
@@ -550,6 +585,32 @@ describe("SettingsModal mobile adaptations", () => {
expect(container.querySelectorAll(".notification-provider-card").length).toBeGreaterThan(1);
});
+ it("keeps the GitHub star count visible in the mobile Settings header", async () => {
+ const css = loadAllAppCss();
+
+ expectNoMobileRule(css, ".settings-header-actions .settings-github-star-btn__count", "display: none;");
+ expectBaseRule(css, ".settings-github-star-btn__count", "display: inline-flex;");
+
+ localStorage.setItem("fusion_github_star_count", JSON.stringify({ count: 1234, fetchedAt: Date.now() }));
+ mockSettingsViewport(true);
+
+ const modalRender = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+ const modalCount = modalRender.container.querySelector(".settings-github-star-btn__count");
+ expect(modalCount).toBeTruthy();
+ expect(modalCount?.textContent).toBe("1.2k");
+ modalRender.unmount();
+
+ vi.mocked(fetchSettings).mockClear();
+ const embeddedRender = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+ const embeddedCount = embeddedRender.container.querySelector(".settings-github-star-btn__count");
+ expect(embeddedCount).toBeTruthy();
+ expect(embeddedCount?.textContent).toBe("1.2k");
+ embeddedRender.unmount();
+ localStorage.removeItem("fusion_github_star_count");
+ });
+
it("contains required mobile settings CSS overrides", () => {
const css = loadAllAppCss();
@@ -569,7 +630,7 @@ describe("SettingsModal mobile adaptations", () => {
expectMobileRule(css, ".settings-nav-item", "gap: var(--space-xs);");
expectMobileRule(css, ".settings-content", "padding: var(--space-sm) var(--space-sm) var(--space-md);");
expectMobileRule(css, ".settings-content textarea", "font-size: 16px;");
- expectMobileRule(css, ".settings-section-heading", "padding: var(--space-md) var(--space-sm) var(--space-sm);");
+ expectMobileRule(css, ".settings-section-heading", "padding: var(--space-md) 0 var(--space-sm);");
expectMobileRule(css, ".settings-section-heading", "margin: 0 0 var(--space-sm);");
expectMobileRule(css, ".settings-scope-icon", "margin-right: 0;");
expectMobileRule(css, ".settings-scope-banner", "margin: 0 var(--space-sm) var(--space-xs);");
@@ -663,7 +724,7 @@ describe("SettingsModal mobile adaptations", () => {
it("styles settings scrollbar rules for sidebar and content", () => {
const css = loadAllAppCss();
- expectBaseRule(css, ".settings-navigation", "border-right: var(--btn-border-width) solid var(--border);");
+ expectBaseRule(css, ".settings-navigation", "background: var(--surface);");
expectBaseRule(css, ".settings-search", "border-bottom: var(--btn-border-width) solid var(--border);");
expectBaseRule(css, ".settings-sidebar", "scrollbar-color: var(--border) transparent;");
expectBaseRule(css, ".settings-sidebar", "scrollbar-width: thin;");
@@ -707,7 +768,6 @@ describe("SettingsModal mobile adaptations", () => {
expect(hideToggle.getAttribute("aria-expanded")).toBe("true");
expect(hideToggle.closest(".settings-mobile-section-picker")).toBe(picker);
expect(document.getElementById("settings-search-row-region")).toBeTruthy();
- expect(picker.nextElementSibling?.classList.contains("settings-search")).toBe(true);
await user.click(getByLabelText("Hide search"));
await waitFor(() => expect(queryByTestId("settings-search-input")).toBeNull());