From bd499de1fb47d45ab0ac64c04f920724787c3685 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 6 Nov 2026 11:18:33 +0100 Subject: [PATCH] feat(ui): add UI scale option to appearance settings Co-Authored-By: Claude Opus 5.5 --- resources/css/app.css | 17 ++++++++++ resources/js/canvas/deploy/state.svelte.js | 2 +- resources/js/canvas/deploy/styles.js | 6 ++-- resources/views/components/reicon.blade.php | 1 + .../views/components/theme-controls.blade.php | 34 +++++++++++++++++++ .../views/components/top-user-menu.blade.php | 2 +- resources/views/layouts/base.blade.php | 8 +++++ 7 files changed, 65 insertions(+), 5 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 96d9bd8f32..e7c80340a1 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -482,6 +482,18 @@ select, /* * Base styles */ +/* UI scale from the Appearance page. It scales rem sizes only, because CSS zoom breaks the + mouse positions in xterm and the canvas. Phones keep 100% so inputs stay at 16px. */ +@media (min-width: 768px) { + html[data-ui-scale="90"] { + font-size: 90%; + } + + html[data-ui-scale="80"] { + font-size: 80%; + } +} + html, body { @apply w-full min-h-full bg-neutral-50 dark:bg-app dark:text-fg-dim; @@ -2783,6 +2795,11 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica box-shadow: var(--shadow-dropdown); } +/* Geist centers lowercase letters 1px below the line box center. Move the icons to match. */ +.top-user-menu-panel .listbox-option svg { + translate: 0 1px; +} + @media (max-width: 767px) { /* The mobile top bar's backdrop filter makes it the containing block for fixed descendants. Keep this menu anchored below its trigger instead diff --git a/resources/js/canvas/deploy/state.svelte.js b/resources/js/canvas/deploy/state.svelte.js index ffb74cd4ff..6ed15f6e50 100644 --- a/resources/js/canvas/deploy/state.svelte.js +++ b/resources/js/canvas/deploy/state.svelte.js @@ -9,7 +9,7 @@ import { resourceHref, save, sleep, targetFrom, TYPES, uid, uniqueLabels, withDeploy, withoutSecrets, withTarget, } from '../../deploy-canvas.js'; -export const PORT_Y = 26; // header centre (3.25rem / 2) +export const PORT_Y = 26; // header centre (.cv-node-head is 52px; px, so the UI scale does not move the ports) const CARD_W = 296; const emptyRun = () => ({ open: false, title: 'Deploy', sub: '', steps: [], panes: [], active: null, defaultHref: '', openHref: '', showOpen: false }); diff --git a/resources/js/canvas/deploy/styles.js b/resources/js/canvas/deploy/styles.js index 554b2935ca..9946a106e8 100644 --- a/resources/js/canvas/deploy/styles.js +++ b/resources/js/canvas/deploy/styles.js @@ -143,7 +143,7 @@ const CSS = ` .cv-root :is(input, textarea, .cv-logs) { user-select: text; } .cv-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } .cv-deploy-now { width: 100%; } - .cv-node-head { display: flex; align-items: center; gap: 0.625rem; height: 3.25rem; padding: 0 0.5rem 0 0.75rem; cursor: grab; user-select: none; } + .cv-node-head { display: flex; align-items: center; gap: 0.625rem; height: 52px; padding: 0 0.5rem 0 0.75rem; cursor: grab; user-select: none; } .cv-node-head i svg { width: 1rem; height: 1rem; } .cv-node-head div { display: grid; min-width: 0; margin-right: auto; } .cv-node-head b { overflow: hidden; font-size: 13.5px; font-weight: 600; line-height: 1.125rem; text-overflow: ellipsis; white-space: nowrap; } @@ -182,7 +182,7 @@ const CSS = ` /* Ports: filled dots on the header line, like the environment canvas. */ .cv-port { - position: absolute; top: calc(1.625rem - 0.375rem); width: 0.75rem; height: 0.75rem; border-radius: 50%; + position: absolute; top: calc(26px - 0.375rem); width: 0.75rem; height: 0.75rem; border-radius: 50%; border: 2px solid var(--cv-card); background: light-dark(#a3a3a3, #737373); z-index: 2; box-shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); transition: transform 120ms ease, background-color 120ms ease; @@ -248,7 +248,7 @@ const CSS = ` /* Result pill after Deploy: the live address(es). */ .cv-result { - position: absolute; display: flex; align-items: center; gap: 0.25rem; height: 2.375rem; padding: 0 0.25rem 0 0.875rem; + position: absolute; display: flex; align-items: center; gap: 0.25rem; height: 38px; padding: 0 0.25rem 0 0.875rem; border: 1px solid color-mix(in srgb, #10b981 45%, transparent); border-radius: 999px; background: var(--cv-card); box-shadow: 0 8px 24px -12px rgb(0 0 0 / 0.25); white-space: nowrap; } diff --git a/resources/views/components/reicon.blade.php b/resources/views/components/reicon.blade.php index 1efad534e2..8b5ffca818 100644 --- a/resources/views/components/reicon.blade.php +++ b/resources/views/components/reicon.blade.php @@ -30,6 +30,7 @@ 'profile' => '', 'teams' => '', 'subscription' => '', + 'paintbrush' => '', 'settings' => '', 'admin' => '', 'sponsor' => '', diff --git a/resources/views/components/theme-controls.blade.php b/resources/views/components/theme-controls.blade.php index 7ed7148bbd..c770289db3 100644 --- a/resources/views/components/theme-controls.blade.php +++ b/resources/views/components/theme-controls.blade.php @@ -68,6 +68,40 @@ +
+
+
+

UI scale

+

Choose the size of text and controls. Phones always use 100%.

+
+
+
+ @foreach ([ + ['value' => '80', 'label' => '80%', 'description' => 'Show more content on the screen.', 'size' => 'text-base'], + ['value' => '90', 'label' => '90%', 'description' => 'Smaller text and controls.', 'size' => 'text-lg'], + ['value' => '100', 'label' => '100%', 'description' => 'The default size.', 'size' => 'text-xl'], + ] as $option) + + @endforeach +
+
+
diff --git a/resources/views/components/top-user-menu.blade.php b/resources/views/components/top-user-menu.blade.php index 3f856e6cf6..62ac76131a 100644 --- a/resources/views/components/top-user-menu.blade.php +++ b/resources/views/components/top-user-menu.blade.php @@ -77,7 +77,7 @@ - + Appearance diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index e3aaccbba0..d2b0e86612 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -159,11 +159,13 @@ } document.querySelector('meta[name=theme-color]')?.setAttribute('content', theme.background); }; + window.storedUiScale = () => ['80', '90', '100'].includes(localStorage.uiScale) ? localStorage.uiScale : '100'; window.applyStoredTheme = () => { const root = document.documentElement; const theme = localStorage.theme === 'purple' ? 'custom' : (localStorage.theme || 'dark'); root.dataset.navStyle = localStorage.navStyle === 'floating' ? 'floating' : 'default'; root.dataset.navItems = localStorage.navItems === 'flat' ? 'flat' : '3d'; + root.dataset.uiScale = window.storedUiScale(); localStorage.theme = theme; if (localStorage.themeColor) localStorage.themeColor = window.normalizeThemeColor(localStorage.themeColor); if (theme === 'custom') { @@ -253,6 +255,7 @@ navStyle: localStorage.getItem('navStyle') === 'floating' ? 'floating' : 'default', navItems: localStorage.getItem('navItems') === 'flat' ? 'flat' : '3d', chartTimes: localStorage.getItem('chartTimes') === 'utc' ? 'utc' : 'local', + uiScale: window.storedUiScale(), presets: window.customThemePresets, // The custom theme dialog edits a draft; Save applies it. A null draft is the built-in Dark theme. dialogOpen: false, @@ -341,6 +344,11 @@ localStorage.setItem('navItems', style); document.documentElement.dataset.navItems = style; }, + setUiScale(scale) { + this.uiScale = scale; + localStorage.setItem('uiScale', scale); + document.documentElement.dataset.uiScale = scale; + }, setChartTimes(value) { this.chartTimes = value; localStorage.setItem('chartTimes', value);