feat(easel): add localization

This commit is contained in:
2026-04-23 15:47:16 +03:00
parent ecb574e286
commit 9351b9f53a
34 changed files with 464 additions and 311 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.pyc
*.mo
.pytest_cache
.venv
#.vscode

View File

@@ -19,7 +19,7 @@ FROM python:3.12-slim
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
RUN apt update && \
apt install -y locales && \
apt install -y locales gettext && \
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG=ru_RU.UTF-8
@@ -28,5 +28,6 @@ ENV TZ="Europe/Moscow"
COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/
RUN cd gallery/easel/route/view/locales/ru/LC_MESSAGES && msgfmt messages.po
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]

View File

@@ -1,10 +1,13 @@
import locale as _locale
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from gallery.sketch.bundle import ApiBundle
from gallery.util import root_path
from .route import api, doc, view
from .route import api, doc
from .route.view import router as view_router
DEFAULT_LOCALE = "ru_RU.UTF-8"
@@ -17,7 +20,8 @@ def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI
redoc_url=None,
)
app.state.api = api_bundle
app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
doc.mount(app)
api.mount(app)
view.mount(app)
app.include_router(view_router)
return app

View File

@@ -1,9 +1,11 @@
from fastapi import FastAPI
from fastapi import APIRouter, Depends
from . import common, schedule, weather
from .common import router as common_router
from .schedule import router as schedule_router
from .translation import set_language
from .weather import router as weather_router
def mount(app: FastAPI):
common.mount(app)
weather.mount(app)
schedule.mount(app)
router = APIRouter(dependencies=[Depends(set_language)])
router.include_router(common_router)
router.include_router(weather_router)
router.include_router(schedule_router)

View File

