Ожидаем показания датчика
diff --git a/Version.toml b/Version.toml new file mode 100644 index 0000000..d684908 --- /dev/null +++ b/Version.toml @@ -0,0 +1,8 @@ +[version] +major = 1 +minor = 2 +patch = 1 + +[build] +date = "2026-09-20" +time = "11:28:15" diff --git a/tools/update_version.py b/tools/update_version.py new file mode 100644 index 0000000..3dc8c69 --- /dev/null +++ b/tools/update_version.py @@ -0,0 +1,64 @@ +from datetime import datetime +from pathlib import Path +import subprocess +import tomllib + + +PROJECT_DIR = Path(__file__).resolve().parent.parent +VERSION_FILE = PROJECT_DIR / "version.toml" + + +def get_git_commit() -> str: + """Возвращает short hash текущего HEAD.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=PROJECT_DIR, + capture_output=True, + text=True, + check=True, + ) + + return result.stdout.strip() + + except (subprocess.CalledProcessError, FileNotFoundError): + return "unknown" + + +def main() -> None: + # Сначала читаем текущий version.toml. + # Нам нужны значения, которые разработчик поменял вручную. + with VERSION_FILE.open("rb") as file: + data = tomllib.load(file) + + major = data["version"]["major"] + minor = data["version"]["minor"] + patch = data["version"]["patch"] + + now = datetime.now() + + content = f"""\ +[version] +major = {major} +minor = {minor} +patch = {patch} + +[build] +date = "{now:%Y-%m-%d}" +time = "{now:%H:%M:%S}" +""" + + VERSION_FILE.write_text( + content, + encoding="utf-8", + ) + + print( + f"Version updated: " + f"{major}.{minor}.{patch} " + f"({now:%Y-%m-%d %H:%M:%S})" + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/web/css/panel.css b/web/css/panel.css index 7bbe5d7..d707627 100644 --- a/web/css/panel.css +++ b/web/css/panel.css @@ -266,7 +266,8 @@ body::after { .co2-card > p { height: 1rem; margin: .2rem 0 .55rem; color: var(--panel-muted); font-size: .58rem; overflow: hidden; } .meter { height: .82rem; border: 1px solid rgba(255,255,255,.8); border-radius: 999px; background: rgba(91,145,181,.18); overflow: hidden; } .meter i { display: block; height: 100%; width: 0; border-radius: inherit; background: linear-gradient(90deg, #08a350, #36dc83); box-shadow: 0 0 18px rgba(18,199,104,.58), inset 0 1px 0 rgba(255,255,255,.64); transition: width .35s ease; } -.meter-labels { display: flex; justify-content: space-between; margin-top: .25rem; color: var(--panel-muted); font-size: .55rem; } +.meter-labels { display: flex; justify-content: space-between; margin-top: .27rem; color: rgba(22,69,108,.78); font-size: .6rem; font-weight: 650; text-shadow: 0 1px 0 rgba(255,255,255,.48); } +:root[data-theme="dark"] .meter-labels { color: rgba(211,235,248,.76); text-shadow: 0 1px 2px rgba(0,0,0,.58); } .filter-line { display: flex; align-items: center; gap: .45rem; margin-top: 1rem; } .filter-meter { flex: 1; } .filter-meter i { background: linear-gradient(90deg, #176dec, #36c1ff); box-shadow: 0 0 18px rgba(31,144,247,.55), inset 0 1px 0 rgba(255,255,255,.58); } @@ -375,27 +376,33 @@ body::after { } body { background-color: #b7d5e3; + background-image: none; +} +:root[data-theme="dark"] body { + background-color: #091722; + background-image: none; +} +body::before { + inset: -1.25rem; background-image: - linear-gradient(118deg, rgba(180,220,239,.16), rgba(238,247,251,.04) 45%, rgba(247,226,204,.1)), + linear-gradient(118deg, rgba(180,220,239,.15), rgba(238,247,251,.025) 45%, rgba(247,226,204,.09)), url("../assets/mountain-lake-light.jpg"); background-position: center; background-size: cover; background-repeat: no-repeat; + filter: blur(6px) saturate(108%); + transform: scale(1.025); + opacity: 1; + pointer-events: none; } -:root[data-theme="dark"] body { - background-color: #091722; +:root[data-theme="dark"] body::before { background-image: linear-gradient(118deg, rgba(2,15,25,.66), rgba(8,27,40,.57) 48%, rgba(20,25,29,.64)), url("../assets/mountain-lake-dark.jpg"); background-position: center; background-size: cover; -} -body::before { - inset: 0; - background: linear-gradient(110deg, rgba(214,242,253,.16), transparent 35%, rgba(255,240,218,.1) 76%, transparent); - filter: none; - transform: none; - opacity: .75; + filter: blur(7px) saturate(106%); + opacity: 1; } body::after { background: @@ -405,7 +412,6 @@ body::after { filter: blur(11px); opacity: .5; } -:root[data-theme="dark"] body::before { background: linear-gradient(115deg, rgba(29,104,142,.1), transparent 40%, rgba(110,76,42,.08)); opacity: .7; } :root[data-theme="dark"] body::after { opacity: .26; } .ambient::before { opacity: .14; filter: blur(76px); } .ambient::after { opacity: .11; filter: blur(82px); } @@ -797,3 +803,70 @@ body::after { .editor-status { width: 100%; } .schedule-edit-button span { display: none; } } + +/* CO2 state drives both the label and the meter, so the visual state cannot + disagree with the air-quality text returned by airQuality(). */ +.co2-card { + --air-color: #7896ac; + --air-glow: rgba(93, 140, 171, .22); + --air-track: rgba(181, 203, 200, .28); +} +.co2-card[data-air-tone="good"] { + --air-color: #1fbd69; + --air-glow: rgba(18, 199, 104, .34); +} +.co2-card[data-air-tone="warn"] { + --air-color: #efa12b; + --air-glow: rgba(244, 161, 42, .35); +} +.co2-card[data-air-tone="bad"] { + --air-color: #e8495c; + --air-glow: rgba(235, 74, 83, .36); +} +.co2-card .co2-meter { + height: .84rem; + border-color: rgba(255,255,255,.82); + background-color: var(--air-track); + background-image: linear-gradient(180deg, rgba(255,255,255,.15), transparent 52%, rgba(0,20,36,.045)); + box-shadow: inset 0 1px 3px rgba(20,61,78,.14), 0 1px 4px rgba(8,43,72,.07); + transition: background-color .65s ease; +} +.co2-card .co2-meter i { + background-color: var(--air-color); + background-image: linear-gradient(180deg, rgba(255,255,255,.28), rgba(255,255,255,.045) 48%, rgba(0,35,20,.1)); + box-shadow: 0 0 8px var(--air-glow), inset 0 1px 0 rgba(255,255,255,.58), inset 0 -1px 2px rgba(0,34,19,.1); + transition: width .35s ease, background-color .65s ease, box-shadow .65s ease; +} +:root[data-theme="dark"] .co2-card .co2-meter { border-color: rgba(205,235,248,.3); box-shadow: inset 0 1px 4px rgba(0,0,0,.27), 0 1px 5px rgba(0,0,0,.12); } +.co2-card .quality-chip.muted { + color: var(--panel-muted); + border-color: var(--tile-line-soft); + background: rgba(199,222,235,.2); + box-shadow: inset 0 1px 0 rgba(255,255,255,.55); +} +.co2-card .quality-chip.good { + color: #076d3d; + border-color: rgba(109,237,167,.65); + background: linear-gradient(145deg, rgba(179,255,214,.58), rgba(92,218,151,.39)); + box-shadow: 0 0 9px rgba(33,203,116,.12), inset 0 1px 0 rgba(255,255,255,.82); +} +.co2-card .quality-chip.warn { + color: #9c5513; + border-color: rgba(244,183,106,.67); + background: linear-gradient(145deg, rgba(255,232,188,.56), rgba(239,168,80,.34)); + box-shadow: 0 0 9px rgba(235,153,53,.11), inset 0 1px 0 rgba(255,255,255,.8); +} +.co2-card .quality-chip.bad { + color: #a62f3d; + border-color: rgba(242,135,147,.62); + background: linear-gradient(145deg, rgba(255,209,216,.53), rgba(225,105,120,.3)); + box-shadow: 0 0 9px rgba(224,83,101,.12), inset 0 1px 0 rgba(255,255,255,.78); +} +:root[data-theme="dark"] .co2-card { --air-track: rgba(8,28,39,.38); } +:root[data-theme="dark"] .co2-card[data-air-tone="good"] { --air-glow: rgba(30,224,128,.3); } +:root[data-theme="dark"] .co2-card[data-air-tone="warn"] { --air-glow: rgba(255,176,49,.3); } +:root[data-theme="dark"] .co2-card[data-air-tone="bad"] { --air-glow: rgba(255,82,102,.32); } +:root[data-theme="dark"] .co2-card .quality-chip.good { color: #b9ffda; background: linear-gradient(145deg, rgba(45,186,113,.35), rgba(11,101,60,.3)); } +:root[data-theme="dark"] .co2-card .quality-chip.warn { color: #ffd59e; background: linear-gradient(145deg, rgba(176,115,30,.34), rgba(91,58,15,.3)); } +:root[data-theme="dark"] .co2-card .quality-chip.bad { color: #ffc0c8; background: linear-gradient(145deg, rgba(174,58,75,.34), rgba(91,25,37,.31)); } +:root[data-theme="dark"] .co2-card .quality-chip.muted { color: var(--panel-muted); background: rgba(42,70,88,.25); } diff --git a/web/css/widget.css b/web/css/widget.css index b0e0ce3..55d019f 100644 --- a/web/css/widget.css +++ b/web/css/widget.css @@ -39,39 +39,37 @@ body { justify-content: center; color: var(--widget-ink); background-color: #b7d5e3; - background-image: - linear-gradient(118deg, rgba(180,220,239,.15), rgba(238,247,251,.025) 45%, rgba(247,226,204,.09)), - url("../assets/mountain-lake-light.jpg"); - background-position: center; - background-size: cover; - background-repeat: no-repeat; - background-attachment: fixed; overflow-x: hidden; isolation: isolate; } :root[data-theme="dark"] body { background-color: #091722; - background-image: - linear-gradient(118deg, rgba(2,15,25,.66), rgba(8,27,40,.57) 48%, rgba(20,25,29,.64)), - url("../assets/mountain-lake-dark.jpg"); } body::before, body::after { content: ""; position: fixed; - inset: 0; - z-index: -2; pointer-events: none; } body::before { - background: linear-gradient(110deg, rgba(214,242,253,.16), transparent 35%, rgba(255,240,218,.1) 76%, transparent); - opacity: .74; + inset: -1.25rem; + z-index: -2; + background-image: + linear-gradient(118deg, rgba(180,220,239,.15), rgba(238,247,251,.025) 45%, rgba(247,226,204,.09)), + url("../assets/mountain-lake-light.jpg"); + background-position: center; + background-size: cover; + background-repeat: no-repeat; + filter: blur(6px) saturate(108%); + transform: scale(1.025); } body::after { + inset: 0; + z-index: -1; background: radial-gradient(circle at 11% 18%, rgba(255,255,255,.32) 0 .5%, transparent 5%), radial-gradient(circle at 64% 23%, rgba(255,244,218,.27) 0 .6%, transparent 6%), @@ -80,7 +78,12 @@ body::after { opacity: .5; } -:root[data-theme="dark"] body::before { background: linear-gradient(115deg, rgba(29,104,142,.1), transparent 40%, rgba(110,76,42,.08)); opacity: .7; } +:root[data-theme="dark"] body::before { + background-image: + linear-gradient(118deg, rgba(2,15,25,.66), rgba(8,27,40,.57) 48%, rgba(20,25,29,.64)), + url("../assets/mountain-lake-dark.jpg"); + filter: blur(7px) saturate(106%); +} :root[data-theme="dark"] body::after { opacity: .25; } .ambient::before { width: 32rem; height: 32rem; left: -13rem; top: -14rem; background: #c9f2ff; opacity: .14; filter: blur(76px); } @@ -351,9 +354,27 @@ body::after { :root[data-theme="dark"] .quality-chip.warn { color: #ffd59e; } :root[data-theme="dark"] .quality-chip.bad { color: #ffc0c8; } -.co2-meter { height: .68rem; border: 1px solid rgba(255,255,255,.78); border-radius: 999px; background: rgba(91,145,181,.17); box-shadow: inset 0 1px 3px rgba(25,68,98,.09); overflow: hidden; } -.co2-meter i { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #08a350, #36dc83); box-shadow: 0 0 10px rgba(18,199,104,.34), inset 0 1px 0 rgba(255,255,255,.55); transition: width .3s ease; } -.meter-labels { display: flex; justify-content: space-between; margin-top: .26rem; padding: 0 .2rem; color: var(--widget-muted); font-size: .54rem; } +.air-card { + --air-color: #7896ac; + --air-glow: rgba(93, 140, 171, .2); + --air-track: rgba(181, 203, 200, .28); +} +.air-card[data-air-tone="good"] { + --air-color: #1fbd69; + --air-glow: rgba(18, 199, 104, .34); +} +.air-card[data-air-tone="warn"] { + --air-color: #efa12b; + --air-glow: rgba(244, 161, 42, .35); +} +.air-card[data-air-tone="bad"] { + --air-color: #e8495c; + --air-glow: rgba(235, 74, 83, .36); +} +.co2-meter { height: .72rem; border: 1px solid rgba(255,255,255,.82); border-radius: 999px; background-color: var(--air-track); background-image: linear-gradient(180deg, rgba(255,255,255,.15), transparent 52%, rgba(0,20,36,.045)); box-shadow: inset 0 1px 3px rgba(20,61,78,.14), 0 1px 4px rgba(8,43,72,.07); overflow: hidden; transition: background-color .65s ease; } +.co2-meter i { display: block; width: 0; height: 100%; border-radius: inherit; background-color: var(--air-color); background-image: linear-gradient(180deg, rgba(255,255,255,.28), rgba(255,255,255,.045) 48%, rgba(0,35,20,.1)); box-shadow: 0 0 8px var(--air-glow), inset 0 1px 0 rgba(255,255,255,.58), inset 0 -1px 2px rgba(0,34,19,.1); transition: width .35s ease, background-color .65s ease, box-shadow .65s ease; } +.meter-labels { display: flex; justify-content: space-between; margin-top: .27rem; padding: 0 .2rem; color: rgba(22,69,108,.78); font-size: .58rem; font-weight: 650; text-shadow: 0 1px 0 rgba(255,255,255,.48); } +:root[data-theme="dark"] .meter-labels { color: rgba(211,235,248,.76); text-shadow: 0 1px 2px rgba(0,0,0,.58); } .mini-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: .52rem; margin-top: .68rem; } .mini-metric { min-width: 0; min-height: 4.35rem; display: flex; align-items: center; justify-content: center; gap: .5rem; padding: .62rem .5rem; border: 1px solid var(--tile-edge-soft); border-radius: .9rem; background: linear-gradient(145deg, rgba(255,255,255,.13), rgba(107,151,177,.055)); box-shadow: inset 0 1px 0 rgba(255,255,255,.33); text-align: center; } .metric-icon { width: 1.55rem; height: 1.55rem; flex: 0 0 auto; color: #137edc; filter: drop-shadow(0 0 4px rgba(20,124,221,.15)); } @@ -401,14 +422,25 @@ body::after { :root[data-theme="dark"] .glass-switch { color: #ddecf4; border-color: rgba(193,228,245,.25); background: linear-gradient(145deg, rgba(77,112,133,.23), rgba(20,44,59,.28)); text-shadow: 0 1px 2px rgba(0,0,0,.4); } :root[data-theme="dark"] .glass-switch[aria-pressed="true"].green, :root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange { color: #effff6; border-color: rgba(127,242,182,.4); background: radial-gradient(circle at 25% -8%, rgba(218,255,236,.3), transparent 42%), linear-gradient(132deg, rgba(255,255,255,.07), transparent 40%, rgba(0,45,23,.12)), linear-gradient(145deg, rgba(51,190,119,.52), rgba(9,102,59,.49)); box-shadow: 0 9px 21px rgba(0,0,0,.22), 0 0 9px rgba(37,207,121,.1), inset 0 1px 0 rgba(218,255,236,.32), inset 0 -3px 8px rgba(0,31,16,.16); } -:root[data-theme="dark"] .co2-meter { border-color: rgba(200,232,247,.24); background: rgba(3,22,34,.34); box-shadow: inset 0 2px 5px rgba(0,0,0,.2); } +:root[data-theme="dark"] .air-card { --air-track: rgba(8,28,39,.38); } +:root[data-theme="dark"] .air-card[data-air-tone="good"] { --air-glow: rgba(30, 224, 128, .3); } +:root[data-theme="dark"] .air-card[data-air-tone="warn"] { --air-glow: rgba(255, 176, 49, .3); } +:root[data-theme="dark"] .air-card[data-air-tone="bad"] { --air-glow: rgba(255, 82, 102, .32); } +:root[data-theme="dark"] .co2-meter { border-color: rgba(205,235,248,.3); box-shadow: inset 0 1px 4px rgba(0,0,0,.27), 0 1px 5px rgba(0,0,0,.12); } :root[data-theme="dark"] .mini-metric { background: linear-gradient(145deg, rgba(196,231,247,.055), rgba(3,24,37,.08)); box-shadow: inset 0 1px 0 rgba(224,246,255,.07); } /* Wide monitor layout. The same markup stays vertical on narrow screens, so controls and API behaviour cannot drift between two widget versions. */ +:root[data-widget-layout="horizontal"] body { + padding: .36rem; +} + :root[data-widget-layout="horizontal"] .widget { width: min(100%, 76rem); max-width: none; + gap: .48rem; + padding: .58rem; + border-radius: 1.35rem; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1.4fr) minmax(0, .8fr) minmax(0, .8fr); grid-template-areas: "header header header header" @@ -418,25 +450,128 @@ body::after { "demo demo demo demo"; align-items: stretch; } -:root[data-widget-layout="horizontal"] .widget-header { grid-area: header; grid-template-columns: minmax(0, 1fr) auto auto; } +:root[data-widget-layout="horizontal"] .widget-header { + grid-area: header; + min-height: 2.6rem; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: .35rem .55rem; + padding: 0 .12rem; +} :root[data-widget-layout="horizontal"] .widget-header .identity { grid-column: 1; grid-row: 1; } :root[data-widget-layout="horizontal"] .widget-header .status-row { grid-column: 2; grid-row: 1; } :root[data-widget-layout="horizontal"] .widget-header .theme-button { grid-column: 3; grid-row: 1; } +:root[data-widget-layout="horizontal"] .identity { gap: .55rem; } +:root[data-widget-layout="horizontal"] .brand-icon { width: 2.45rem; height: 2.45rem; border-radius: .82rem; } +:root[data-widget-layout="horizontal"] .brand-icon svg { width: 1.48rem; height: 1.48rem; } +:root[data-widget-layout="horizontal"] .identity h1 { font-size: 1.42rem; } +:root[data-widget-layout="horizontal"] .identity-copy > small { margin-top: .18rem; font-size: .61rem; } +:root[data-widget-layout="horizontal"] .title-row { gap: .42rem; } +:root[data-widget-layout="horizontal"] .widget .pill { min-height: 1.52rem; padding: .2rem .5rem; font-size: .54rem; } +:root[data-widget-layout="horizontal"] .mode-detail { font-size: .59rem; } +:root[data-widget-layout="horizontal"] .theme-button { width: 2.2rem; height: 2.2rem; border-radius: .75rem; } :root[data-widget-layout="horizontal"] .speed-card { grid-area: speed; } :root[data-widget-layout="horizontal"] .temperature-card { grid-area: temperature; } +:root[data-widget-layout="horizontal"] .control-card { + min-height: 8.75rem; + padding: .66rem .78rem .58rem; +} +:root[data-widget-layout="horizontal"] .card-title { gap: .5rem; } +:root[data-widget-layout="horizontal"] .card-title h2, +:root[data-widget-layout="horizontal"] .switch-title h2, +:root[data-widget-layout="horizontal"] .section-title h2 { font-size: .86rem; } +:root[data-widget-layout="horizontal"] .card-title p { margin-top: .1rem; font-size: .58rem; } +:root[data-widget-layout="horizontal"] .feature-icon { width: 1.65rem; height: 1.65rem; } +:root[data-widget-layout="horizontal"] .hero-control { + grid-template-columns: 2.75rem 1fr 2.75rem; + gap: .55rem; + margin: .46rem 0 .34rem; +} +:root[data-widget-layout="horizontal"] .hero-step { width: 2.75rem; height: 2.75rem; border-radius: .78rem; font-size: 1.5rem; } +:root[data-widget-layout="horizontal"] .hero-value { font-size: 2.4rem; } +:root[data-widget-layout="horizontal"] .speed-segments button { height: .68rem; } +:root[data-widget-layout="horizontal"] .scale-labels, +:root[data-widget-layout="horizontal"] .temperature-labels { margin-top: .2rem; font-size: .52rem; } +:root[data-widget-layout="horizontal"] .temperature-range { height: 1.05rem; } +:root[data-widget-layout="horizontal"] .temperature-range::-webkit-slider-runnable-track { height: .55rem; } +:root[data-widget-layout="horizontal"] .temperature-range::-moz-range-track { height: .55rem; } +:root[data-widget-layout="horizontal"] .temperature-range::-webkit-slider-thumb { width: 1.32rem; height: 1.32rem; margin-top: -.4rem; border-width: .21rem; } +:root[data-widget-layout="horizontal"] .temperature-range::-moz-range-thumb { width: .9rem; height: .9rem; border-width: .21rem; } :root[data-widget-layout="horizontal"] .switch-grid { display: contents; } :root[data-widget-layout="horizontal"] .switch-grid > * { z-index: 1; } :root[data-widget-layout="horizontal"] .power-card { grid-area: power; } :root[data-widget-layout="horizontal"] .heater-card { grid-area: heater; } -:root[data-widget-layout="horizontal"] .switch-card { min-height: 11.4rem; } -:root[data-widget-layout="horizontal"] .air-card { grid-area: air; } +:root[data-widget-layout="horizontal"] .switch-card { + min-height: 8.75rem; + gap: .45rem; + padding: .66rem .68rem .58rem; +} +:root[data-widget-layout="horizontal"] .switch-title { gap: .4rem; } +:root[data-widget-layout="horizontal"] .switch-title small { font-size: .52rem; } +:root[data-widget-layout="horizontal"] .switch-icon { width: 1.5rem; height: 1.5rem; } +:root[data-widget-layout="horizontal"] .glass-switch { min-height: 2.7rem; border-radius: .78rem; font-size: 1.05rem; } +:root[data-widget-layout="horizontal"] .info-card { padding: .62rem .68rem; } +:root[data-widget-layout="horizontal"] .section-icon { width: 1.5rem; height: 1.5rem; } +:root[data-widget-layout="horizontal"] .open-section { width: 1.75rem; height: 1.75rem; border-radius: .58rem; font-size: .82rem; } +:root[data-widget-layout="horizontal"] .air-card { + grid-area: air; + display: grid; + grid-template-columns: minmax(0, 1.55fr) minmax(10.8rem, .82fr); + grid-template-rows: auto auto auto auto; + column-gap: .72rem; + align-content: start; +} +:root[data-widget-layout="horizontal"] .air-card .section-title { grid-column: 1 / -1; grid-row: 1; } +:root[data-widget-layout="horizontal"] .air-card .co2-summary { + grid-column: 1; + grid-row: 2; + margin: .42rem .05rem .3rem; +} +:root[data-widget-layout="horizontal"] .air-card .co2-meter { grid-column: 1; grid-row: 3; } +:root[data-widget-layout="horizontal"] .air-card .meter-labels { grid-column: 1; grid-row: 4; } +:root[data-widget-layout="horizontal"] .air-card .mini-metrics { + grid-column: 2; + grid-row: 2 / 5; + align-self: stretch; + gap: .38rem; + margin-top: .4rem; +} +:root[data-widget-layout="horizontal"] .air-card .mini-metric { + min-height: 0; + flex-direction: column; + gap: .14rem; + padding: .35rem .25rem; + border-radius: .72rem; +} +:root[data-widget-layout="horizontal"] .air-card .metric-icon { width: 1.25rem; height: 1.25rem; } +:root[data-widget-layout="horizontal"] .air-card .mini-metric small { font-size: .51rem; } +:root[data-widget-layout="horizontal"] .air-card .mini-metric strong { margin-top: .1rem; font-size: 1.08rem; } +:root[data-widget-layout="horizontal"] .air-card .co2-summary strong { font-size: 1.65rem; } +:root[data-widget-layout="horizontal"] .air-card .quality-chip { padding: .25rem .48rem; font-size: .54rem; } +:root[data-widget-layout="horizontal"] .co2-meter { height: .62rem; } :root[data-widget-layout="horizontal"] .schedule-card { grid-area: schedule; } :root[data-widget-layout="horizontal"] .air-card, -:root[data-widget-layout="horizontal"] .schedule-card { min-height: 9.6rem; } -:root[data-widget-layout="horizontal"] .open-panel { grid-area: open; } +:root[data-widget-layout="horizontal"] .schedule-card { min-height: 6.9rem; } +:root[data-widget-layout="horizontal"] .schedule-times { + gap: .5rem; + margin-top: .42rem; + padding: .08rem .18rem .12rem; +} +:root[data-widget-layout="horizontal"] .schedule-times small { font-size: .53rem; } +:root[data-widget-layout="horizontal"] .schedule-times strong { font-size: 1.18rem; } +:root[data-widget-layout="horizontal"] .schedule-times > i { height: 2.2rem; } +:root[data-widget-layout="horizontal"] .open-panel { + grid-area: open; + min-height: 2.45rem; + gap: .5rem; + padding: .38rem .75rem; + border-radius: .82rem; + font-size: .68rem; +} +:root[data-widget-layout="horizontal"] .open-panel svg { width: 1.15rem; height: 1.15rem; } +:root[data-widget-layout="horizontal"] .open-panel b { font-size: .85rem; } :root[data-widget-layout="horizontal"] .demo-note { grid-area: demo; } -button:disabled { opacity: .58; cursor: wait; transform: none !important; } +button:disabled { opacity: .58; cursor: default; transform: none !important; } @media (min-width: 560px) and (min-height: 900px) { html { font-size: 17px; } diff --git a/web/js/api.js b/web/js/api.js index ae0c8c1..25c9eb7 100644 --- a/web/js/api.js +++ b/web/js/api.js @@ -220,6 +220,48 @@ export function airQuality(co2) { return { label: "Нужна вентиляция", tone: "bad", pct: Math.min(100, value / 20) }; } +const AIR_METER_STOPS = [ + [400, [35, 143, 83]], + [700, [46, 166, 99]], + [820, [82, 181, 111]], + [900, [190, 153, 55]], + [1200, [214, 128, 45]], + [1500, [215, 83, 55]], + [2000, [188, 48, 72]], +]; + +const interpolateChannel = (start, end, amount) => Math.round(start + (end - start) * amount); + +export function airMeterPalette(co2) { + const value = Number(co2); + if (!Number.isFinite(value)) { + return { + color: "rgb(105, 137, 158)", + glow: "rgba(83, 123, 150, .2)", + }; + } + + const bounded = Math.max(AIR_METER_STOPS[0][0], Math.min(AIR_METER_STOPS.at(-1)[0], value)); + let lower = AIR_METER_STOPS[0]; + let upper = AIR_METER_STOPS.at(-1); + + for (let index = 1; index < AIR_METER_STOPS.length; index += 1) { + if (bounded <= AIR_METER_STOPS[index][0]) { + lower = AIR_METER_STOPS[index - 1]; + upper = AIR_METER_STOPS[index]; + break; + } + } + + const amount = upper[0] === lower[0] ? 0 : (bounded - lower[0]) / (upper[0] - lower[0]); + const [red, green, blue] = lower[1].map((channel, index) => interpolateChannel(channel, upper[1][index], amount)); + + return { + color: `rgb(${red}, ${green}, ${blue})`, + glow: `rgba(${red}, ${green}, ${blue}, .26)`, + }; +} + export function modeInfo(status) { const schedule = status?.schedule || {}; if (!schedule.available) return { label: "Без расписания", detail: "Недоступно", tone: "muted" }; diff --git a/web/js/panel.js b/web/js/panel.js index c2bc4ee..a63eda5 100644 --- a/web/js/panel.js +++ b/web/js/panel.js @@ -1,4 +1,4 @@ -import { api, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, setRangeProgress, showToast } from "./api.js"; +import { api, airMeterPalette, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, setRangeProgress, showToast } from "./api.js"; const $ = selector => document.querySelector(selector); configureTheme($("#theme-button")); @@ -27,7 +27,6 @@ if (isDemo) $("#demo-note").hidden = false; function text(selector, value) { $(selector).textContent = value; } function setPill(selector, label, tone) { const element = $(selector); element.textContent = label; element.className = `pill ${tone}`; } -function toneColor(tone) { return `var(--${tone === "bad" ? "red" : tone === "warn" ? "orange" : tone === "good" ? "green" : tone === "info" ? "blue" : "muted"})`; } function toggle(selector, active, label = "") { const button = $(selector); @@ -63,11 +62,16 @@ function actionLabel(value) { function renderAir(sensor, tion) { const quality = airQuality(sensor.co2); + const airTone = ["good", "warn", "bad"].includes(quality.tone) ? quality.tone : "muted"; + const airPalette = airMeterPalette(sensor.co2); + const airCard = $(".co2-card"); text("#co2", numberOrDash(sensor.co2)); text("#air-label", quality.label); text("#air-advice", quality.tone === "good" ? "Проветривание работает нормально" : quality.tone === "warn" ? "Автоматика при необходимости повысит скорость" : quality.tone === "bad" ? "Рекомендуется усилить вентиляцию" : "Ожидаем показания датчика"); - $("#air-label").style.color = toneColor(quality.tone); - $("#air-label").style.background = `var(--${quality.tone === "bad" ? "red" : quality.tone === "warn" ? "orange" : quality.tone === "good" ? "green" : "glass"}-soft)`; + $("#air-label").className = `quality-chip ${airTone}`; + airCard.dataset.airTone = airTone; + airCard.style.setProperty("--air-color", airPalette.color); + airCard.style.setProperty("--air-glow", airPalette.glow); $("#co2-progress").style.width = `${Math.max(0, Math.min(100, quality.pct))}%`; setPill("#sensor-status", sensor.online ? "Online" : "Offline", sensor.online ? "good" : "bad"); text("#room-temp", numberOrDash(sensor.temperature ?? tion.in_temp, 1)); diff --git a/web/js/widget.js b/web/js/widget.js index 425d029..13782df 100644 --- a/web/js/widget.js +++ b/web/js/widget.js @@ -1,4 +1,4 @@ -import { api, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, panelUrl, showToast } from "./api.js"; +import { api, airMeterPalette, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, panelUrl, showToast } from "./api.js"; const requestedLayout = new URLSearchParams(window.location.search).get("layout"); const layoutMode = ["horizontal", "vertical"].includes(requestedLayout) ? requestedLayout : null; @@ -75,7 +75,13 @@ function render(status) { setText("#co2", numberOrDash(sensor.co2)); setText("#air-label", quality.label); - $("#air-label").className = `quality-chip ${toneClass(quality.tone)}`; + const airTone = toneClass(quality.tone); + const airPalette = airMeterPalette(sensor.co2); + const airCard = $(".air-card"); + $("#air-label").className = `quality-chip ${airTone}`; + airCard.dataset.airTone = airTone; + airCard.style.setProperty("--air-color", airPalette.color); + airCard.style.setProperty("--air-glow", airPalette.glow); $("#co2-progress").style.width = `${Math.max(0, Math.min(100, quality.pct))}%`; setText("#room-temp", numberOrDash(sensor.temperature ?? tion.in_temp, 1)); setText("#humidity", numberOrDash(sensor.humidity)); diff --git a/web/panel.html b/web/panel.html index dae673e..6434186 100644 --- a/web/panel.html +++ b/web/panel.html @@ -80,8 +80,8 @@ Обновление…
Ожидаем показания датчика