Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ User-facing changes only. The release workflow publishes the matching
Hidden or Hands only choice, instead of menus behind Customize weapons.
- HUD options with long lists of choices, including HUD crosshair glyphs, show every choice on the
page instead of a drop-down menu.
- Crosshair, Viewmodels and Mods' Casual setup show the same bar while changes aren't in TF2 yet,
with Discard changes and Apply changes. Viewmodels applies straight from that bar, without a
separate review window.
- Comfig shows an update button only when a newer mastercomfig is out, and keeps the comfig.app
customizer next to the comfig-custom folder it makes. Removing the crosshair pack is a button at
the bottom of the Crosshair page instead of a hidden menu, and buttons that repeated a switch or
the sidebar (Open Comfig addons, Open Viewmodels) are gone.
- Launch shows this profile's and Steam's options side by side when they differ, with Use this
profile's options and Use Steam's options.

## [0.2.2] - 2026-10-01

Expand Down
55 changes: 53 additions & 2 deletions apps/desktop/src/ComfigPane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ vi.mock("./hooks/useAppStatus", () => ({ useAppStatus: () => status }));
let dom: JSDOM;
let root: Root;

/** A mastercomfig profile with its base package installed. */
const BASE_FILES = [
{ path: "tf/custom/mastercomfig-base.vpk", sha256: "base", storage: "exclusive" as const },
];

