From 1ac259e814839e4e40aee5e69de2fe35304958a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Tue, 22 Sep 2026 11:10:07 +0300 Subject: [PATCH 1/6] Take weather to Android, iOS and macOS Weather was the richest example that already spanned more than one target -- live forecasts, city management, scrollable rails -- but it ran only on the board, the web simulator and Android. It is also the one app whose layout makes a good cross-platform reference, so the gaps were worth closing. Its stylesheets are authored for the 410x502 board at ratio 1.5, which is 273 logical px wide, so it declares that as its design width and every phone and desktop target scales it from there. Two rules earn their place behind a tall viewport: the forecast's 68px cap keeps it from crowding the hero on the board but leaves a dead band on a phone, and the backdrops were rendered at panel size, so they need to stretch rather than letterbox. The board's logical height is 335, so neither rule ever reaches it. The `fit` attribute on the backdrop images had to go: it is recorded as an inline style, and inline styles replay after class rules, so it would have overridden the media-scoped object-fit without a trace. The CSS already says contain, so the board sees no change. Display.setBrightness and setFlushConfig stay. They are ESP32 tuning and inert setters elsewhere; only the comment needed to say so. Co-Authored-By: Claude Opus 5 (1M context) --- apps/weather/components/WeatherBackground.css | 12 +++++++++++ apps/weather/components/WeatherBackground.tsx | 21 +++++++++++-------- apps/weather/components/WeatherForecast.css | 9 ++++++++ apps/weather/index.tsx | 12 ++++++----- apps/weather/macos.json | 10 +++++++++ apps/weather/package.json | 5 ++++- docs/EXAMPLE-CATALOG.md | 1 + 7 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 apps/weather/macos.json diff --git a/apps/weather/components/WeatherBackground.css b/apps/weather/components/WeatherBackground.css index 092ac6a..9a483b3 100644 --- a/apps/weather/components/WeatherBackground.css +++ b/apps/weather/components/WeatherBackground.css @@ -21,3 +21,15 @@ .weather-bg-img.is-hidden { opacity: 0; } + +/* Taller viewports than the 410x502 panel the backdrops were rendered for: the + phone and desktop targets. `contain` would letterbox a panel-shaped image + against the shell fill, so stretch it instead — it is a soft gradient, and + `fill` is the one object-fit every shell renders the same way (macOS maps + `cover` to proportional-fit, the Android view stretches regardless). + The board's logical height is 502/1.5 = 335, so this never applies there. */ +@media (min-height: 400px) { + .weather-bg-img { + object-fit: fill; + } +} diff --git a/apps/weather/components/WeatherBackground.tsx b/apps/weather/components/WeatherBackground.tsx index 39b4afb..4383258 100644 --- a/apps/weather/components/WeatherBackground.tsx +++ b/apps/weather/components/WeatherBackground.tsx @@ -5,22 +5,25 @@ import { weather } from '../stores/WeatherStore' // captured from the gea engine itself (WASM simulator), so they are pixel-identical // to the old CSS `.weather-shell` gradient — same radial glow, 158deg ramp, and // dither. The win: re-rasterizing this backdrop when content changes is a 1:1 -// decoded-image blit (the images are exactly panel-sized) instead of recomputing a -// 3-layer linear+radial gradient per pixel (~114ms, the bulk of a city switch). -// Inactive layers stay decoded (display:none) so switching visuals is instant — -// same pattern as WeatherVisual's hero art. Visual mapping is clear, cloud/fog, -// rain and snow; the root shell itself stays static. +// decoded-image blit (on the 410x502 panel the images are exactly panel-sized) +// instead of recomputing a 3-layer linear+radial gradient per pixel (~114ms, the +// bulk of a city switch). Inactive layers stay decoded (display:none) so switching +// visuals is instant — same pattern as WeatherVisual's hero art. Visual mapping is +// clear, cloud/fog, rain and snow; the root shell itself stays static. +// +// Fit comes from WeatherBackground.css, NOT a `fit` attribute: the attribute is an +// INLINE style, and inline styles replay after class rules, so it would override the +// taller-than-the-panel rule the phone/desktop targets need. export function WeatherBackground() { return (
- + - - + +
) } diff --git a/apps/weather/components/WeatherForecast.css b/apps/weather/components/WeatherForecast.css index 9dc9eb2..065658e 100644 --- a/apps/weather/components/WeatherForecast.css +++ b/apps/weather/components/WeatherForecast.css @@ -8,6 +8,15 @@ padding: 4px 0 0; } +/* The 68px cap keeps the forecast from crowding the hero on the 410x502 panel + (logical height 335). A phone or a desktop window is much taller, so let the + rails take the slack instead of leaving a dead band under them. */ +@media (min-height: 400px) { + .forecast { + max-height: none; + } +} + .section-head { align-items: center; display: flex; diff --git a/apps/weather/index.tsx b/apps/weather/index.tsx index 74c178f..7f0ac48 100644 --- a/apps/weather/index.tsx +++ b/apps/weather/index.tsx @@ -2,11 +2,13 @@ import { Display, mount } from '@geastack/core' import { App } from './components/App' import { weather } from './stores/WeatherStore' -// Weather needs WiFi for live forecasts. esp_wifi_init requires a chunk of -// internal DRAM (task stack + driver state) that the default 2-deep flush -// pipeline (~56 rows) leaves too little of — WiFi init fails ESP_ERR_NO_MEM and -// fetch() can never connect. A 36-row 2-deep pipeline frees ~32 KB internal so -// WiFi comes up; the smaller flush chunk is a fine trade for this app. +// ESP32-only tuning; both calls are inert setters on the phone and desktop +// targets (they just store the value, no panel to drive). Weather needs WiFi for +// live forecasts, and esp_wifi_init requires a chunk of internal DRAM (task stack +// + driver state) that the default 2-deep flush pipeline (~56 rows) leaves too +// little of — WiFi init fails ESP_ERR_NO_MEM and fetch() can never connect. A +// 36-row 2-deep pipeline frees ~32 KB internal so WiFi comes up; the smaller +// flush chunk is a fine trade for this app. Display.setBrightness(50) Display.setFlushConfig({ rows: 36, depth: 2 }) diff --git a/apps/weather/macos.json b/apps/weather/macos.json new file mode 100644 index 0000000..db55e6e --- /dev/null +++ b/apps/weather/macos.json @@ -0,0 +1,10 @@ +{ + "window": { + "title": "Weather", + "width": 410, + "height": 760, + "minWidth": 273, + "minHeight": 335, + "appearance": "dark" + } +} diff --git a/apps/weather/package.json b/apps/weather/package.json index 4b07ed4..91e49b4 100644 --- a/apps/weather/package.json +++ b/apps/weather/package.json @@ -22,11 +22,14 @@ "name": "Weather", "entry": "index.tsx", "runtime": "gea", + "designWidth": 273, "targets": { "web": true, "esp32": true, "geaos": false, - "android": true + "android": true, + "ios": true, + "macos": true }, "icons": { "32": "icons/icon-32.png", diff --git a/docs/EXAMPLE-CATALOG.md b/docs/EXAMPLE-CATALOG.md index 93fc6e2..55973ef 100644 --- a/docs/EXAMPLE-CATALOG.md +++ b/docs/EXAMPLE-CATALOG.md @@ -14,6 +14,7 @@ tests, and marketing proof. | Games | `sky-hop`, `sky-hop-jsx`, `tic-tac-toe`, `tilt-breakout`, `button-tetris` | Interaction, simple game loops, collision/physics, deterministic logic. | | Device features | `camera-showcase`, `camera-studio`, `voice-notes`, `hid-clicker`, `weather`, `maps` | Target capabilities such as camera, audio, HID, network, and map assets. | | Apple/native experiments | `notes-jsx`, `notes-native`, `ios-device-showcase`, `ios-metal-*`, `ios-native-showcase` | Apple target and native renderer experiments. | +| Cross-platform reference | `weather` | One source across embedded, phone and desktop: `esp32`, `android`, `ios`, `macos`, `web`. Scales its fixed-px layout through `gea.designWidth`. | | Windows/native experiments | `notes-jsx`, `notes-windows` | Windows target: the JSX notes app through the Win32 renderer, and the same app written against `@geastack/windows/Controls`. | | Reactive experiments | `reactive-counter`, `reactive-child-probe`, `reactive-nested-probe`, `reactive-tic-tac-toe` | Store/reactivity experiments, often hidden from launchers. | From 7e973b34319276e1c7560909c518f874abfbaff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Tue, 22 Sep 2026 12:04:31 +0300 Subject: [PATCH 2/6] Stop working around the native renderers in weather's stylesheets Two rules were added to make weather look acceptable on the phone and desktop targets, and both were the wrong shape: they bent the app to fit the renderers instead of leaving the CSS correct. `object-fit: fill` was chosen for the backdrops purely because it is the one value all three backends happen to agree on -- macOS maps `cover` to a proportional fit and the Android view stretches whatever it is given. The forecast rule was worse than cosmetic. `max-height: none` is not a length the engine understands: it reaches parseLengthForNode, strtod stops at the `n`, and the property lands as 0 -- which clampSize treats as a hard maximum because 0 is not the kUnset sentinel. With `min-height: 0` there is no floor to restore it, so `.forecast` computed to height 0 and iOS, which hides a view of zero size along with its whole subtree, dropped the section head and both rails. The board never saw it because the rule sits behind a tall-viewport query its 335 logical px never match. The dead band under the rails on a tall screen is what the reference rendering shows too, so it stays until the design says otherwise in CSS. Co-Authored-By: Claude Opus 5 (1M context) --- apps/weather/components/WeatherBackground.css | 12 ------------ apps/weather/components/WeatherForecast.css | 9 --------- 2 files changed, 21 deletions(-) diff --git a/apps/weather/components/WeatherBackground.css b/apps/weather/components/WeatherBackground.css index 9a483b3..092ac6a 100644 --- a/apps/weather/components/WeatherBackground.css +++ b/apps/weather/components/WeatherBackground.css @@ -21,15 +21,3 @@ .weather-bg-img.is-hidden { opacity: 0; } - -/* Taller viewports than the 410x502 panel the backdrops were rendered for: the - phone and desktop targets. `contain` would letterbox a panel-shaped image - against the shell fill, so stretch it instead — it is a soft gradient, and - `fill` is the one object-fit every shell renders the same way (macOS maps - `cover` to proportional-fit, the Android view stretches regardless). - The board's logical height is 502/1.5 = 335, so this never applies there. */ -@media (min-height: 400px) { - .weather-bg-img { - object-fit: fill; - } -} diff --git a/apps/weather/components/WeatherForecast.css b/apps/weather/components/WeatherForecast.css index 065658e..9dc9eb2 100644 --- a/apps/weather/components/WeatherForecast.css +++ b/apps/weather/components/WeatherForecast.css @@ -8,15 +8,6 @@ padding: 4px 0 0; } -/* The 68px cap keeps the forecast from crowding the hero on the 410x502 panel - (logical height 335). A phone or a desktop window is much taller, so let the - rails take the slack instead of leaving a dead band under them. */ -@media (min-height: 400px) { - .forecast { - max-height: none; - } -} - .section-head { align-items: center; display: flex; From 07e9c35cee9be6ba8567e3e33d72ec56c121cc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Tue, 22 Sep 2026 18:27:15 +0300 Subject: [PATCH 3/6] Size the weather macOS window at exactly 2x its design width designWidth is 273, so a 410px window gave a device pixel ratio of 1.502 and every CSS length was rounded to an integer on its own. A 1px padding became 2px on each side, so a 26px cell had 35px of content where CSS gives it 36 -- enough to wrap "10 PM" onto a second line and make the row overflow. 546 is 2 x 273, so every length in the stylesheet lands on a whole device pixel and the rounding divergence disappears on this target. Co-Authored-By: Claude Opus 5 (1M context) --- apps/weather/macos.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/weather/macos.json b/apps/weather/macos.json index db55e6e..4a44534 100644 --- a/apps/weather/macos.json +++ b/apps/weather/macos.json @@ -1,10 +1,10 @@ { "window": { "title": "Weather", - "width": 410, - "height": 760, + "width": 546, + "height": 780, "minWidth": 273, - "minHeight": 335, + "minHeight": 390, "appearance": "dark" } } From 6e487f5a75b69a10c6e4a1b62e2445c8c9e61cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Sun, 27 Sep 2026 15:28:53 +0300 Subject: [PATCH 4/6] fix(weather): carry the year in weatherTimeSortKey The key was month*744 + day*24 + hour, so on 31 December the hourly entries for January sorted below the current hour: the scan in applyApiWeather walked past the end of the array and every hourly slot came out blank. Scaling the year needs a multiplier above the 9695 intra-year maximum (12*744 + 31*24 + 23), which 8928 is not, so the key is nested rather than flat. Checked monotonic across 14,784 sampled points from 2020 to 2030. Co-Authored-By: Claude Opus 5 (1M context) --- apps/weather/stores/WeatherStore.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/weather/stores/WeatherStore.ts b/apps/weather/stores/WeatherStore.ts index dbee455..020e45b 100644 --- a/apps/weather/stores/WeatherStore.ts +++ b/apps/weather/stores/WeatherStore.ts @@ -67,8 +67,14 @@ function hourLabel(hour: number): string { return '' + (hour - 12) + ' PM' } +// Carries the year, so a forecast that crosses New Year still sorts forward: +// without it January's key falls below December's and the hourly scan below +// runs off the end of the array, blanking every slot. function weatherTimeSortKey(time: string): number { - return Number(time.substring(5, 7)) * 744 + Number(time.substring(8, 10)) * 24 + Number(time.substring(11, 13)) + const year = Number(time.substring(0, 4)) + const month = Number(time.substring(5, 7)) + const day = Number(time.substring(8, 10)) + return ((year * 12 + month) * 31 + day) * 24 + Number(time.substring(11, 13)) } function weekdayFromISO(time: string): string { From eb4661c9cf002bf6beaa242188982e1ef99a3341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Sun, 27 Sep 2026 15:29:00 +0300 Subject: [PATCH 5/6] feat(weather): enable the Windows target No @geastack/windows dependency: weather runs the `gea` runtime, and notes-jsx already targets Windows the same way without declaring it. Only the windows-native runtime needs that package. Built and run against the Win32 target: the backdrop, the translucent chip surfaces and the forecast rail below the metric strip all render, and localStorage survives a restart. The `.screen` padding is not applied (--safe-x: 17px, content still sits at x=0), which is a pre-existing renderer gap, not a target-enablement one. Co-Authored-By: Claude Opus 5 (1M context) --- apps/weather/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/weather/package.json b/apps/weather/package.json index 91e49b4..ef4608f 100644 --- a/apps/weather/package.json +++ b/apps/weather/package.json @@ -29,7 +29,8 @@ "geaos": false, "android": true, "ios": true, - "macos": true + "macos": true, + "windows": true }, "icons": { "32": "icons/icon-32.png", From d96991ec15b2e1b2f3d4cce86841a3ca451f71f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kt=C3=BCrk=20Acar?= Date: Sun, 27 Sep 2026 22:54:00 +0300 Subject: [PATCH 6/6] weather: cover backdrop, symmetric city chips The backdrop art is the 410x502 panel itself; object-fit: cover fills a taller desktop window instead of leaving bands above and below it. City chips drop the extra bottom padding so their labels sit centred. Co-Authored-By: Claude Fable 5.1 --- apps/weather/components/CityRail.css | 2 +- apps/weather/components/WeatherBackground.css | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/weather/components/CityRail.css b/apps/weather/components/CityRail.css index 1b8daf5..0b2d569 100644 --- a/apps/weather/components/CityRail.css +++ b/apps/weather/components/CityRail.css @@ -31,7 +31,7 @@ gap: 4px; justify-content: flex-start; min-width: 0; - padding: 5px 6px 10px; + padding: 5px 6px; scroll-snap-align: start; } diff --git a/apps/weather/components/WeatherBackground.css b/apps/weather/components/WeatherBackground.css index 092ac6a..029336b 100644 --- a/apps/weather/components/WeatherBackground.css +++ b/apps/weather/components/WeatherBackground.css @@ -15,7 +15,11 @@ top: 0; width: 100%; height: 100%; - object-fit: contain; + /* The art is the 410x502 panel itself (the shell's exact aspect), so on that + shell cover and contain agree. On a taller viewport -- a resizable desktop + window -- cover fills it edge to edge instead of leaving solid bands above + and below the gradient. */ + object-fit: cover; } .weather-bg-img.is-hidden {