Skip to content

feat: add UI size slider to settings#3524

Open
IgnazioDS wants to merge 3 commits intoopenfrontio:mainfrom
IgnazioDS:feat/ui-size-slider
Open

feat: add UI size slider to settings#3524
IgnazioDS wants to merge 3 commits intoopenfrontio:mainfrom
IgnazioDS:feat/ui-size-slider

Conversation

@IgnazioDS
Copy link
Copy Markdown

@IgnazioDS IgnazioDS commented Mar 27, 2026

Resolves #3067

Description:

Adds a UI scale slider to the in-game settings panel so players can adjust interface size from 50% to 200% in 10% increments. The setting is persisted through UserSettings, normalized before storage/application, applied during startup, and exposed with translated settings text.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

IgnazioDS

Add a UI scale slider (50%-200%) to the in-game settings panel,
allowing players to adjust the interface size regardless of their
OS scaling, browser zoom, or screen type (mobile, ultrawide, etc.).

Implementation:
- UserSettings: uiScale()/setUiScale()/applyUiScale() with
  localStorage persistence and clamping to 50-200 range
- SettingsModal: range input slider following the existing volume
  slider pattern, placed after the audio settings
- Main.ts: apply saved scale on page load
- en.json: translation key for the label

Uses CSS zoom on the root element for proper layout reflow.

Closes openfrontio#3067
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 27, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Walkthrough

Adds a persistent UI scale feature: new localization key, UI slider in settings, UserSettings methods for reading/setting/applying a clamped 50–200% scale, document zoom application, startup application during client initialization, and tests for normalization/persistence/zoom.

Changes

Cohort / File(s) Summary
Translations
resources/lang/en.json
Added user_setting.ui_scale_label"UI Scale".
Core settings logic
src/core/game/UserSettings.ts
Added uiScale(), setUiScale(scale) (normalizes: finite, snaps to 10, clamps 50–200, persists) and applyUiScale(scale?) which sets document.documentElement.style.zoom to value/100.
Settings UI
src/client/graphics/layers/SettingsModal.ts
Added range slider row (min=50, max=200, step=10) bound to userSettings.uiScale(), onUiScaleChange handler wired to @input, and right-aligned percentage display.
Startup integration
src/client/Main.ts
Calls userSettings.applyUiScale() during Client.initialize() earlier in startup (after SDK init, before token prefetch).
Tests
tests/core/game/UserSettings.test.ts
Added tests verifying default/normalized/clamped values, persistence to localStorage, and that document.documentElement.style.zoom reflects applied scale.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant UserSettings
    participant Browser

    Client->>UserSettings: applyUiScale()
    activate UserSettings
    UserSettings->>UserSettings: read uiScale() (default 100, normalize)
    UserSettings->>Browser: set document.documentElement.style.zoom = value/100
    deactivate UserSettings
Loading
sequenceDiagram
    participant User
    participant SettingsModal
    participant UserSettings
    participant Storage
    participant Browser

    User->>SettingsModal: adjust range slider
    SettingsModal->>SettingsModal: onUiScaleChange()
    SettingsModal->>UserSettings: setUiScale(scale)
    activate UserSettings
    UserSettings->>UserSettings: normalize/snap/clamp scale
    UserSettings->>Storage: persist settings.uiScale
    UserSettings->>Browser: applyUiScale -> set zoom
    deactivate UserSettings
    SettingsModal->>User: update displayed percentage
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A tiny dial to tweak the view,
From half to double — pick what’s true.
Saved to stay the way you like,
Zooming pages, snug and right. 🎚️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add UI size slider to settings' directly and clearly describes the main change: adding a UI scale slider feature to the settings panel.
Description check ✅ Passed The description is directly related to the changeset, providing context about the UI scale slider feature, issue resolution, and implementation details including persistence and normalization.
Linked Issues check ✅ Passed The pull request successfully implements all coding requirements from issue #3067: adds a UI scale slider (50-200%) to settings, persists the setting, applies it on startup, handles normalization/clamping, and includes translated text.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the UI scale slider feature. No unrelated modifications were introduced outside the feature objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/game/UserSettings.ts`:
- Around line 269-284: Normalize UI scale in one place and ensure both setting
and applying use it: create or use a single normalization step (e.g.,
normalizeUiScale(value?: number)) that ensures the value is finite, defaults to
the stored uiScale(), clamps to [50,200], and snaps to 10% steps (round to
nearest 10), then use that normalized value in setUiScale (before
setFloat/applyUiScale) and in applyUiScale (call normalizeUiScale() instead of
using raw scale or uiScale()), and ensure getFloat/setFloat still receive only
the normalized number for "settings.uiScale".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc210491-5b9a-462b-8791-f52c1212e273