@@ -1,34 +1,35 @@
from pathlib import Path
from typing import NamedTuple
from fastapi import FastAPI, Request
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from gallery.util import root_path
from gallery.version import __version__
from ..translation import _
class Section(NamedTuple):
link: str
title: str
icon: str
SECTIONS = [
Section("weather", "Weather"),
Section("schedule", "TV program"),
Section("weather", "Weather", "brightness-high"),
Section("schedule", "TV program", "tv"),
]
def mount(app: FastAPI):
base_dir = Path(__file__).parent
print("!", root_path / "static/dist")
app.mount("/static/main", StaticFiles(directory=root_path / "static/dist"))
app.mount("/static/common", StaticFiles(directory=base_dir / "static"))
templates = Jinja2Templates(directory=base_dir / "templates")
@app.get("/", response_class=HTMLResponse)
router = APIRouter()
templates = Jinja2Templates(directory=base_dir / "templates")
templates.env.globals.update({"_": _})
@router.get("/", response_class=HTMLResponse)
async def get_section_list(request: Request):
return templates.TemplateResponse(
request=request,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{request.state.language}}">
<head>
{% block head %}
@@ -10,13 +10,11 @@
content="ie=edge">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet"
href="/static/main/gallery-static.css?v={{version}}">
href="/static/gallery.css?v={{version}}">
<script type="module"
src="/static/main/gallery-static.js?v={{version}}"></script>
<link rel="stylesheet"
href="/static/common/style.css?v={{version}}">
src="/static/gallery.es.js?v={{version}}"></script>
<link rel="icon"
href="/static/common/favicon.ico?v={{version}}"
href="/favicon.ico?v={{version}}"
type="image/x-icon">
{% endblock %}
</head>
@@ -24,13 +22,43 @@
<body>
<div class="app col-lg-8 mx-auto p-3 py-md-5">
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<a href="/"
class="d-flex align-items-center text-dark text-decoration-none">
<div class="icon me-2"
style="background-image: url(/static/common/gallery.png);"></div>
<span class="fs-4 text-body">API Gallery</span>
</a>
<app-link href="/"
icon="gear">API Gallery</app-link>
{% block header %}{% endblock %}
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<li class="nav-item dropdown">
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
id="bd-language"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Select language (default)">
<span class="me-2 language-icon-active">🇬🇧</span>
<span class="d-lg-none ms-2"
id="bd-language-text">Select language</span>
</button>
<ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-language-text">
<li>
<button type="button"
class="dropdown-item d-flex align-items-center"
data-bs-language-value="en"
aria-pressed="false">
<span class="me-2 language-icon">🇬🇧</span>
English
</button>
</li>
<li>
<button type="button"
class="dropdown-item d-flex align-items-center"
data-bs-language-value="ru"
aria-pressed="false">
<span class="me-2 language-icon">🇷🇺</span>
Russian
</button>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
@@ -84,13 +112,6 @@
</div>
<script>
(function () {
document.toggleTheme = () => {
const current = document.documentElement.getAttribute('data-bs-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-bs-theme', next);
localStorage.setItem('theme', next);
};
const params = new URLSearchParams(window.location.search);
const widget = params.get('widget') || window.location.hostname.startsWith('weather');
if (widget) {

View File

@@ -10,15 +10,17 @@
{% for section in sections %}
<a href="{{section.link}}"
class="list-group-item list-group-item-action px-4">
<div class="d-flex w-100">
<span class="icon me-2"
style="background-image: url(/static/{{section.link}}/{{section.link}}.png);"></span>
<h4>{{section.title}}</h4>
</div>
<app-link href="{{section.link}}"
icon="{{section.icon}}">
{{_(section.title)}}
</app-link>
</a>
{% endfor %}
</div>
<hr class="col-3 col-md-2 mb-5">
<h1>Docs</h1>
<a href="/docs" target="_blank"><h4>Swagger</h4></a>
<a href="/docs"
target="_blank">
<h4>Swagger</h4>
</a>
{% endblock %}

View File

@@ -0,0 +1,15 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Weather"
msgstr "Погода"
msgid "TV program"
msgstr "Телепрограмма"

View File

@@ -1,10 +1,8 @@
import asyncio
import datetime
from pathlib import Path
from fastapi import FastAPI
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from gallery.easel.core import AppRequest
@@ -12,21 +10,23 @@ from gallery.sketch.schedule.catalog import BUNDLE
from gallery.version import __version__
from ..common.util import TagType, TagUtil
from ..translation import _
from .filters import timedelta_format
def mount(app: FastAPI):
base_dir = Path(__file__).parent
app.mount("/static/schedule", StaticFiles(directory=base_dir / "static"))
templates = Jinja2Templates(
directory=[
base_dir.parent / "common/templates",
base_dir / "templates",
]
)
templates.env.globals.update({"_": _})
templates.env.filters["timedelta_format"] = timedelta_format
@app.get("/schedule", response_class=HTMLResponse)
router = APIRouter()
@router.get("/schedule", response_class=HTMLResponse)
async def get_schedule_list(request: AppRequest):
schedule_api = request.app.state.api.schedule
channels = await schedule_api.get_channels()
@@ -40,7 +40,8 @@ def mount(app: FastAPI):
},
)
@app.get("/schedule/tag/{tag}", response_class=HTMLResponse)
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse)
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
tag_value = TagUtil.parse_tag(tag)
schedule_api = request.app.state.api.schedule
@@ -58,11 +59,13 @@ def mount(app: FastAPI):
},
)
@app.get("/schedule/{channel}", response_class=RedirectResponse)
@router.get("/schedule/{channel}", response_class=RedirectResponse)
async def get_channel_default(channel: str):
return RedirectResponse(f"{channel}/tag/today")
@app.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, channel: str, tag: str):
tag_value = TagUtil.parse_tag(tag)
schedule_api = request.app.state.api.schedule

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -2,13 +2,11 @@
{% block title %}
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet"
href="/static/schedule/style.css?v={{version}}">
<link rel="icon"
href="/static/schedule/favicon.ico?v={{version}}"
type="image/x-icon">
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}

