From 2b3a6cbeae303e355ab51374b069f1e0a7bd8af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Tue, 22 Sep 2026 11:05:15 +0300 Subject: [PATCH] Add a gea.designWidth manifest field An app whose stylesheets are written in fixed px was laid out against one particular width -- the panel it was drawn for. Targets that render at an arbitrary size (a phone screen, a resizable desktop window) have had no way to learn that width, so they either pinned the app to a build-time ratio or followed the display density, and a layout drawn for a small panel ended up either magnified or stranded in a corner. designWidth records the logical CSS width the app was authored against. A shell divides its real surface width by it to get the device pixel ratio, and since the engine multiplies every CSS px by that ratio (ui::cssPixelLength), the design scales to whatever it lands on with no second set of stylesheets. This is deliberately not cssDevicePixelRatio: that one is a FIXED ratio a board declares about itself, while this is a property of the app, and the ratio it implies is different on every screen. Absent, each target keeps its own default, so an app that declares nothing builds exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- src/manifest.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/manifest.mjs b/src/manifest.mjs index fba2262..5a6eceb 100644 --- a/src/manifest.mjs +++ b/src/manifest.mjs @@ -120,6 +120,9 @@ export function validateApp(app) { if (Number.isNaN(app.cssDevicePixelRatio)) { errors.push(`gea.cssDevicePixelRatio must be a positive number: ${JSON.stringify(app.manifest?.cssDevicePixelRatio)}`) } + if (Number.isNaN(app.designWidth)) { + errors.push(`gea.designWidth must be a positive number: ${JSON.stringify(app.manifest?.designWidth)}`) + } return errors } @@ -225,6 +228,7 @@ export function appSummary(ctx, app) { compilerPlugins: app.compilerPlugins, defines: app.defines, cssDevicePixelRatio: app.cssDevicePixelRatio, + designWidth: app.designWidth, targetConfig: app.targetConfig, launcher: app.launcher } @@ -362,6 +366,21 @@ export function normalizeCssDevicePixelRatio(raw) { return typeof ratio === 'number' && Number.isFinite(ratio) && ratio > 0 ? ratio : NaN } +// The logical CSS width the app's stylesheets were authored against. Unlike +// cssDevicePixelRatio -- a FIXED ratio a board declares -- this is the app's own +// design width, and the targets that render at an arbitrary size (phone screens, +// a resizable desktop window) derive their device pixel ratio from it: +// dpr = viewportWidth / designWidth. That scales a fixed-px layout uniformly to +// whatever panel it lands on, because the engine multiplies every CSS px by the +// ratio (ui::cssPixelLength). Absent, each target keeps its own default ratio, so +// an app that declares nothing builds exactly as before. NaN marks a malformed +// value for validateApp to report; 0 means "not declared". +export function normalizeDesignWidth(raw) { + if (raw === undefined || raw === null || raw === '') return 0 + const width = typeof raw === 'string' ? Number(raw) : raw + return typeof width === 'number' && Number.isFinite(width) && width > 0 ? width : NaN +} + export function normalizeDefines(raw) { const entries = [] if (Array.isArray(raw)) { @@ -479,6 +498,7 @@ export function normalizeApp(root, packageJson) { compilerPlugins: normalizeStringList(gea.compilerPlugins).map(normalizeManifestRelativePath), defines: normalizeDefines(gea.defines), cssDevicePixelRatio: normalizeCssDevicePixelRatio(gea.cssDevicePixelRatio), + designWidth: normalizeDesignWidth(gea.designWidth), targetConfig: normalizeTargetConfig(gea.targets), launcher: normalizeLauncher(gea.launcher), manifest: gea,