From 6e3075fae5d273619e7f50b901570cdc6d8aed3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 23 Aug 2026 23:44:35 +0200 Subject: [PATCH] v0.4.3 --- BUILD_REPORT.md | 64 +- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 23 +- README.md | 8 +- docs/PROJECT_SPEC.md | 6 +- .../gree_controller/manifest.json | 2 +- scripts/README.md | 2 +- src/db.rs | 16 - src/queries.rs | 19 - web/index.html | 16 +- web/styles.css | 742 ++++++------------ web/sw.js | 2 +- 13 files changed, 313 insertions(+), 591 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 138dd1f..4749628 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,49 +1,49 @@ -# GREE Controller v0.4.2 - build and validation report +# GREE Controller v0.4.3 - build and validation report ## Scope -Version 0.4.2 expands climate history and removes the Node/Tailwind build pipeline. +Version 0.4.3 restores the classic rounded GREE Controller UI while keeping the neutral light/dark palette and green accents introduced in the previous release. It also removes three Rust dead-code warnings reported by the LXC release build. -### History changes +## UI changes -- Added a dedicated `zone_readings` SQLite table. -- History samples include GREE indoor temperature, optional Home Assistant room temperature, calculated control temperature, effective profile target, device setpoint, Home Assistant outdoor temperature, power, HVAC mode, fan speed, demand, control source and active preset. -- Zone history is sampled no faster than every 15 seconds (or the configured device poll interval when longer) to avoid unnecessary SQLite growth. -- Added `GET /api/history` for one zone or all zones. -- History UI now supports a detailed per-zone view and common multi-zone comparison charts. -- Existing `readings` data and tables remain intact; the new table is created automatically on startup. +- Restored the classic large rounded cards and panels. +- Restored circular thermostat +/- and power controls. +- Restored compact desktop navigation and five-item mobile bottom navigation. +- Kept the v0.4.2 climate-history functionality and multi-series charts. +- Kept neutral dark/light surfaces. +- Dark accent: `#3ecf8e`. +- Light accent: `#24b47e`. +- UI is plain committed CSS in `web/styles.css`; no frontend package manager or CSS build step is used. +- Theme-aware scrollbars remain enabled and compact horizontal control scrollbars remain hidden. -### Web asset changes +## Rust warning cleanup -- `web/styles.css` is now the only CSS source used by the application. -- Removed `package.json`, `tailwind.config.js`, `web/tailwind.input.css` and `scripts/build-web.sh`. -- Removed all Node/npm/Tailwind build hooks from `dev.sh`, `install.sh` and `update.sh`. -- The CSS remains fully local and is embedded in the Rust binary with `include_str!`. +Removed code that was superseded by bucketed zone history: -## Validation performed +- `Db::list_zone_readings` +- `queries::LIST_ZONE_READINGS_BY_ZONE` +- `queries::LIST_ZONE_READINGS_ALL` -Passed in the packaging environment: +The active history path is `Db::list_zone_history` with the bucketed SQL queries. -- JavaScript syntax: `node --check web/app.js` and `web/sw.js`. -- JSON syntax for EN/PL language files and the PWA manifest. -- EN/PL translation-key parity (300 keys each). -- Shell syntax for all scripts in `scripts/`. -- Python syntax for the Home Assistant migration generator. -- HTML parsing and CSS brace-balance checks. -- SQLite schema creation plus insert/throttle/read tests for `zone_readings`. -- SQL centralization check: new SQL statements are in `src/queries.rs`. -- No Node/npm/Tailwind build files remain in the release tree. +## Validation performed in packaging environment -## Rust compilation +- HTML structure sanity checks. +- JavaScript syntax check with Node's parser only; Node is not part of the application/runtime. +- JSON validation for language files and HA manifest. +- Shell syntax checks for `scripts/*.sh`. +- Python syntax check for migration helper. +- Verified no `list_zone_readings` or `LIST_ZONE_READINGS_*` references remain. +- Verified no frontend build/package files are required. +- ZIP integrity test and SHA-256 manifest generation. -The packaging environment does not contain Cargo/Rust, so a full Rust compile could not be executed here. The LXC installer/updater still runs `cargo test --all-targets` before replacing the installed binary, unless `--skip-tests` is explicitly used. +A Rust toolchain is not available in the packaging environment, so the authoritative Rust tests and warning check run on the LXC through `scripts/update.sh` before installation. -## Recommended LXC update +Expected LXC validation: ```bash -unzip gree-controller-v0.4.2.zip -cd gree-controller -sudo ./scripts/update.sh +cargo test --all-targets +cargo build --release ``` -The updater preserves `/etc/gree-controller.env` and `/var/lib/gree-controller/gree-controller.db` and rolls back automatically if the new service fails its health check. +The three warnings supplied from v0.4.2 have been removed at source rather than suppressed. diff --git a/Cargo.lock b/Cargo.lock index a6b4c4e..a5a9db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.4.2" +version = "0.4.3" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index a72bc4f..87a88c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.4.2" +version = "0.4.3" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index a935c65..bd7992b 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,15 +1,15 @@ da945a594f47b2b0c326cc0275b10a23cb509f545a1841ba9c57f00086e553ff ./.env.example 17780b6416a709efae772c37fb1151a35e51e6e8e6f428566d2f3eb308a4c58d ./.gitignore -c7adcd3ac665fe6a769519a2eb61dd96a494146c05708cab557238620e25368d ./BUILD_REPORT.md -963046cc499a783fd381006051513c0f7138097f793e7d2e8c44417192dc4b99 ./Cargo.toml +d8981625e50f5df8633f17c82fd24f8f0ee1f7fdc85feda77d82aa910fcaa214 ./BUILD_REPORT.md +f0a46de96da26d647d40de85670473502457e8527d219e4a4b94f12faec653bd ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -53c95efcdd17d4d21aefbed2154d6eef3697d3e9757a11fcce72948693cfb84b ./README.md +c24b1043c8e362bb1478fb7512d324c30b20b17658a34aaabc6ea99c34e6c69b ./README.md a4fa9bfee9735ed8ed95ea31456e0cce503d82502ae3f550108ffca51b0f0c3d ./build.rs 0fb1606ecf67d7c73c53c4a9011d7ece4f2aa3b678eefeb446c4af5c28f91295 ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md 7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md 10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md -ccfc7bc12d2b7e3b8d85c395e2eb37c991c2c3a3f901759cc0d2b5b0e4304b82 ./docs/PROJECT_SPEC.md +8fa2f24618026d9d5c39343c0a6a402c6329c384d76df63297ba026dcea11ee0 ./docs/PROJECT_SPEC.md 33214270b96ac4c64e3db41c11e54158792b4a87ee5668c651bad571766b591a ./home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py e0cf725c9f84be51cdbd83a5ab12b2b8288f90b36623cb4671cefcbf04378504 ./home-assistant/custom_components/gree_controller/api.py @@ -18,14 +18,15 @@ fde31b8e020fd36be2d9d9b1e554254f5da8a1cc593240ebb8d78820154dd790 ./home-assista e1821b74859bc40773a6ee39e6ccc9650980b62af50d6426b46cdb3e3a90d200 ./home-assistant/custom_components/gree_controller/const.py 2d27d7cb67c53e819b99a0302cd0bd339c27e3e2dfc85e07d30c31f505f740b5 ./home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py -ec45d55405c18631a66dc02baa924011dcc42d1e51bf5e209677ec6ce7a8282b ./home-assistant/custom_components/gree_controller/manifest.json +e361220da4193b6c02f06718aa6240b6af54f1c6330916dd66f0ea5ea8f26542 ./home-assistant/custom_components/gree_controller/manifest.json 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md c871974574a31e6c90e3572c6cae73fd014a9b2dffd486936bcf1a93727eb95d ./lang/en.json 27e7edd19b2010e6ab71ccb397cf94e99e390d9caca3eac83000d558d871b990 ./lang/pl.json -896c553a9e2a6de6ce6f69ae50b3eb78c0803bbc9573eb841440d98955eec919 ./scripts/README.md +bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md +896caae04468f743d85d011638a0683253d8271c0db8365aea9bcdc646795cf1 ./scripts/__pycache__/generate_ha_migration.cpython-313.pyc 3640e34a1ed5a93be3e96610848b16b8e748d5ca95975ac7e95d38cbf32695a8 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh dbb92ddc27b8724faf709983e4feecd3f188052b2cba39daba96d4bc16914df5 ./scripts/dev.sh @@ -38,7 +39,7 @@ b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smok b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh 9620b0d32a24ec8111b46f34f4502565dfc601896ef92ba9be74fc25a4bd020f ./src/api.rs 5138f9762540da497360667bd93e5e7274515a37baf3ca3c19c38b23a57c9e2b ./src/config.rs -c68ec99a0dd26d3a49a9d14974af3bdca106aaebaf737a9d76a811850d7ecb53 ./src/db.rs +53f4d02b6e7259525c45766a06002f2094ce51bc0b6ec1a04b2fe039cd8714f7 ./src/db.rs 3f5ffc1ff1fabab5952506d26349f37c23ea270e1351f0e8579a920c1bbc0f84 ./src/engine.rs ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs 584a5dad5d52cafa0ab9d0883c501c7bc5d3126f515ace98a261a78a6d714c3a ./src/home_assistant.rs @@ -47,12 +48,12 @@ ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs 7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs b66ae7c0186b6cc3939d922b0a83f763fd670662c170c97cebca8235d99f663c ./src/protocol/gree.rs a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs -a27f64422712934b6d550a7dc6e520ff9f89099ecbcab2594169f0bd97052318 ./src/queries.rs +6ca5aea6efd4e8c1c9ec0ed81289c0f964a3f82117859439d62750c250a31822 ./src/queries.rs 3bd56018114a7199082e9e9d973107b73cb8259693a80743f4022364cda62095 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 7b2f7054ab62d33fe9f43c9cab5056b3de2e4c3196e168f8669a640f5bf2d77b ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -07a9405ad577d56fda37c51ad0d8f4ff7bff176727ccea45eb52cc8be23af8e5 ./web/index.html +c5d82744adbd17e00b21f48abf279f3362345ac12f7d0fb769f21cb97b7c0b41 ./web/index.html 6db5f01ef1284c6de175c411fb7014a7b312436bc5964ee8086ed94650047b8c ./web/manifest.webmanifest -b2889d002d1c345b554a69279081754ed4b984bb98431819a9f384f79b297b8d ./web/styles.css -ae6af783cd2830e899494eda419b2310496ef75301b88d8ef6fa5ad5ca1e886b ./web/sw.js +973c283e1c69c2c050ca863c6f190429280d06d6fc6b6c93648c4678076aeeb1 ./web/styles.css +187af1d3b5c6e26c06a7b3b87c31fa0abe296343f56d4fe167fbd8377b0ee02e ./web/sw.js diff --git a/README.md b/README.md index d4d51cd..ea46c89 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud. -Current version: **0.4.2**. +Current version: **0.4.3**. ## Highlights @@ -39,7 +39,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do On Debian, Ubuntu or an LXC container: ```bash -unzip gree-controller-v0.4.2.zip +unzip gree-controller-v0.4.3.zip cd gree-controller chmod +x scripts/*.sh ./scripts/dev.sh @@ -85,11 +85,11 @@ Appearance selector: - Light, - Dark. -The selected appearance is stored in the `gree_controller_theme` cookie. Version 0.4.2 refreshes the interface with a Supabase-inspired dashboard language: layered neutral surfaces, compact controls, thin borders, small radii, a restrained green brand accent, a desktop sidebar, mobile bottom navigation, and theme-aware scrollbars. The design remains original to GREE Controller and does not copy Supabase branding or application code. +The selected appearance is stored in the `gree_controller_theme` cookie. Version 0.4.3 restores the classic GREE Controller interface: large rounded cards, circular thermostat controls, compact desktop navigation and mobile bottom navigation. It keeps the newer neutral dark/light palette and green accent (`#3ecf8e` in dark mode and `#24b47e` in light mode). ### Static offline CSS -The Supabase/Tailwind-inspired interface is shipped as a normal, committed `web/styles.css` file and embedded directly into the Rust binary. There is no Node.js, npm, Tailwind CLI, CDN or CSS build step. Editing the UI means editing `web/styles.css` and `web/index.html` directly, then rebuilding the Rust application. +The interface is plain static CSS. `web/styles.css` is committed with the project and embedded directly into the Rust binary. There is no frontend package manager, CDN, or CSS build step. Edit `web/styles.css` and `web/index.html` directly, then rebuild the Rust application. ## Connecting a physical GREE device diff --git a/docs/PROJECT_SPEC.md b/docs/PROJECT_SPEC.md index 3136e09..cef6719 100644 --- a/docs/PROJECT_SPEC.md +++ b/docs/PROJECT_SPEC.md @@ -175,11 +175,11 @@ The zone controller supports `device`, `combined`, and `home_assistant` temperat - Every SQLite statement and schema definition is centralized in `src/queries.rs`; database/domain code must not embed SQL strings elsewhere. -## Web UI design system (v0.4.2) +## Web UI design system (v0.4.3) -The embedded UI uses a compact Supabase-inspired dashboard language while retaining GREE Controller branding and information architecture. Light, Dark and System themes are driven by CSS design tokens. The visual system favors neutral layered surfaces, thin borders, 6-8 px radii, compact controls, a restrained green accent, no decorative shadows, a desktop sidebar and mobile bottom navigation. +The embedded UI uses the classic GREE Controller visual language: large rounded surfaces, circular thermostat controls, comfortable spacing, compact desktop navigation and mobile bottom navigation. Light, Dark and System themes use neutral surfaces with a restrained green accent. -The UI uses a Supabase/Tailwind-inspired visual language but ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no Node.js, npm, Tailwind CLI, CDN, or CSS build pipeline. +The UI ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no frontend package manager, CDN, or CSS build pipeline. ### Rich zone history diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index fb8e886..a7705e1 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.4.2", + "version": "0.4.3", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/scripts/README.md b/scripts/README.md index 076fa87..c9d22c6 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -16,4 +16,4 @@ All operator-facing scripts live in this directory. - `configure-gree-network.sh ` — configures a dedicated GREE NIC, automatic subnet broadcast, disables the simulator, and restarts the service. - `network-debug.sh [broadcast-ip]` — prints interface/routing diagnostics and a tcpdump command. -The running controller never uses a CDN or Node.js. `web/styles.css` is a normal static CSS file embedded into the Rust binary; there is no CSS build step. +The running controller uses only the committed `web/styles.css` embedded into the Rust binary. There is no frontend dependency installation, CDN, or CSS build step. diff --git a/src/db.rs b/src/db.rs index 378b73a..1ed8d9b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -270,22 +270,6 @@ impl Db { Ok(changed > 0) } - pub fn list_zone_readings(&self, zone_id: Option<&str>, since: DateTime, limit: u32) -> Result> { - let conn = self.lock()?; - let limit = limit.clamp(1, 20_000) as i64; - let mut rows_out = Vec::new(); - if let Some(zone_id) = zone_id { - let mut stmt = conn.prepare(queries::LIST_ZONE_READINGS_BY_ZONE)?; - let rows = stmt.query_map(params![zone_id, since.to_rfc3339(), limit], Self::map_zone_reading)?; - for row in rows { rows_out.push(row?); } - } else { - let mut stmt = conn.prepare(queries::LIST_ZONE_READINGS_ALL)?; - let rows = stmt.query_map(params![since.to_rfc3339(), limit], Self::map_zone_reading)?; - for row in rows { rows_out.push(row?); } - } - Ok(rows_out) - } - pub fn list_zone_history(&self, zone_id: Option<&str>, since: DateTime, bucket_seconds: i64, limit: u32) -> Result> { let conn = self.lock()?; let bucket_seconds = bucket_seconds.max(1); diff --git a/src/queries.rs b/src/queries.rs index 3f5aa0a..fcf9dcf 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -212,25 +212,6 @@ WHERE NOT EXISTS ( ) "#; -pub const LIST_ZONE_READINGS_BY_ZONE: &str = r#" -SELECT id,zone_id,device_id,timestamp,gree_temperature,external_temperature, - control_temperature,target_temperature,device_setpoint,outdoor_temperature, - power,mode,fan_speed,demand,control_source,active_preset -FROM zone_readings -WHERE zone_id=?1 AND timestamp>=?2 -ORDER BY timestamp ASC -LIMIT ?3 -"#; - -pub const LIST_ZONE_READINGS_ALL: &str = r#" -SELECT id,zone_id,device_id,timestamp,gree_temperature,external_temperature, - control_temperature,target_temperature,device_setpoint,outdoor_temperature, - power,mode,fan_speed,demand,control_source,active_preset -FROM zone_readings -WHERE timestamp>=?1 -ORDER BY timestamp ASC -LIMIT ?2 -"#; pub const LIST_ZONE_HISTORY_BY_ZONE_BUCKETED: &str = r#" SELECT MIN(id),zone_id,MAX(device_id),MIN(timestamp), diff --git a/web/index.html b/web/index.html index 93cf8d7..fdc3ce2 100644 --- a/web/index.html +++ b/web/index.html @@ -34,7 +34,7 @@ - + @@ -155,15 +155,11 @@ diff --git a/web/styles.css b/web/styles.css index f9015c9..0228ff1 100644 --- a/web/styles.css +++ b/web/styles.css @@ -16,6 +16,7 @@ --text-soft: #d4d4d4; --muted: #a1a1aa; --muted-2: #737373; + --muted-strong: #b5b5bd; --accent: #3ecf8e; --accent-strong: #46d99a; --accent-soft: rgba(62, 207, 142, .10); @@ -30,17 +31,13 @@ --teal: #2dd4bf; --purple: #a78bfa; --orange: #fb923c; - --muted-strong: #b5b5bd; --backdrop: rgba(0, 0, 0, .68); --grid: rgba(255, 255, 255, .07); --scroll-thumb: #444444; --scroll-thumb-hover: #575757; - --radius: 8px; - --radius-sm: 6px; - --control-h: 38px; + --radius: 22px; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } - :root[data-theme="light"] { color-scheme: light; --bg: #f8faf9; @@ -59,6 +56,7 @@ --text-soft: #3f3f46; --muted: #71717a; --muted-2: #a1a1aa; + --muted-strong: #52525b; --accent: #24b47e; --accent-strong: #1ea672; --accent-soft: rgba(36, 180, 126, .09); @@ -73,541 +71,303 @@ --teal: #0f9f8f; --purple: #7c3aed; --orange: #c56b21; - --muted-strong: #52525b; --backdrop: rgba(23, 23, 23, .32); --grid: rgba(24, 24, 27, .08); --scroll-thumb: #c8ccca; --scroll-thumb-hover: #aeb3b0; } - * { box-sizing: border-box; } -html { background: var(--bg); } -html, body { min-height: 100%; } -body { - margin: 0; - min-height: 100vh; - color: var(--text); - background: var(--bg); - font-size: 14px; - line-height: 1.45; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; -} -button, input, select, textarea { font: inherit; } +html, body { background: var(--bg); } +body { margin: 0; min-height: 100vh; color: var(--text); } +button, input, select { font: inherit; } button { cursor: pointer; } -button:disabled { opacity: .48; cursor: wait; } -a { color: var(--accent); } -::selection { color: var(--text); background: var(--accent-soft); } - -/* Accessible focus treatment in the same restrained style as the rest of the UI. */ -:where(button, input, select, textarea, summary):focus-visible { - outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent); - outline-offset: 2px; -} - -.topbar { - position: sticky; - top: 0; - z-index: 40; - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - min-height: 54px; - padding: 8px 14px; - border-bottom: 1px solid var(--line); - background: var(--nav-bg); -} +button:disabled { opacity: .5; cursor: wait; } +.topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 1rem; min-height: 72px; padding: 12px max(18px, env(safe-area-inset-left)); border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 94%, transparent); backdrop-filter: blur(14px); } .brand { display: flex; align-items: center; min-width: 0; } .brand strong, .brand small { display: block; } -.brand strong { color: var(--text); font-size: 13px; font-weight: 650; letter-spacing: -.01em; white-space: nowrap; } -.brand small { margin-top: 1px; color: var(--muted); font-size: 10px; } -.brand.large { margin-bottom: 16px; } -.brand.large strong { font-size: 20px; } -.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; } -.toolbar-select { - width: auto; - min-width: 58px; - min-height: 34px; - padding: 6px 28px 6px 9px; - border-radius: var(--radius-sm); - font-size: 12px; -} -.theme-select { min-width: 92px; } - -button { - min-height: 36px; - border: 1px solid var(--line); - border-radius: var(--radius-sm); - padding: 8px 12px; - color: var(--text-soft); - background: var(--surface); - font-size: 13px; - font-weight: 520; - transition: border-color .12s ease, background-color .12s ease, color .12s ease, opacity .12s ease; -} -button:hover { border-color: var(--line-strong); background: var(--surface-2); color: var(--text); } -button:active { background: var(--surface-3); } -.primary { - border-color: color-mix(in srgb, var(--accent) 74%, var(--line)); - color: var(--accent-text); - background: var(--accent); - font-weight: 650; -} -.primary:hover { border-color: var(--accent-strong); color: var(--accent-text); background: var(--accent-strong); } -.secondary { border-color: var(--line); color: var(--text-soft); background: var(--surface); font-weight: 540; } -.secondary:hover { border-color: var(--line-strong); background: var(--surface-2); } +.brand strong { font-size: 15px; letter-spacing: -.01em; white-space: nowrap; } +.brand small { margin-top: 2px; color: var(--muted); font-size: 11px; } +.brand.large { margin-bottom: 1rem; } +.brand.large strong { font-size: 21px; } +.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 7px; } +.toolbar-select { width: auto; min-width: 58px; min-height: 40px; padding: 7px 28px 7px 10px; border-radius: 11px; } +.theme-select { min-width: 94px; } +button { border: 0; border-radius: 13px; padding: 11px 15px; color: var(--text); background: var(--surface-2); transition: transform .15s ease, background .15s ease, opacity .15s ease; } +button:active { transform: scale(.97); } +.primary { background: var(--accent); color: var(--accent-text); font-weight: 800; } +.primary:hover { background: var(--accent-strong); } +.secondary { border: 1px solid var(--line); background: var(--surface-muted); font-weight: 650; } +.secondary:hover { background: var(--surface-2); } .danger { color: var(--danger); } -.danger:hover { border-color: color-mix(in srgb, var(--danger) 38%, var(--line)); background: var(--danger-soft); color: var(--danger); } -.compact { min-height: 34px; padding: 7px 11px; } -.icon-button { - display: inline-grid; - place-items: center; - width: 34px; - min-width: 34px; - height: 34px; - min-height: 34px; - padding: 0; - color: var(--muted); - background: transparent; -} -.icon-button:hover { color: var(--text); background: var(--surface-2); } -.icon-button svg { width: 16px; height: 16px; } - -main { - width: min(1280px, 100%); - margin: 0 auto; - padding: 28px 18px 102px; -} -.view { display: none; } -.view.active { display: block; animation: reveal .12s ease-out; } -@keyframes reveal { from { opacity: .65; transform: translateY(2px); } } - +.compact { padding: 10px 14px; } +.icon-button { width: 40px; height: 40px; padding: 0; font-size: 22px; background: transparent; } +main { width: min(1180px, 100%); margin: 0 auto; padding: 24px 18px 108px; } +.view { display: none; animation: reveal .2s ease; } +.view.active { display: block; } +@keyframes reveal { from { opacity: 0; transform: translateY(5px); } } h1, h2, h3, p { margin-top: 0; } -h1 { margin-bottom: 7px; font-size: clamp(25px, 5vw, 34px); line-height: 1.08; font-weight: 620; letter-spacing: -.035em; } -h2 { margin-bottom: 0; font-size: clamp(20px, 4vw, 25px); line-height: 1.15; font-weight: 610; letter-spacing: -.025em; } -h3 { margin-bottom: 9px; font-size: 15px; font-weight: 620; letter-spacing: -.01em; } -.eyebrow { color: var(--muted); font-size: 10px; font-weight: 650; letter-spacing: .08em; text-transform: uppercase; } -.lead { max-width: 760px; color: var(--muted); font-size: 13px; line-height: 1.6; } - -.hero { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - min-height: 154px; - padding: 24px; - border: 1px solid var(--line); - border-radius: var(--radius); - background: var(--surface); -} -.hero::after { display: none; } -.hero h1 { margin: 4px 0 7px; } -.hero p { margin: 0; color: var(--muted); font-size: 13px; } -.hero-temp { z-index: 1; white-space: nowrap; color: var(--text); font-size: clamp(48px, 9vw, 68px); font-weight: 310; line-height: 1; letter-spacing: -.065em; } -.hero-temp small { margin-left: 4px; color: var(--muted); font-size: .28em; font-weight: 550; letter-spacing: 0; } - -.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 10px 0 28px; } -.metric { padding: 14px 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); } -.metric span { display: block; margin-bottom: 6px; color: var(--muted); font-size: 11px; } -.metric strong { color: var(--text); font-size: 21px; font-weight: 590; letter-spacing: -.025em; } - -.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 14px; margin: 8px 0 14px; } -.section-heading > div { min-width: 0; } -.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); gap: 10px; } +h1 { margin-bottom: 8px; font-size: clamp(28px, 6vw, 44px); letter-spacing: -.045em; } +h2 { margin-bottom: 0; font-size: clamp(23px, 4vw, 31px); letter-spacing: -.035em; } +h3 { margin-bottom: 10px; } +.eyebrow { color: var(--accent); font-size: 11px; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; } +.lead { max-width: 690px; color: var(--muted); line-height: 1.55; } +.hero { position: relative; overflow: hidden; display: flex; justify-content: space-between; align-items: flex-end; gap: 18px; min-height: 210px; padding: clamp(24px, 6vw, 44px); border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line)); border-radius: 30px; background: var(--surface); } +.hero::after { content: ""; position: absolute; right: -80px; bottom: -120px; width: 220px; height: 220px; border: 34px solid color-mix(in srgb, var(--accent) 7%, transparent); border-radius: 50%; } +.hero h1 { margin: 5px 0 8px; } +.hero p { margin: 0; color: var(--muted); } +.hero-temp { z-index: 1; white-space: nowrap; font-size: clamp(48px, 12vw, 88px); font-weight: 250; letter-spacing: -.075em; } +.hero-temp small { margin-left: 4px; color: var(--muted); font-size: .28em; font-weight: 600; letter-spacing: 0; } +.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0 34px; } +.metric { padding: 18px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); } +.metric span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 12px; } +.metric strong { font-size: 24px; letter-spacing: -.03em; } +.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin: 8px 0 18px; } +.device-grid, .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 14px; } .device-card, .panel, .list-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); } -.device-card { overflow: hidden; padding: 16px; } -.device-card.off { opacity: .78; } +.device-card { overflow: hidden; padding: 20px; } +.device-card.off { opacity: .86; } .device-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .device-title { min-width: 0; } -.device-title h3 { overflow: hidden; margin: 0 0 3px; color: var(--text); text-overflow: ellipsis; white-space: nowrap; font-size: 15px; } -.device-title p { overflow: hidden; margin: 0; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.device-title h3 { overflow: hidden; margin: 0 0 4px; text-overflow: ellipsis; white-space: nowrap; font-size: 19px; } +.device-title p { overflow: hidden; margin: 0; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } .status { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); } -.status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--danger); } +.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--danger); } .status.online::before { background: var(--accent); } -.power-button { - display: grid; - place-items: center; - flex: 0 0 auto; - width: 36px; - height: 36px; - min-height: 36px; - padding: 0; - border-radius: var(--radius-sm); - color: var(--muted); - background: var(--surface-2); - font-size: 17px; -} -.power-button.on { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); } - -.temperature-control { display: flex; align-items: center; justify-content: center; gap: 16px; padding: 20px 0 14px; } -.temperature-control button { - display: grid; - place-items: center; - width: 36px; - height: 36px; - min-height: 36px; - padding: 0; - border: 1px solid var(--line); - border-radius: var(--radius-sm); - color: var(--text-soft); - background: var(--surface); - font-size: 18px; -} -.target-temp { min-width: 100px; text-align: center; color: var(--text); font-size: 42px; font-weight: 320; line-height: 1; letter-spacing: -.055em; } -.target-temp small { margin-left: 3px; color: var(--muted); font-size: 13px; font-weight: 540; } -.current-line { display: flex; justify-content: center; gap: 6px; margin: -6px 0 15px; color: var(--muted); font-size: 11px; } -.current-line strong { color: var(--text-soft); font-weight: 560; } - -.mode-row, .fan-row, .preset-row { - display: flex; - gap: 5px; - overflow-x: auto; - padding: 2px 0 8px; - scrollbar-width: none; -} -.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; } -.mode-row button, .fan-row button, .preset-row button { - flex: 1 0 auto; - min-width: 56px; - min-height: 32px; - padding: 6px 9px; - border-color: var(--line); - color: var(--muted); - background: var(--surface-2); - font-size: 11px; - font-weight: 540; -} -.mode-row button.active, .fan-row button.active, .preset-row button.active { - border-color: var(--accent-border); - color: var(--accent); - background: var(--accent-soft); - font-weight: 620; -} -.device-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-top: 2px; } -.device-toggles button { min-height: 32px; padding: 6px 5px; color: var(--muted); background: var(--surface-2); font-size: 10px; } -.device-toggles button.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); } -.card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); } -.card-footer small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.card-menu { display: flex; gap: 3px; } -.card-menu button { min-height: 28px; padding: 5px 7px; border-color: transparent; color: var(--muted); background: transparent; font-size: 10px; } -.card-menu button:hover { border-color: var(--line); color: var(--text); background: var(--surface-2); } - -.list-card { padding: 15px; } +.power-button { display: grid; place-items: center; flex: 0 0 auto; width: 46px; height: 46px; padding: 0; border-radius: 50%; color: var(--muted); font-size: 21px; } +.power-button.on { background: var(--accent); color: var(--accent-text); } +.temperature-control { display: flex; align-items: center; justify-content: center; gap: 22px; padding: 24px 0 17px; } +.temperature-control button { width: 42px; height: 42px; padding: 0; border: 1px solid var(--line); border-radius: 50%; background: transparent; font-size: 22px; } +.target-temp { min-width: 106px; text-align: center; font-size: 48px; font-weight: 300; letter-spacing: -.06em; } +.target-temp small { margin-left: 3px; color: var(--muted); font-size: 15px; font-weight: 650; } +.current-line { display: flex; justify-content: center; gap: 6px; margin: -10px 0 18px; color: var(--muted); font-size: 12px; } +.current-line strong { color: var(--text); } +.mode-row, .fan-row { display: flex; gap: 7px; overflow-x: auto; padding: 2px 0 10px; scrollbar-width: none; } +.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar { display: none; } +.mode-row button, .fan-row button { flex: 1 0 auto; min-width: 58px; padding: 9px 10px; color: var(--muted); background: var(--surface-muted); font-size: 12px; } +.mode-row button.active, .fan-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; } +.device-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 4px; } +.device-toggles button { padding: 9px 5px; color: var(--muted); font-size: 11px; } +.device-toggles button.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); } +.card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); } +.card-footer small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } +.card-menu { display: flex; gap: 5px; } +.card-menu button { padding: 7px 9px; background: transparent; color: var(--muted); font-size: 12px; } +.list-card { padding: 19px; } .list-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } -.list-card h3 { margin: 0 0 4px; color: var(--text); font-size: 15px; } -.list-card p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; } -.badge { display: inline-flex; align-items: center; min-height: 23px; padding: 3px 7px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); background: var(--surface-2); font-size: 10px; font-weight: 550; } -.badge.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); } -.card-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; margin-top: 13px; } -.card-stat { padding: 8px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); text-align: center; } +.list-card h3 { margin: 0 0 5px; font-size: 18px; } +.list-card p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; } +.badge { display: inline-flex; align-items: center; padding: 5px 9px; border-radius: 99px; color: var(--muted); background: var(--surface-muted); font-size: 11px; } +.badge.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); } +.card-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 16px; } +.card-stat { padding: 10px; border-radius: 12px; background: var(--surface-muted); text-align: center; } .card-stat small, .card-stat strong { display: block; } -.card-stat small { margin-bottom: 4px; color: var(--muted); font-size: 9px; } -.card-stat strong { color: var(--text-soft); font-size: 13px; font-weight: 590; } -.empty { grid-column: 1/-1; padding: 34px 22px; border: 1px dashed var(--line-strong); border-radius: var(--radius); color: var(--muted); background: color-mix(in srgb, var(--surface) 50%, transparent); text-align: center; } -.empty strong { display: block; margin-bottom: 6px; color: var(--text); } -.empty.compact { padding: 16px; } - -.panel { padding: 16px; } +.card-stat small { margin-bottom: 5px; color: var(--muted); font-size: 10px; } +.card-stat strong { font-size: 15px; } +.empty { grid-column: 1/-1; padding: 38px 24px; border: 1px dashed var(--line); border-radius: var(--radius); color: var(--muted); text-align: center; } +.empty strong { display: block; margin-bottom: 7px; color: var(--text); } +.panel { padding: 20px; } .chart-panel { overflow: hidden; } -.history-chart-card { margin-top: 12px; } -.chart-toolbar { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px; } -.chart-toolbar select { flex: 1; min-width: 160px; } -.chart-title-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:10px; } -.chart-title-row h3 { margin:0 0 4px; font-size:14px; } -.chart-title-row p { margin:0; color:var(--muted); font-size:11px; } -.history-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; } -.history-stat { min-width:0; padding:10px; border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--surface-2); } -.history-stat small,.history-stat strong,.history-stat span { display:block; } -.history-stat small { color:var(--muted); font-size:10px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } -.history-stat strong { margin-top:4px; color:var(--text); font-size:17px; font-weight:620; } -.history-stat span { margin-top:2px; color:var(--muted-2); font-size:9px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } -.chart-wrap { position: relative; overflow-x: auto; min-height: 310px; scrollbar-width:none; -ms-overflow-style:none; } -.chart-wrap::-webkit-scrollbar { display:none; } -.chart-wrap.compact-chart { min-height:230px; } -#historyTemperatureChart,#historyOperationChart { width:100%; min-width:720px; } -#historyTemperatureChart { height:360px; } -#historyOperationChart { height:260px; } -.legend { display:flex; flex-wrap:wrap; gap:8px 16px; margin-top:9px; color:var(--muted); font-size:10px; } -.legend > span { display:flex; align-items:center; gap:6px; } -.legend-line { width:16px; height:2px; border-radius:999px; background:var(--legend-color,var(--accent)); } - -.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } +.chart-toolbar { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 18px; } +.chart-toolbar select { flex: 1; min-width: 140px; } +.chart-wrap { position: relative; overflow-x: auto; min-height: 310px; } +#historyChart { width: 100%; min-width: 680px; height: 340px; } +.legend { display: flex; gap: 18px; margin-top: 10px; color: var(--muted); font-size: 12px; } +.legend > span { display: flex; align-items: center; gap: 7px; } +.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); } +.dot.target { background: var(--warning); } +.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; } .form-grid h3, .form-grid hr, .form-grid .wide { grid-column: 1/-1; } -.form-grid h3 { margin: 2px 0 0; } -.form-grid hr, .dialog-form hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid var(--line); } -label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 550; } -input, select, textarea { - width: 100%; - min-height: var(--control-h); - border: 1px solid var(--line); - border-radius: var(--radius-sm); - outline: 0; - padding: 8px 10px; - color: var(--text); - background: var(--input-bg); - font-size: 13px; - transition: border-color .12s ease, background-color .12s ease; -} -input:hover, select:hover, textarea:hover { border-color: var(--line-strong); } -input:focus, select:focus, textarea:focus { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); } -input::placeholder, textarea::placeholder { color: var(--muted-2); } -.check { display: flex; grid-auto-flow: column; justify-content: start; align-items: center; gap: 8px; min-height: 36px; } -.check input { width: 16px; min-height: 16px; accent-color: var(--accent); } -.form-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 4px; } -.system-panel { margin-top: 10px; color: var(--muted); font-size: 12px; line-height: 1.7; } -.system-panel strong { color: var(--text-soft); font-weight: 580; } - -.log-list { display: grid; gap: 0; padding: 0; overflow: hidden; } -.log-row { display: grid; grid-template-columns: 78px 150px 1fr; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 11px; } +.form-grid hr, .dialog-form hr { width: 100%; border: 0; border-top: 1px solid var(--line); } +label { display: grid; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 650; } +input, select { width: 100%; min-height: 45px; border: 1px solid var(--line); border-radius: 12px; outline: 0; padding: 10px 12px; color: var(--text); background: var(--input-bg); } +input:focus, select:focus { border-color: var(--accent); outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); outline-offset: 1px; } +.check { display: flex; grid-auto-flow: column; justify-content: start; align-items: center; gap: 9px; min-height: 44px; } +.check input { width: 19px; min-height: 19px; accent-color: var(--accent); } +.form-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 6px; } +.system-panel { margin-top: 14px; color: var(--muted); line-height: 1.7; } +.system-panel strong { color: var(--text); } +.log-list { display: grid; gap: 2px; padding: 8px; } +.log-row { display: grid; grid-template-columns: 78px 150px 1fr; gap: 12px; padding: 11px 12px; border-bottom: 1px solid var(--line); font-size: 12px; } .log-row:last-child { border-bottom: 0; } -.log-row:hover { background: var(--surface-2); } .log-row time, .log-row .kind { color: var(--muted); } .log-row.error .kind { color: var(--danger); } .log-row.warn .kind { color: var(--warning); } - -.bottom-nav { - position: fixed; - z-index: 30; - right: 0; - bottom: 0; - left: 0; - display: grid; - grid-template-columns: repeat(5, 1fr); - padding: 6px max(8px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); - border-top: 1px solid var(--line); - background: var(--nav-bg); -} -.bottom-nav button { - display: grid; - place-items: center; - gap: 3px; - min-height: 50px; - padding: 5px 2px; - border-color: transparent; - border-radius: var(--radius-sm); - color: var(--muted); - background: transparent; - font-size: 10px; -} -.bottom-nav button:hover { border-color: transparent; color: var(--text); background: var(--surface-2); } -.bottom-nav button span { display: grid; place-items: center; width: 20px; height: 20px; color: currentColor; font-size: 16px; line-height: 1; } -.bottom-nav button span svg { width: 18px; height: 18px; stroke-width: 1.6; } -.bottom-nav button b { font-weight: 540; } -.bottom-nav button.active { color: var(--accent); background: var(--accent-soft); } -.desktop-nav-only { display: none !important; } - -/* Dialogs deliberately have no drop shadow: separation comes from border + backdrop. */ -dialog { - width: min(540px, calc(100% - 24px)); - max-height: min(88vh, 760px); - overflow: auto; - border: 1px solid var(--line-strong); - border-radius: 10px; - padding: 0; - color: var(--text); - background: var(--dialog-bg); -} -dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); } -.dialog-form { display: grid; gap: 12px; padding: 17px; } +.bottom-nav { position: fixed; z-index: 30; right: 0; bottom: 0; left: 0; display: grid; grid-template-columns: repeat(5, 1fr); padding: 8px max(8px, env(safe-area-inset-right)) calc(8px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); border-top: 1px solid var(--line); background: color-mix(in srgb, var(--nav-bg) 96%, transparent); backdrop-filter: blur(14px); } +.bottom-nav button { display: grid; place-items: center; gap: 3px; padding: 7px 2px; border-radius: 12px; color: var(--muted); background: transparent; font-size: 10px; } +.bottom-nav button span { font-size: 20px; line-height: 1; } +.bottom-nav button b { font-weight: 650; } +.bottom-nav button.active { color: var(--accent); } +dialog { width: min(540px, calc(100% - 24px)); max-height: min(88vh, 760px); overflow: auto; border: 1px solid var(--line); border-radius: 24px; padding: 0; color: var(--text); background: var(--dialog-bg); } +dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(4px); } +.dialog-form { display: grid; gap: 14px; padding: 20px; } .dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.dialog-head h2 { margin: 0; font-size: 18px; } -.dialog-head button { display: grid; place-items: center; width: 32px; min-width: 32px; height: 32px; min-height: 32px; padding: 0; border-color: transparent; color: var(--muted); background: transparent; font-size: 21px; } -.dialog-head button:hover { border-color: var(--line); color: var(--text); background: var(--surface-2); } -.two { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } -#externalSensorFields { display: grid; gap: 10px; } -.field-note { margin: -3px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; } -.sensor-detail { margin-top: 9px; color: var(--muted); font-size: 11px; line-height: 1.5; } -fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 9px; } -legend { padding: 0 5px; color: var(--muted); font-size: 10px; } +.dialog-head h2 { font-size: 23px; } +.dialog-head button { width: 38px; height: 38px; padding: 0; background: transparent; color: var(--muted); font-size: 25px; } +.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +#externalSensorFields { display: grid; gap: 12px; } +.field-note { margin: -4px 0 0; color: var(--muted); font-size: .8rem; line-height: 1.45; } +.sensor-detail { margin-top: 10px; color: var(--muted); font-size: .8rem; line-height: 1.45; } +fieldset { border: 1px solid var(--line); border-radius: 14px; padding: 10px; } +legend { padding: 0 5px; color: var(--muted); font-size: 11px; } .days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; } -.days label { display: grid; place-items: center; gap: 4px; padding: 6px 2px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); font-size: 9px; } -.days input { width: 14px; min-height: 14px; accent-color: var(--accent); } -.sheet { width: min(500px, calc(100% - 18px)); margin: auto auto 8px; } -.sheet .dialog-head { padding: 15px 15px 4px; } -.menu-list { display: grid; padding: 7px 9px 10px; } -.menu-list button { display: flex; justify-content: space-between; min-height: 42px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; padding: 11px 8px; background: transparent; text-align: left; } -.menu-list button:hover { background: var(--surface-2); } +.days label { display: grid; place-items: center; gap: 5px; padding: 6px 2px; border-radius: 8px; background: var(--surface-muted); font-size: 10px; } +.days input { width: 16px; min-height: 16px; accent-color: var(--accent); } +.sheet { width: min(500px, calc(100% - 18px)); margin: auto auto 10px; } +.sheet .dialog-head { padding: 18px 18px 5px; } +.menu-list { display: grid; padding: 8px 10px 12px; } +.menu-list button { display: flex; justify-content: space-between; border-bottom: 1px solid var(--line); border-radius: 0; padding: 16px 9px; background: transparent; text-align: left; } .menu-list button:last-child { border-bottom: 0; } .auth-dialog { width: min(420px, calc(100% - 30px)); } - -#toast { - position: fixed; - z-index: 100; - right: 16px; - bottom: 82px; - max-width: min(380px, calc(100% - 32px)); - transform: translateY(8px); - opacity: 0; - pointer-events: none; - padding: 10px 12px; - border: 1px solid var(--line-strong); - border-radius: var(--radius); - color: var(--text-soft); - background: var(--surface-2); - transition: .14s ease; - font-size: 12px; -} +#toast { position: fixed; z-index: 100; right: 18px; bottom: 92px; max-width: min(380px, calc(100% - 36px)); transform: translateY(15px); opacity: 0; pointer-events: none; padding: 12px 15px; border: 1px solid var(--line); border-radius: 13px; background: var(--surface-2); transition: .2s ease; font-size: 13px; } #toast.show { transform: translateY(0); opacity: 1; } -#toast.error { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); background: color-mix(in srgb, var(--danger-soft) 42%, var(--surface-2)); } - -.token-manager { display: grid; gap: 10px; } -.token-list { display: grid; gap: 6px; } -.token-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); } -.token-row > div { min-width: 0; display: grid; gap: 2px; } -.token-row strong { color: var(--text); font-size: 12px; font-weight: 590; } -.token-row small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } - -/* Zone quick controls */ -.zone-quick-control { display: grid; gap: 4px; margin-top: 9px; padding: 8px 9px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); } -.temperature-control.compact { padding: 3px 0 6px; gap: 12px; } -.temperature-control.compact button { width: 34px; height: 34px; min-height: 34px; } -.target-temp.compact { min-width: 86px; font-size: 29px; } -.zone-quick-control .mode-row { padding-bottom: 1px; } - -.discovery-name-list { display: grid; gap: 10px; } -.discovery-name-row { display: grid; gap: 7px; padding: 10px 0; border-bottom: 1px solid var(--line); } -.discovery-name-row:last-child { border-bottom: 0; } -.discovery-name-row span { display: grid; gap: 2px; } -.discovery-name-row small { color: var(--muted); overflow-wrap: anywhere; } - -/* Quiet themed scrollbars. No bright native tracks in dark mode. */ -* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; } -*::-webkit-scrollbar { width: 8px; height: 8px; } -*::-webkit-scrollbar-track { background: transparent; } -*::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 999px; background: var(--scroll-thumb); background-clip: padding-box; } -*::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; } - -.house-climate { display: grid; gap: 13px; margin: -12px 0 28px; } -.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; } -.house-climate-head h3 { margin: 2px 0 3px; font-size: 15px; } -.house-climate-head p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.5; } -.outside-pill { flex: 0 0 auto; min-width: 90px; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); text-align: right; } -.outside-pill small, .outside-pill strong { display: block; } -.outside-pill small { color: var(--muted); font-size: 9px; } -.outside-pill strong { margin-top: 2px; color: var(--text); font-size: 16px; font-weight: 590; } -.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } -.house-mode-row button { min-height: 36px; color: var(--muted); background: var(--surface-2); } -.house-mode-row button.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); font-weight: 620; } -.mode-indicator { display: inline-block; width: 6px; height: 6px; margin-right: 6px; border-radius: 999px; vertical-align: 1px; background: var(--muted-2); } -.house-mode-row button[data-value="cool"] .mode-indicator { background: #60a5fa; } -.house-mode-row button[data-value="heat"] .mode-indicator { background: var(--warning); } -.house-mode-row button.active .mode-indicator { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 9%, transparent); } - -.zone-thermostat { overflow: hidden; } -.zone-thermostat.demanding { border-color: var(--accent-border); } -.thermostat-main { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; margin: 13px 0 8px; } -.thermostat-main > div:first-child { text-align: left; } -.thermostat-main > div:last-child { text-align: right; } -.thermostat-main small, .thermostat-main strong { display: block; } -.thermostat-main small { margin-bottom: 3px; color: var(--muted); font-size: 9px; } -.thermostat-main strong { color: var(--text-soft); font-size: 14px; font-weight: 590; } -.zone-mode-row { padding-bottom: 4px; } -.zone-state-line { display: flex; justify-content: space-between; gap: 10px; padding: 7px 1px 0; color: var(--muted); font-size: 10px; } -.zone-state-line span:last-child { text-align: right; } -.dashboard-zones { margin-bottom: 28px; } - -.schedule-template-panel { display: grid; gap: 11px; margin-bottom: 14px; } -.schedule-template-panel h3 { margin-bottom: 3px; } -.template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; } -.profile-editor { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 9px 10px; background: var(--surface-2); } -.profile-editor summary { cursor: pointer; color: var(--text-soft); font-weight: 590; font-size: 12px; } -.profile-editor[open] summary { margin-bottom: 10px; } -.profile-grid { display: grid; grid-template-columns: 90px repeat(3, 1fr); align-items: end; gap: 7px; margin-top: 7px; } -.profile-grid > strong { align-self: center; font-size: 11px; } -.profile-grid label span { font-size: 9px; } -.warning-note { color: var(--warning); } -.automation-hint { margin-bottom: 14px; } -.automation-hint strong { display: block; margin-bottom: 4px; color: var(--text-soft); font-size: 12px; } -.automation-hint p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.55; } - +#toast.error { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); } @media (min-width: 900px) { - .topbar { padding-left: 14px; } - .brand { width: 176px; flex: 0 0 176px; } - .bottom-nav { - top: 54px; - right: auto; - bottom: 0; - left: 0; - align-content: start; - grid-template-columns: 1fr; - width: 190px; - padding: 10px 8px; - border-top: 0; - border-right: 1px solid var(--line); - background: var(--nav-bg); - } - .bottom-nav button { - grid-template-columns: 22px 1fr; - justify-items: start; - place-items: center start; - gap: 8px; - min-height: 38px; - padding: 7px 9px; - text-align: left; - font-size: 12px; - } - .bottom-nav button span { width: 18px; height: 18px; } - .bottom-nav button b { font-size: 12px; } - .bottom-nav button.active { color: var(--text); background: var(--surface-2); } - .bottom-nav button.active span { color: var(--accent); } - .bottom-nav .desktop-nav-only { display: grid !important; } - .bottom-nav button[data-nav="more"] { display: none; } - main { - width: min(1360px, calc(100% - 190px)); - margin: 0 0 0 190px; - padding: 30px 28px 56px; - } - #toast { bottom: 20px; } + .bottom-nav { top: 92px; right: auto; bottom: auto; left: max(16px, calc((100vw - 1380px)/2)); grid-template-columns: 1fr; width: 90px; border: 1px solid var(--line); border-radius: 22px; padding: 9px; background: var(--surface); } + .bottom-nav button { padding: 10px 4px; } + main { padding-left: 122px; padding-bottom: 50px; } } - @media (max-width: 720px) { - .topbar { flex-wrap: wrap; gap: 6px; padding: 8px 10px; } + .topbar { flex-wrap: wrap; } .top-actions { width: 100%; } .top-actions .primary { margin-left: auto; } } - @media (max-width: 620px) { - main { padding: 15px 11px 92px; } - .hero { min-height: 142px; padding: 18px; } - .hero-temp { font-size: 48px; } - .metrics { gap: 6px; } - .metric { padding: 11px 9px; } - .metric strong { font-size: 18px; } + main { padding: 18px 13px 105px; } + .hero { min-height: 190px; padding: 23px; } + .hero-temp { font-size: 52px; } + .metrics { gap: 7px; } + .metric { padding: 14px 10px; } + .metric strong { font-size: 20px; } .section-heading { align-items: center; } - .section-heading button { min-height: 32px; padding: 6px 9px; font-size: 11px; } + .section-heading button { padding: 9px 11px; font-size: 12px; } .form-grid { grid-template-columns: 1fr; } .form-grid h3, .form-grid hr, .form-grid .wide { grid-column: auto; } .log-row { grid-template-columns: 62px 1fr; } .log-row .message { grid-column: 1/-1; } - .theme-select { min-width: 84px; } + .theme-select { min-width: 86px; } +} +@media (max-width: 410px) { + .topbar { padding-inline: 12px; } + .brand strong { font-size: 13px; } + .hero { align-items: start; flex-direction: column; } + .hero-temp { align-self: flex-end; margin-top: -15px; } + .two { grid-template-columns: 1fr; } + .toolbar-select { min-width: 54px; } + .theme-select { min-width: 82px; } +} + +.token-manager { display: grid; gap: 12px; } +.token-list { display: grid; gap: 8px; } +.token-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-muted); } +.token-row > div { min-width: 0; display: grid; gap: 3px; } +.token-row strong { color: var(--text); font-size: 13px; } +.token-row small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } +.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } +.empty.compact { padding: 18px; } +@media (max-width: 620px) { .token-row { align-items: stretch; flex-direction: column; } .token-row button { width: 100%; } - .house-climate { margin-top: -18px; } +} + +/* Zone quick controls */ +.zone-quick-control { display: grid; gap: 4px; margin-top: 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-muted); } +.temperature-control.compact { padding: 4px 0 7px; gap: 16px; } +.temperature-control.compact button { width: 38px; height: 38px; } +.target-temp.compact { min-width: 92px; font-size: 32px; } +.zone-quick-control .mode-row { padding-bottom: 2px; } + +.discovery-name-list { display:grid; gap:12px; } +.discovery-name-row { display:grid; gap:8px; padding:12px 0; border-bottom:1px solid var(--border); } +.discovery-name-row:last-child { border-bottom:0; } +.discovery-name-row span { display:grid; gap:2px; } +.discovery-name-row small { color:var(--muted); overflow-wrap:anywhere; } + +/* Themed scrollbars: visible where scrolling is useful, but visually quiet. */ +* { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted); } +*::-webkit-scrollbar { width: 9px; height: 9px; } +*::-webkit-scrollbar-track { background: var(--surface-muted); border-radius: 99px; } +*::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 38%, var(--surface-2)); border: 2px solid var(--surface-muted); border-radius: 99px; } +*::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--accent) 58%, var(--surface-2)); } +.mode-row, .fan-row, .preset-row { scrollbar-width: none; } +.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; } + +.house-climate { display: grid; gap: 16px; margin: -15px 0 34px; } +.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } +.house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; } +.house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; } +.outside-pill { flex: 0 0 auto; min-width: 94px; padding: 10px 13px; border-radius: 14px; background: var(--surface-muted); text-align: right; } +.outside-pill small, .outside-pill strong { display: block; } +.outside-pill small { color: var(--muted); font-size: 10px; } +.outside-pill strong { margin-top: 3px; font-size: 18px; } +.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); } +.house-mode-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; } + +.zone-thermostat { overflow: hidden; } +.zone-thermostat.demanding { border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); } +.thermostat-main { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; margin: 15px 0 9px; } +.thermostat-main > div:first-child { text-align: left; } +.thermostat-main > div:last-child { text-align: right; } +.thermostat-main small, .thermostat-main strong { display: block; } +.thermostat-main small { margin-bottom: 3px; color: var(--muted); font-size: 10px; } +.thermostat-main strong { font-size: 16px; } +.preset-row { display: flex; gap: 6px; overflow-x: auto; padding: 3px 0 8px; } +.preset-row button { flex: 1 0 auto; min-height: 38px; padding: 8px 11px; color: var(--muted); background: var(--surface-muted); font-size: 12px; } +.preset-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; } +.zone-mode-row { padding-bottom: 5px; } +.zone-state-line { display: flex; justify-content: space-between; gap: 10px; padding: 8px 1px 1px; color: var(--muted); font-size: 11px; } +.zone-state-line span:last-child { text-align: right; } +.dashboard-zones { margin-bottom: 34px; } + +.schedule-template-panel { display: grid; gap: 13px; margin-bottom: 18px; } +.schedule-template-panel h3 { margin-bottom: 4px; } +.template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; } +.profile-editor { border: 1px solid var(--line); border-radius: 14px; padding: 11px 12px; background: var(--surface-muted); } +.profile-editor summary { cursor: pointer; color: var(--text); font-weight: 700; font-size: 13px; } +.profile-editor[open] summary { margin-bottom: 12px; } +.profile-grid { display: grid; grid-template-columns: 90px repeat(3, 1fr); align-items: end; gap: 8px; margin-top: 8px; } +.profile-grid > strong { align-self: center; font-size: 12px; } +.profile-grid label span { font-size: 10px; } +.warning-note { color: var(--warning); } + +@media (max-width: 620px) { + .house-climate { margin-top: -20px; } .house-climate-head { align-items: flex-start; } - .house-climate-head p { max-width: 230px; } + .house-climate-head p { max-width: 220px; } .thermostat-main { grid-template-columns: 1fr auto 1fr; gap: 4px; } - .thermostat-main .temperature-control.compact { gap: 7px; } - .thermostat-main .target-temp.compact { min-width: 70px; font-size: 26px; } + .thermostat-main .temperature-control.compact { gap: 9px; } + .thermostat-main .target-temp.compact { min-width: 75px; font-size: 28px; } .profile-grid { grid-template-columns: 1fr 1fr; } - .profile-grid > strong { grid-column: 1/-1; margin-top: 3px; } + .profile-grid > strong { grid-column: 1/-1; margin-top: 4px; } .zone-state-line { flex-direction: column; gap: 2px; } .zone-state-line span:last-child { text-align: left; } } -@media (max-width: 410px) { - .brand strong { font-size: 12px; } - .hero { align-items: flex-start; flex-direction: column; } - .hero-temp { align-self: flex-end; margin-top: -12px; } - .two { grid-template-columns: 1fr; } - .toolbar-select { min-width: 52px; } - .theme-select { min-width: 78px; } -} +.automation-hint { margin-bottom: 18px; } +.automation-hint strong { display: block; margin-bottom: 5px; } +.automation-hint p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.5; } -@media (max-width: 720px) { - .history-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } - .chart-toolbar { display:grid; grid-template-columns:1fr 1fr; } - .chart-toolbar button { grid-column:1/-1; } +/* Rich climate history (v0.4.2+) using the classic rounded UI. */ +.history-chart-card { margin-top: 14px; } +.chart-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; } +.chart-title-row h3 { margin: 0 0 4px; font-size: 17px; } +.chart-title-row p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.45; } +.history-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; } +.history-stat { min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-muted); } +.history-stat small, .history-stat strong, .history-stat span { display: block; } +.history-stat small { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } +.history-stat strong { margin-top: 4px; color: var(--text); font-size: 18px; font-weight: 700; } +.history-stat span { overflow: hidden; margin-top: 2px; color: var(--muted-2); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } +.chart-wrap { scrollbar-width: none; -ms-overflow-style: none; } +.chart-wrap::-webkit-scrollbar { display: none; } +.chart-wrap.compact-chart { min-height: 230px; } +#historyTemperatureChart, #historyOperationChart { width: 100%; min-width: 720px; } +#historyTemperatureChart { height: 360px; } +#historyOperationChart { height: 260px; } +.legend { flex-wrap: wrap; gap: 9px 18px; } +.legend-line { width: 18px; height: 3px; border-radius: 999px; background: var(--legend-color, var(--accent)); } +@media (max-width: 620px) { + .history-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .chart-toolbar { display: grid; grid-template-columns: 1fr 1fr; } + .chart-toolbar button { grid-column: 1 / -1; } } diff --git a/web/sw.js b/web/sw.js index 7d156ab..0df4aa6 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'gree-controller-v042'; +const CACHE = 'gree-controller-v043'; const ASSETS = ['/', '/styles.css', '/app.js', '/favicon.svg', '/manifest.webmanifest', '/lang/index.json', '/lang/en.json']; self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting()))); self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));