View File

@@ -1,16 +1,8 @@
{% extends "base.html" %}
{% block title %}TV program{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet"
href="/static/schedule/style.css?v={{version}}">
<link rel="icon"
href="/static/schedule/favicon.ico?v={{version}}"
type="image/x-icon">
{% endblock %}
{% block title %}{{_("TV program")}}{% endblock %}
{% block content %}
<h1>TV program</h1>
<h1>{{_("TV program")}}</h1>
<div class="list-group mb-5">
<a href="schedule/tag/today"
class="list-group-item list-group-item-action px-4">

View File

@@ -1,14 +1,12 @@
{% extends "base.html" %}
{% block title %}
{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}}
{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet"
href="/static/schedule/style.css?v={{version}}">
<link rel="icon"
href="/static/schedule/favicon.ico?v={{version}}"
type="image/x-icon">
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}

View File

@@ -0,0 +1,34 @@
import gettext
from contextvars import ContextVar
from pathlib import Path
from fastapi import Cookie, Header, Request
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
ContextVar("translation")
)
async def set_language(
request: Request,
accept_language: str = Header("en"),
language: str | None = Cookie(None),
):
# Simplify the header (e.g., "en-US,en;q=0.9" -> "en")
lang = language or accept_language.split(",")[0].split("-")[0]
try:
t = gettext.translation(
"messages", localedir=Path(__file__).parent / "locales", languages=[lang]
)
except FileNotFoundError:
t = gettext.NullTranslations()
token = _translation.set(t)
request.state.language = lang
yield lang
_translation.reset(token)
def _(message: str) -> str:
return _translation.get().gettext(message)

View File