📥 Commits

Reviewing files that changed from the base of the PR and between 217a2c4 and e9bc7e0.

📒 Files selected for processing (4)
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/core/game/UserSettings.ts

Comment thread src/core/game/UserSettings.ts
@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Mar 27, 2026
@github-actions
Copy link
Copy Markdown

This pull request is stale because it has been open for fourteen days with no activity. If you want to keep this pull request open, add a comment or update the branch.

@github-actions github-actions bot added the Stale PRs that haven't been touched for over two weeks. label Apr 16, 2026
@iiamlewis iiamlewis added this to the v31 milestone Apr 16, 2026
@iiamlewis iiamlewis added UI/UX UI/UX changes including assets, menus, QoL, etc. Feature and removed Stale PRs that haven't been touched for over two weeks. labels Apr 16, 2026
@iiamlewis iiamlewis requested a review from a team as a code owner April 16, 2026 13:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/core/game/UserSettings.ts (1)

390-405: ⚠️ Potential issue | 🟡 Minor

Normalize UI scale in one place before read/apply.

Right now only setUiScale clamps. Stored values read by uiScale() and values applied by applyUiScale() can still bypass finite/step normalization, so startup can apply invalid/out-of-range zoom.

Proposed fix
+  private normalizeUiScale(scale: number): number {
+    if (!Number.isFinite(scale)) return 100;
+    const stepped = Math.round(scale / 10) * 10;
+    return Math.max(50, Math.min(200, stepped));
+  }
+
   uiScale(): number {
-    return this.getFloat("settings.uiScale", 100);
+    return this.normalizeUiScale(this.getFloat("settings.uiScale", 100));
   }
 
   setUiScale(scale: number): void {
-    const clamped = Math.max(50, Math.min(200, scale));
-    this.setFloat("settings.uiScale", clamped);
-    this.applyUiScale(clamped);
+    const normalized = this.normalizeUiScale(scale);
+    this.setFloat("settings.uiScale", normalized);
+    this.applyUiScale(normalized);
   }
 
   applyUiScale(scale?: number): void {
-    const value = scale ?? this.uiScale();
+    const value = this.normalizeUiScale(scale ?? this.uiScale());
     if (typeof document !== "undefined") {
       document.documentElement.style.zoom = (value / 100).toString();
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/UserSettings.ts` around lines 390 - 405, Stored and applied UI
scale can be out-of-range or non-finite because only setUiScale clamps; add a
single normalization step and use it everywhere: implement a helper (e.g.,
normalizeUiScale(value?: number): number) that ensures the value is a finite
number, rounds/steps as needed, and clamps to the allowed range (50–200) with
default 100; update uiScale() to return
normalizeUiScale(this.getFloat("settings.uiScale", 100)), update
setUiScale(scale) to store normalizeUiScale(scale) and pass that to
applyUiScale, and update applyUiScale(scale?) to call normalizeUiScale(scale ??
this.uiScale()) before using document.documentElement.style.zoom; reference
these symbols: uiScale, setUiScale, applyUiScale, normalizeUiScale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/graphics/layers/SettingsModal.ts`:
- Around line 288-315: Run Prettier on this file to fix formatting issues
reported by CI; specifically reformat the SettingsModal.ts block that contains
the UI scale range input (references: class SettingsModal, methods/properties
userSettings.uiScale(), onUiScaleChange, translateText) so indentation,
attribute spacing, and JSX/HTML template literal formatting comply with project
Prettier config, then re-stage the updated file and push the commit.

---

Duplicate comments:
In `@src/core/game/UserSettings.ts`:
- Around line 390-405: Stored and applied UI scale can be out-of-range or
non-finite because only setUiScale clamps; add a single normalization step and
use it everywhere: implement a helper (e.g., normalizeUiScale(value?: number):
number) that ensures the value is a finite number, rounds/steps as needed, and
clamps to the allowed range (50–200) with default 100; update uiScale() to
return normalizeUiScale(this.getFloat("settings.uiScale", 100)), update
setUiScale(scale) to store normalizeUiScale(scale) and pass that to
applyUiScale, and update applyUiScale(scale?) to call normalizeUiScale(scale ??
this.uiScale()) before using document.documentElement.style.zoom; reference
these symbols: uiScale, setUiScale, applyUiScale, normalizeUiScale.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b227b8c-7a48-47b4-8704-464a2e2d9284

📥 Commits

Reviewing files that changed from the base of the PR and between e9bc7e0 and 446402e.

📒 Files selected for processing (4)
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/graphics/layers/SettingsModal.ts
  • src/core/game/UserSettings.ts
✅ Files skipped from review due to trivial changes (1)
  • resources/lang/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/Main.ts

Comment thread src/client/graphics/layers/SettingsModal.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/core/game/UserSettings.test.ts (1)

61-71: LGTM — nice non-finite coverage.

Good call covering NaN and +Infinity. Consider adding -Infinity and a negative finite value (e.g. -50) too — -50 is finite so it would bypass the finite guard and rely on the clamp; both paths together make the intent crystal clear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/game/UserSettings.test.ts` around lines 61 - 71, Add two more
assertions to the test for non-finite and negative values: call
userSettings.setUiScale(Number.NEGATIVE_INFINITY) and assert
userSettings.uiScale() === 100, localStorage.getItem("settings.uiScale") ===
"100", and document.documentElement.style.zoom === "1"; then call
userSettings.setUiScale(-50) and assert the clamped value (use
userSettings.uiScale()), the stored string in
localStorage.getItem("settings.uiScale"), and the
document.documentElement.style.zoom reflect the expected clamped/normalized
behavior based on the existing clamp logic.
src/client/graphics/layers/SettingsModal.ts (2)

288-309: UI scale row looks good; minor a11y polish.

The row mirrors the volume slider pattern, which is nice and consistent. Two small suggestions you can take or leave:

  • The <img src=${settingsIcon}> reuses the gear icon already used by the Performance Overlay row below, which can confuse users scanning the list. A dedicated icon (or a different existing one) would help discoverability — one of the stated goals in issue #3067.
  • The slider has no accessible label association. Screen readers will announce "slider, 50 to 200" with no name. Adding aria-label=${translateText("user_setting.ui_scale_label")} on the <input type="range"> would fix that.
♿ Optional fix for a11y
                 <input
                   type="range"
                   min="50"
                   max="200"
                   step="10"
+                  aria-label=${translateText("user_setting.ui_scale_label")}
                   .value=${this.userSettings.uiScale()}
                   `@input`=${this.onUiScaleChange}
                   class="w-full border border-slate-500 rounded-lg"
                 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/SettingsModal.ts` around lines 288 - 309, The UI
scale row reuses the gear icon and lacks an accessible label; update the row in
SettingsModal.ts to use a distinct icon (replace settingsIcon with a more
appropriate icon symbol) so it doesn't duplicate the Performance Overlay icon,
and add an aria-label to the range input (use
translateText("user_setting.ui_scale_label") as the value) alongside the
existing .value and `@input` handler (onUiScaleChange) so screen readers get a
proper name while leaving userSettings.uiScale() logic intact.

196-200: Guard against NaN from the input value.

parseFloat on an empty or malformed value returns NaN. setUiScale already normalizes NaN to 100 on the UserSettings side, so behavior is safe, but that means a stray parse failure silently resets the slider to 100% during a drag. Tiny hardening: fall back to the current value when parsing fails, so the user's slider keeps its position.

♻️ Optional fix
   private onUiScaleChange(event: Event) {
-    const scale = parseFloat((event.target as HTMLInputElement).value);
-    this.userSettings.setUiScale(scale);
+    const raw = parseFloat((event.target as HTMLInputElement).value);
+    const scale = Number.isFinite(raw) ? raw : this.userSettings.uiScale();
+    this.userSettings.setUiScale(scale);
     this.requestUpdate();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/SettingsModal.ts` around lines 196 - 200, The
onUiScaleChange handler parses the input value into scale with parseFloat and
can produce NaN which will cause the UI to jump to the normalized default;
instead validate the parsed value and if NaN use the current setting as a
fallback before calling setUiScale: in onUiScaleChange, after computing scale =
parseFloat(...), if Number.isNaN(scale) replace it with the existing value from
this.userSettings (e.g. this.userSettings.uiScale or
this.userSettings.getUiScale()) and then call
this.userSettings.setUiScale(scale) followed by this.requestUpdate().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/game/UserSettings.test.ts`:
- Around line 1-37: Add explicit vitest imports at the top of the test file to
match project convention: import { vi, describe, it, expect, beforeEach,
afterEach } from "vitest"; this ensures the globals used in the file (vi,
describe, it, expect, beforeEach, afterEach) are explicitly imported. Leave the
existing createMockLocalStorage, the beforeEach/afterEach hooks calling
userSettings.removeCached("settings.uiScale") and
document.documentElement.style.zoom resets unchanged; just add the import line
so the test follows the project's import style and avoids implicit globals.

---

Nitpick comments:
In `@src/client/graphics/layers/SettingsModal.ts`:
- Around line 288-309: The UI scale row reuses the gear icon and lacks an
accessible label; update the row in SettingsModal.ts to use a distinct icon
(replace settingsIcon with a more appropriate icon symbol) so it doesn't
duplicate the Performance Overlay icon, and add an aria-label to the range input
(use translateText("user_setting.ui_scale_label") as the value) alongside the
existing .value and `@input` handler (onUiScaleChange) so screen readers get a
proper name while leaving userSettings.uiScale() logic intact.
- Around line 196-200: The onUiScaleChange handler parses the input value into
scale with parseFloat and can produce NaN which will cause the UI to jump to the
normalized default; instead validate the parsed value and if NaN use the current
setting as a fallback before calling setUiScale: in onUiScaleChange, after
computing scale = parseFloat(...), if Number.isNaN(scale) replace it with the
existing value from this.userSettings (e.g. this.userSettings.uiScale or
this.userSettings.getUiScale()) and then call
this.userSettings.setUiScale(scale) followed by this.requestUpdate().

In `@tests/core/game/UserSettings.test.ts`:
- Around line 61-71: Add two more assertions to the test for non-finite and
negative values: call userSettings.setUiScale(Number.NEGATIVE_INFINITY) and
assert userSettings.uiScale() === 100, localStorage.getItem("settings.uiScale")
=== "100", and document.documentElement.style.zoom === "1"; then call
userSettings.setUiScale(-50) and assert the clamped value (use
userSettings.uiScale()), the stored string in
localStorage.getItem("settings.uiScale"), and the
document.documentElement.style.zoom reflect the expected clamped/normalized
behavior based on the existing clamp logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8432e858-fd3a-4a2c-8984-6a800ec33c8a

📥 Commits

Reviewing files that changed from the base of the PR and between 446402e and 54a5009.

📒 Files selected for processing (3)
  • src/client/graphics/layers/SettingsModal.ts
  • src/core/game/UserSettings.ts
  • tests/core/game/UserSettings.test.ts

Comment on lines +1 to +37
import { UserSettings } from "../../../src/core/game/UserSettings";

function createMockLocalStorage(): Storage {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => {
store[key] = String(value);
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
get length() {
return Object.keys(store).length;
},
key: (index: number) => Object.keys(store)[index] ?? null,
};
}

describe("UserSettings UI scale", () => {
let userSettings: UserSettings;

beforeEach(() => {
vi.stubGlobal("localStorage", createMockLocalStorage());
userSettings = new UserSettings();
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
});

afterEach(() => {
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
vi.unstubAllGlobals();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether vitest globals are enabled and how other tests import helpers.
fd -a 'vitest.config*'
fd -a 'vite.config*'
rg -nP '^\s*import\s+\{[^}]*\b(vi|describe|it|expect)\b' --type=ts -g 'tests/**' | head -n 20
rg -nP 'globals\s*:\s*true' -g 'vitest.config*' -g 'vite.config*'

Repository: openfrontio/OpenFrontIO

Length of output: 2110


🏁 Script executed:

cat -n /home/jailuser/git/vite.config.ts | grep -A 5 -B 5 "globals"

Repository: openfrontio/OpenFrontIO

Length of output: 319


Add explicit vitest imports to match project convention.

The file uses vi, describe, it, expect, beforeEach, afterEach without importing them. While globals: true is enabled in vite.config.ts, every other test file in the project explicitly imports these from "vitest". Keep this test consistent with the rest of the codebase:

Suggested import
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { UserSettings } from "../../../src/core/game/UserSettings";

Also note: UserSettings.cache is a static Map shared across instances. removeCached("settings.uiScale") in beforeEach/afterEach correctly clears it for this key, but watch for the same pattern when testing other keys — easy footgun to miss.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { UserSettings } from "../../../src/core/game/UserSettings";
function createMockLocalStorage(): Storage {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => {
store[key] = String(value);
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
get length() {
return Object.keys(store).length;
},
key: (index: number) => Object.keys(store)[index] ?? null,
};
}
describe("UserSettings UI scale", () => {
let userSettings: UserSettings;
beforeEach(() => {
vi.stubGlobal("localStorage", createMockLocalStorage());
userSettings = new UserSettings();
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
});
afterEach(() => {
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
vi.unstubAllGlobals();
});
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { UserSettings } from "../../../src/core/game/UserSettings";
function createMockLocalStorage(): Storage {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => {
store[key] = String(value);
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
get length() {
return Object.keys(store).length;
},
key: (index: number) => Object.keys(store)[index] ?? null,
};
}
describe("UserSettings UI scale", () => {
let userSettings: UserSettings;
beforeEach(() => {
vi.stubGlobal("localStorage", createMockLocalStorage());
userSettings = new UserSettings();
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
});
afterEach(() => {
userSettings.removeCached("settings.uiScale");
document.documentElement.style.zoom = "";
vi.unstubAllGlobals();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/game/UserSettings.test.ts` around lines 1 - 37, Add explicit
vitest imports at the top of the test file to match project convention: import {
vi, describe, it, expect, beforeEach, afterEach } from "vitest"; this ensures
the globals used in the file (vi, describe, it, expect, beforeEach, afterEach)
are explicitly imported. Leave the existing createMockLocalStorage, the
beforeEach/afterEach hooks calling userSettings.removeCached("settings.uiScale")
and document.documentElement.style.zoom resets unchanged; just add the import
line so the test follows the project's import style and avoids implicit globals.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

Feature: UI size slider

3 participants