feat(FN-2974): style inline version update control and improve touch target

The merge adds an inline version refresh control to the Settings modal with improved touch targets for better mobile usability, replacing a separate duplicate-updates check button. Changes span the SettingsModal component, its CSS styling, and new test coverage for the control behavior.

Fusion-Task-Id: FN-2974
This commit is contained in:
Fusion
2026-04-30 05:08:50 -07:00
committed by gsxdsm
parent 620049cf15
commit 21c78cae2d
3 changed files with 104 additions and 53 deletions

View File

@@ -14,8 +14,7 @@
/* Keep the GitHub Star and Help buttons at matching heights regardless of
their differing icon sizes (provider icon 16px vs. HelpCircle 13px). */
.settings-header-actions > .settings-github-star-btn,
.settings-header-actions > .btn,
.settings-header-actions > .settings-update-btn {
.settings-header-actions > .btn {
height: 26px;
box-sizing: border-box;
}
@@ -23,8 +22,7 @@
@media (max-width: 768px) {
/* Keep settings header actions compact; override global mobile
`.btn-icon { min-height/min-width: 36px; }` inflation. */
.settings-header-actions > .btn-icon,
.settings-header-actions > .settings-update-btn {
.settings-header-actions > .btn-icon {
min-height: 26px;
min-width: 26px;
}
@@ -137,11 +135,42 @@
gap: var(--space-sm);
}
.settings-update-btn {
.settings-version-check-btn {
--settings-inline-touch-target: calc(var(--space-lg) + var(--space-lg) + var(--space-xs));
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin: 0;
min-height: var(--settings-inline-touch-target);
padding: var(--space-xs) var(--space-sm);
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: opacity var(--transition-fast), box-shadow var(--transition-fast);
}
.settings-version-check-btn:hover:not(:disabled) {
opacity: 0.85;
}
.settings-version-check-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
border-radius: var(--radius-sm);
}
.settings-update-btn svg.spinning {
.settings-version-check-btn:disabled {
cursor: default;
opacity: 0.7;
}
.settings-version-check-btn svg {
color: var(--text-muted);
}
.settings-version-check-btn svg.spinning {
animation: settings-update-spin 1s linear infinite;
}

View File

@@ -1897,42 +1897,11 @@ export function SettingsModal({
<option value="weekly">Weekly</option>
</select>
<small>
Controls how often the dashboard re-fetches the npm registry. The
&quot;Check Now&quot; button below always triggers an immediate fetch
regardless of this setting.
Controls how often the dashboard re-fetches the npm registry.
Use the version + refresh control in the header to trigger an
immediate check at any time.
</small>
</div>
<div className="form-group">
<label>Check Now</label>
<div className="settings-update-check">
<button
type="button"
className="btn btn-sm settings-update-btn"
onClick={() => {
void handleCheckForUpdates();
}}
disabled={updateCheckLoading}
>
<RefreshCw className={updateCheckLoading ? "spinning" : undefined} size={14} />
{updateCheckLoading ? "Checking…" : "Check for updates"}
</button>
{updateCheckResult && (
<span
aria-live="polite"
className={`settings-update-result ${
updateCheckResult.error
? "settings-update-result--error"
: updateCheckResult.updateAvailable
? "settings-update-result--available"
: "settings-update-result--up-to-date"
}`}
>
{renderUpdateCheckResultContent()}
</span>
)}
</div>
<small>Manually check for the latest version right now.</small>
</div>
</>
);
}
@@ -4675,7 +4644,21 @@ export function SettingsModal({
<div className="settings-modal-heading">
<h3>Settings</h3>
<div className="settings-update-check">
{appVersion && <p className="settings-modal-version">Version {appVersion}</p>}
{appVersion && (
<button
type="button"
className="settings-version-check-btn"
onClick={() => {
void handleCheckForUpdates();
}}
disabled={updateCheckLoading}
aria-label="Check for updates"
title="Check for updates"
>
<span className="settings-modal-version">Version {appVersion}</span>
<RefreshCw size={12} className={updateCheckLoading ? "spinning" : undefined} />
</button>
)}
{updateCheckResult && (
<span
aria-live="polite"
@@ -4693,18 +4676,6 @@ export function SettingsModal({
</div>
</div>
<div className="settings-header-actions">
<button
type="button"
className="btn btn-icon btn-sm settings-update-btn"
onClick={() => {
void handleCheckForUpdates();
}}
disabled={updateCheckLoading}
aria-label="Check for updates"
title="Check for updates"
>
<RefreshCw size={14} className={updateCheckLoading ? "spinning" : undefined} />
</button>
{form.showGitHubStarButton !== false && (
<a
href="https://github.com/Runfusion/Fusion"

View File

@@ -515,6 +515,8 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
expect(screen.getByRole("button", { name: "Check for updates" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Check Now" })).not.toBeInTheDocument();
expect(screen.queryByText("Manually check for the latest version right now.")).not.toBeInTheDocument();
});
it("clicking check for updates shows up-to-date message", async () => {
@@ -576,6 +578,55 @@ describe("SettingsModal", () => {
expect(button).not.toBeDisabled();
});
});
it("clicking version text area triggers update check", async () => {
mockCheckForUpdates.mockResolvedValueOnce({
currentVersion: "1.2.3",
latestVersion: "1.2.3",
updateAvailable: false,
});
renderModal();
await waitForSettingsModalReady();
const inlineButton = screen.getByRole("button", { name: "Check for updates" });
expect(within(inlineButton).getByText("Version 1.2.3")).toBeInTheDocument();
await userEvent.click(within(inlineButton).getByText("Version 1.2.3"));
await waitFor(() => {
expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
});
});
it("refresh icon has spinning class while loading", async () => {
let resolveCheck: ((result: UpdateCheckResponse) => void) | undefined;
const pendingCheck = new Promise<UpdateCheckResponse>((resolve) => {
resolveCheck = resolve;
});
mockCheckForUpdates.mockReturnValueOnce(pendingCheck);
renderModal();
await waitForSettingsModalReady();
const button = screen.getByRole("button", { name: "Check for updates" });
fireEvent.click(button);
await waitFor(() => {
const spinningIcon = button.querySelector(".spinning");
expect(spinningIcon).not.toBeNull();
});
resolveCheck?.({
currentVersion: "1.2.3",
latestVersion: "1.2.3",
updateAvailable: false,
});
await waitFor(() => {
expect(button.querySelector(".spinning")).toBeNull();
});
});
});
describe("settings export filename", () => {