@@ -1,7 +1,7 @@
import datetime
from pathlib import Path
from fastapi import FastAPI
from fastapi import APIRouter, FastAPI
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -11,21 +11,22 @@ from gallery.sketch.weather.model import WeatherResponse
from gallery.version import __version__
from ..common.util import TagType, TagUtil
from ..translation import _
from .filters import cloudness_icon, wind_direction_icon
def mount(app: FastAPI):
base_dir = Path(__file__).parent
app.mount("/static/weather", StaticFiles(directory=base_dir / "static"))
templates = Jinja2Templates(
directory=[
base_dir.parent / "common/templates",
base_dir / "templates",
]
)
templates.env.globals.update({"_": _})
templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon
def build_weather_response(request: AppRequest, response: WeatherResponse):
return templates.TemplateResponse(
request=request,
@@ -38,7 +39,9 @@ def mount(app: FastAPI):
},
)
@app.get("/weather", response_class=HTMLResponse)
router = APIRouter()
@router.get("/weather", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, query: str | None = None):
weather_api = request.app.state.api.weather
locations = (await weather_api.find_locations(query)) if query else []
@@ -51,23 +54,27 @@ def mount(app: FastAPI):
},
)
@app.get("/weather/{location}", response_class=RedirectResponse)
@router.get("/weather/{location}", response_class=RedirectResponse)
async def get_weather_default(location: str):
return RedirectResponse(f"{location}/tag/today")
@app.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
weather_api = request.app.state.api.weather
response = await weather_api.get_day(location, date)
return build_weather_response(request, response)
@app.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, location: str, days: int):
weather_api = request.app.state.api.weather
response = await weather_api.get_days(location, days)
return build_weather_response(request, response)
@app.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
async def get_weather_tag(request: AppRequest, location: str, tag: str):
tag_value = TagUtil.parse_tag(tag)
weather_api = request.app.state.api.weather

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,71 +0,0 @@
.header {
font-size: 0.9rem;
text-align: left;
padding-top: 0.25rem;
}
.date {
background: rgba(1, 0, 0, 0.1) !important;
}
.date.now {
background: rgba(0, 128, 255, 0.2) !important;
}
.date .value a {
all: unset;
cursor: pointer;
}
.cloudness {
vertical-align: top;
}
.cloudness .icon {
font-size: 1rem;
}
.cloudness .icon:first-child {
font-size: 2rem;
}
.temperature {
padding: 0;
}
.temperature .value {
padding: 0.1rem 0.4rem;
}
.temperature .value.positive {
color: orangered;
}
.temperature .value.negative {
color: blue;
}
.wind .direction {
font-size: 1rem;
}
.wind .gust {
font-size: 1rem;
}
.precipitation .value {
color: blue;
}
.pressure {
padding: 0 !important;
}
.pressure .value {
padding: 0.1rem 0.4rem;
color: blueviolet;
}
.humidity .value {
color: blue;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1,13 +1,5 @@
{% extends "base.html" %}
{% block title %}Weather{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet"
href="/static/weather/style.css?v={{version}}">
<link rel="icon"
href="/static/weather/favicon.ico?v={{version}}"
type="image/x-icon">
{% endblock %}
{% block content %}
<h1>Weather</h1>

View File

@@ -1,12 +1,9 @@
{% extends "base.html" %}
{% block title %}Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet"
href="/static/weather/style.css?v={{version}}">
<link rel="icon"
href="/static/weather/favicon.ico?v={{version}}"
type="image/x-icon">
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
{% block header %}
<span class="fs-4 text-body ms-2 me-2">/</span>
<app-link href="/weather" icon="brightness-high">{{_("Weather")}}</app-link>
{% endblock %}
{% block content %}
@@ -25,7 +22,8 @@
{% endif %}
</h4>
<div class="table-responsive">
<table class="table table-borderless table-compact text-center w-auto" style="font-size: 130%;">
<table class="table table-weather table-borderless table-compact text-center w-auto"
style="font-size: 130%;">
<tbody>
<!-- date -->
<tr>

6
scripts/locales Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
cd "$(dirname $(dirname "$0"))" || exit
cd gallery/easel/route/view/locales/ru/LC_MESSAGES || exit
msgfmt messages.po

View File

@@ -1,5 +1,5 @@
{
"name": "gallery-static",
"name": "gallery",
"version": "0.1.0",
"scripts": {
"build": "vite build",

17
static/src/components.ts Normal file
View File

@@ -0,0 +1,17 @@
class AppLinkElement extends HTMLElement {
static observedAttributes = ["icon", "href"];
constructor() {
super();
this.innerHTML = `
<a href="${this.getAttribute("href")}"
class="d-flex align-items-center text-body text-decoration-none">
<span class="fs-4">
<i class="bi bi-${this.getAttribute("icon")}"></i>
<span>${this.textContent}</span>
</span>
</a>`;
}
}
customElements.define("app-link", AppLinkElement);

View File

@@ -1,3 +1,5 @@
import "./main.scss";
import "bootstrap";
import "./theme";
import "./language";
import "./components";

62
static/src/language.ts Normal file
View File

@@ -0,0 +1,62 @@
(() => {
const getStoredLanguage = () => {
const m = document.cookie.match(/language=(\w+)/);
return m ? m[1] : null;
};
const setStoredLanguage = (language: string) => (document.cookie = `language=${language}; max-age=34560000; path=/`);
const getPreferredLanguage = () => {
const storedLanguage = getStoredLanguage();
if (storedLanguage) {
return storedLanguage;
}
const result = window.navigator.language.split("-")[0];
return ["en", "ru"].includes(result) ? result : "en";
};
const setLanguage = (language: string) => {};
setLanguage(getPreferredLanguage());
const showActiveLanguage = (language: string, focus = false) => {
const languageSwitcher = document.querySelector("#bd-language");
if (!languageSwitcher) {
return;
}
const languageSwitcherText = document.querySelector("#bd-language-text");
const activeLanguageIcon = document.querySelector(".language-icon-active");
const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
const activeLanguageIconContent = btnToActive?.querySelector("span")?.textContent;
document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
element.classList.remove("active");
element.setAttribute("aria-pressed", "false");
});
btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true");
activeLanguageIcon.textContent = activeLanguageIconContent;
const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);
if (focus) {
languageSwitcher.focus();
}
};
window.addEventListener("DOMContentLoaded", () => {
showActiveLanguage(getPreferredLanguage());
document.querySelectorAll("[data-bs-language-value]").forEach((toggle) => {
toggle.addEventListener("click", () => {
const language = toggle.getAttribute("data-bs-language-value") || "";
setStoredLanguage(language);
setLanguage(language);
showActiveLanguage(language, true);
window.location.reload();
});
});
});
})();

View File

@@ -2,8 +2,18 @@
$bootstrap-icons-font-dir: "bootstrap-icons/font/fonts";
@import "bootstrap-icons/font/bootstrap-icons";
@import "./widget.scss";
@import "./weather.scss";
.table.table-compact {
td {
padding: 0.1rem 0.4rem;
}
}
.icon {
display: inline-block;
width: 2rem;
height: 2rem;
background-size: contain;
}

View File

@@ -1,14 +1,6 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
"use strict";
const getStoredTheme = () => localStorage.getItem("theme");
const setStoredTheme = (theme) => localStorage.setItem("theme", theme);
const setStoredTheme = (theme: string) => localStorage.setItem("theme", theme);
const getPreferredTheme = () => {
const storedTheme = getStoredTheme();
@@ -19,7 +11,7 @@
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
};
const setTheme = (theme) => {
const setTheme = (theme: string) => {
if (theme === "auto") {
document.documentElement.setAttribute(
"data-bs-theme",
@@ -32,7 +24,7 @@
setTheme(getPreferredTheme());
const showActiveTheme = (theme, focus = false) => {
const showActiveTheme = (theme: string, focus = false) => {
const themeSwitcher = document.querySelector("#bd-theme");
if (!themeSwitcher) {
@@ -74,7 +66,7 @@
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
toggle.addEventListener("click", () => {
const theme = toggle.getAttribute("data-bs-theme-value");
const theme = toggle.getAttribute("data-bs-theme-value") || '';
setStoredTheme(theme);
setTheme(theme);
showActiveTheme(theme, true);

73
static/src/weather.scss Normal file
View File

@@ -0,0 +1,73 @@
.table-weather {
.header {
font-size: 0.9rem;
text-align: left;
padding-top: 0.25rem;
}
.date {
background: rgba(1, 0, 0, 0.1) !important;
}
.date.now {
background: rgba(0, 128, 255, 0.2) !important;
}
.date .value a {
all: unset;
cursor: pointer;
}
.cloudness {
vertical-align: top;
}
.cloudness .icon {
font-size: 1rem;
}
.cloudness .icon:first-child {
font-size: 2rem;
}
.temperature {
padding: 0;
}
.temperature .value {
padding: 0.1rem 0.4rem;
}
.temperature .value.positive {
color: orangered;
}
.temperature .value.negative {
color: blue;
}
.wind .direction {
font-size: 1rem;
}
.wind .gust {
font-size: 1rem;
}
.precipitation .value {
color: blue;
}
.pressure {
padding: 0 !important;
}
.pressure .value {
padding: 0.1rem 0.4rem;
color: blueviolet;
}
.humidity .value {
color: blue;
}
}

View File

@@ -1,10 +1,3 @@
.icon {
display: inline-block;
width: 2rem;
height: 2rem;
background-size: contain;
}
.widget .app {
padding: 0.5rem !important;
}

View File

@@ -8,7 +8,7 @@ export default defineConfig({
lib: {
entry: "./src/index.ts",
name: packageJson.name,
fileName: (format) => `${packageJson.name}.js`,
fileName: (format) => `${packageJson.name}.${format}.js`,
},
},
});