beforeEach(() => {
status.running = false;
status.busy = false;
Expand All @@ -37,7 +42,13 @@ function render(
) {
root.render(
createElement(ComfigPane, {
detail: { id: "profile", name: "Main", layer: "comfig", launchOptions: "", files: [] },
detail: {
id: "profile",
name: "Main",
layer: "comfig",
launchOptions: "",
files: BASE_FILES,
},
state,
onApplyPreset,
onApplyModules: async () => false,
Expand Down Expand Up @@ -94,6 +105,40 @@ describe("ComfigPane workspaces", () => {
);
expect(document.body.textContent).toContain("mastercomfig 9.100.0");
expect(document.body.textContent).toContain("Update available: 9.100.1");
const update = document.querySelector<HTMLButtonElement>('[data-testid="comfig-update"]');
expect(update?.textContent).toBe("Update to 9.100.1");
expect(update?.className).toContain("btn-primary");
});

it("shows no update button when the packages are current, and one place to add comfig-custom", async () => {
await act(async () =>
render(
undefined,
{ ...PREVIEW_COMFIG_STATE, release: { version: "9.100.1", packages: {} } },
{ onCheckRelease: async () => "9.100.1" },
),
);
expect(document.querySelector('[data-testid="comfig-release-status"]')?.textContent).toBe(
"Up to date.",
);
expect(document.querySelector('[data-testid="comfig-update"]')).toBeNull();
expect(document.querySelector('[data-testid="comfig-import"]')?.textContent).toBe(
"Add folder…",
);
expect(document.querySelector('[data-testid="comfig-extras"]')?.className).toContain(
"text-link",
);
});

it("offers installing missing packages first", async () => {
await act(async () =>
render(undefined, PREVIEW_COMFIG_STATE, {
detail: { id: "bare", name: "Bare", layer: "comfig", launchOptions: "", files: [] },
}),
);
const update = document.querySelector<HTMLButtonElement>('[data-testid="comfig-update"]');
expect(update?.textContent).toBe("Install packages");
expect(update?.className).toContain("btn-primary");
});

it("keeps an unknown version explicit and reports a failed check", async () => {
Expand All @@ -120,7 +165,13 @@ describe("ComfigPane workspaces", () => {
);
await act(async () =>
render(undefined, PREVIEW_COMFIG_STATE, {
detail: { id: "second", name: "Second", layer: "comfig", launchOptions: "", files: [] },
detail: {
id: "second",
name: "Second",
layer: "comfig",
launchOptions: "",
files: BASE_FILES,
},
onCheckRelease: async () => "9.100.1",
}),
);
Expand Down
141 changes: 78 additions & 63 deletions apps/desktop/src/ComfigPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import {
type ComfigUiState,
canUseTransparentViewmodels,
comfigUpdateAvailable,
comfigReleaseView,
hasBaseVpk,
hasComfigCustom,
OFFICIAL_ADDON_DETAILS,
Expand Down Expand Up @@ -170,6 +170,18 @@ export function ComfigPane({
};
}, [supported, detail?.id, releaseKey]);
const checkedRelease = releaseCheck?.key === releaseKey ? releaseCheck : null;
const release = comfigReleaseView({
installed: state.release?.version ?? null,
packagesInstalled: hasBaseVpk(detail?.files.map((file) => file.path) ?? []),
latest: checkedRelease?.latest ?? null,
error: checkedRelease?.error ?? null,
checking:
Boolean(onCheckRelease) &&
checkedRelease !== null &&
!checkedRelease.latest &&
!checkedRelease.error,
checkable: Boolean(onCheckRelease),
});
const installedVersion = state.release?.version;
const paths = detail?.files.map((file) => file.path) ?? [];
const packagesInstalled = hasBaseVpk(paths);
Expand Down Expand Up @@ -347,76 +359,79 @@ export function ComfigPane({
</div>
</PaneSection>

<section className="section" aria-label="Comfig packages">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="min-w-0">
<h2 className="t-section">Packages and extras</h2>
<p className="t-meta mt-1">
{installedVersion
? `mastercomfig ${installedVersion}`
: packagesInstalled
? "mastercomfig version unknown"
: "No mastercomfig packages installed."}
</p>
{supported ? (
<p data-testid="comfig-release-status" className="t-meta mt-1" aria-live="polite">
{checkedRelease?.error
? checkedRelease.error
: checkedRelease?.latest
? installedVersion === checkedRelease.latest
? "Packages are up to date."
: installedVersion
? comfigUpdateAvailable(installedVersion, checkedRelease.latest)
? `Update available: ${checkedRelease.latest}.`
: `Latest release: ${checkedRelease.latest}.`
: `Latest release: ${checkedRelease.latest}. Update packages before adding addons.`
: onCheckRelease
? "Checking for updates…"
: "Updates have not been checked."}
<section className="section" aria-labelledby="comfig-packages-heading">
<h2 id="comfig-packages-heading" className="t-section">
Packages
</h2>
<div className="mt-2">
<div className="row flex-wrap" data-testid="comfig-release">
<div className="min-w-0">
<p className="t-row">
{installedVersion
? `mastercomfig ${installedVersion}`
: packagesInstalled
? "mastercomfig"
: "mastercomfig isn't installed"}
</p>
{supported ? (
<p className="t-meta mt-0.5 flex flex-wrap items-baseline gap-x-2">
<span data-testid="comfig-release-status" aria-live="polite">
{release.status}
</span>
{onCheckRelease && !release.checking ? (
<button
type="button"
data-testid="comfig-check-release"
className="text-link"
onClick={() => setCheckAttempt((value) => value + 1)}
>
Check again
</button>
) : null}
</p>
) : null}
</div>
{release.action ? (
<button
type="button"
data-testid="comfig-update"
disabled={locked}
onClick={onUpdatePackages}
className={`btn ${release.action.primary ? "btn-primary" : "btn-ghost"}`}
>
{busy ? "Working…" : release.action.label}
</button>
) : null}
</div>

<div className="pane-actions">
<button
type="button"
data-testid="comfig-update"
disabled={locked}
onClick={onUpdatePackages}
className="btn btn-primary"
>
{busy ? "Working…" : "Update packages"}
</button>
{supported && onCheckRelease ? (
<div className="row flex-wrap" data-testid="comfig-custom">
<div className="min-w-0">
<p className="t-row">comfig-custom folder</p>
<p className="t-meta mt-0.5">
{customImported
? "Added to this profile."
: "Extra settings made on comfig.app come as this folder."}
</p>
</div>
<div className="pane-actions">
<button
type="button"
data-testid="comfig-extras"
onClick={() => void openEmbeddedPage("comfig-extras")}
className="text-link"
>
Open comfig.app
<ArrowSquareOut size={13} aria-hidden="true" />
</button>
<button
type="button"
data-testid="comfig-import"
disabled={locked}
onClick={onImportCustom}
className="btn btn-ghost"
disabled={
checkedRelease !== null && !checkedRelease.latest && !checkedRelease.error
}
onClick={() => setCheckAttempt((value) => value + 1)}
>
Check for updates
{customImported ? "Replace folder…" : "Add folder…"}
</button>
) : null}
<button
type="button"
data-testid="comfig-import"
disabled={locked}
onClick={onImportCustom}
className="btn btn-ghost"
>
{customImported ? "Replace comfig-custom…" : "Import comfig-custom…"}
</button>
<button
type="button"
data-testid="comfig-extras"
onClick={() => void openEmbeddedPage("comfig-extras")}
className="btn btn-ghost"
>
Open extras
<ArrowSquareOut size={13} />
</button>
</div>
</div>
</div>
</section>
Expand Down
Loading
Loading