@danilosimonatto/ionicons-minimal-weather-widget 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/astro/WeatherWidget.astro +1 -1
- package/dist/weather-widget.js +215 -0
- package/package.json +14 -4
- package/src/styles.css +29 -0
- package/src/template.html +36 -0
- package/src/weather-widget.entry.js +4 -0
- package/{weather-widget.js → src/weather-widget.vite.js} +50 -89
- package/vite.config.js +20 -0
package/README.md
CHANGED
|
@@ -77,6 +77,26 @@ Props:
|
|
|
77
77
|
- 🎨 Customizable icon style and units
|
|
78
78
|
- 🔐 Uses your OpenWeather API key via the `api-key` attribute
|
|
79
79
|
|
|
80
|
+
## Development (editing HTML/CSS with formatting)
|
|
81
|
+
|
|
82
|
+
The published widget bundle is **generated** so consumers can keep importing a single file.
|
|
83
|
+
|
|
84
|
+
- Edit:
|
|
85
|
+
- `src/styles.css`
|
|
86
|
+
- `src/template.html`
|
|
87
|
+
- `src/weather-widget.vite.js`
|
|
88
|
+
- Then run:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npm run build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For auto-rebuild while developing (e.g. with `npm link`), run:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npm run dev
|
|
98
|
+
```
|
|
99
|
+
|
|
80
100
|
## License
|
|
81
101
|
|
|
82
102
|
MIT
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { defineCustomElements as w } from "ionicons/loader";
|
|
2
|
+
import { addIcons as f } from "ionicons";
|
|
3
|
+
import { helpCircleSharp as S, helpCircleOutline as b, helpCircle as C, snowSharp as O, snowOutline as x, snow as E, thunderstormSharp as k, thunderstormOutline as I, thunderstorm as v, rainySharp as A, rainyOutline as P, rainy as q, cloudyNightSharp as M, cloudyNightOutline as N, cloudyNight as U, cloudySharp as L, cloudyOutline as $, cloudy as F, partlySunnySharp as R, partlySunnyOutline as T, partlySunny as W, moonSharp as j, moonOutline as z, moon as K, sunnySharp as _, sunnyOutline as D, sunny as H } from "ionicons/icons";
|
|
4
|
+
const B = `:host {
|
|
5
|
+
--color-primary: #000;
|
|
6
|
+
display: inline-block;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.weather-widget {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: 8px;
|
|
13
|
+
color: var(--color-primary, #000);
|
|
14
|
+
text-align: center;
|
|
15
|
+
font-size: 1rem;
|
|
16
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
17
|
+
Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.error {
|
|
21
|
+
color: #ffebee;
|
|
22
|
+
background: rgba(255, 0, 0, 0.2);
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.weather-content {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: row;
|
|
29
|
+
gap: 5px;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
}
|
|
33
|
+
`, J = `<div
|
|
34
|
+
class="weather-widget"
|
|
35
|
+
part="root"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
class="loading"
|
|
39
|
+
part="loading"
|
|
40
|
+
>
|
|
41
|
+
Loading…
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
class="weather-content"
|
|
46
|
+
part="content"
|
|
47
|
+
hidden
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
class="city-name"
|
|
51
|
+
part="city"
|
|
52
|
+
></div>
|
|
53
|
+
<ion-icon
|
|
54
|
+
class="icon"
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
></ion-icon>
|
|
57
|
+
<span
|
|
58
|
+
class="temperature"
|
|
59
|
+
part="temperature"
|
|
60
|
+
></span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
class="error"
|
|
65
|
+
part="error"
|
|
66
|
+
hidden
|
|
67
|
+
></div>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
let a;
|
|
71
|
+
function G() {
|
|
72
|
+
return a || (f({
|
|
73
|
+
sunny: H,
|
|
74
|
+
sunnyOutline: D,
|
|
75
|
+
sunnySharp: _,
|
|
76
|
+
moon: K,
|
|
77
|
+
moonOutline: z,
|
|
78
|
+
moonSharp: j,
|
|
79
|
+
partlySunny: W,
|
|
80
|
+
partlySunnyOutline: T,
|
|
81
|
+
partlySunnySharp: R,
|
|
82
|
+
cloudy: F,
|
|
83
|
+
cloudyOutline: $,
|
|
84
|
+
cloudySharp: L,
|
|
85
|
+
cloudyNight: U,
|
|
86
|
+
cloudyNightOutline: N,
|
|
87
|
+
cloudyNightSharp: M,
|
|
88
|
+
rainy: q,
|
|
89
|
+
rainyOutline: P,
|
|
90
|
+
rainySharp: A,
|
|
91
|
+
thunderstorm: v,
|
|
92
|
+
thunderstormOutline: I,
|
|
93
|
+
thunderstormSharp: k,
|
|
94
|
+
snow: E,
|
|
95
|
+
snowOutline: x,
|
|
96
|
+
snowSharp: O,
|
|
97
|
+
helpCircle: C,
|
|
98
|
+
helpCircleOutline: b,
|
|
99
|
+
helpCircleSharp: S
|
|
100
|
+
}), a = w(window)), a;
|
|
101
|
+
}
|
|
102
|
+
const Q = (e) => String(e || "").toUpperCase() === "F" ? "F" : "C", V = (e) => e === "outline" || e === "sharp" || e === "filled" ? e : "filled", X = (e, t) => t === "filled" ? e : `${e}-${t}`, Y = {
|
|
103
|
+
// 01d - sunny
|
|
104
|
+
"01d": "sunny",
|
|
105
|
+
"01n": "moon",
|
|
106
|
+
// 02d - partly-sunny
|
|
107
|
+
"02d": "partly-sunny",
|
|
108
|
+
"02n": "cloudy-night",
|
|
109
|
+
// 03d, 04d, 50d - cloudy
|
|
110
|
+
"03d": "cloudy",
|
|
111
|
+
"03n": "cloudy",
|
|
112
|
+
"04d": "cloudy",
|
|
113
|
+
"04n": "cloudy",
|
|
114
|
+
"50d": "cloudy",
|
|
115
|
+
"50n": "cloudy",
|
|
116
|
+
// 09d, 10d - rainy
|
|
117
|
+
"09d": "rainy",
|
|
118
|
+
"09n": "rainy",
|
|
119
|
+
"10d": "rainy",
|
|
120
|
+
"10n": "rainy",
|
|
121
|
+
// 11d - thunderstorm
|
|
122
|
+
"11d": "thunderstorm",
|
|
123
|
+
"11n": "thunderstorm",
|
|
124
|
+
// 13d - snow
|
|
125
|
+
"13d": "snow",
|
|
126
|
+
"13n": "snow"
|
|
127
|
+
};
|
|
128
|
+
class Z extends HTMLElement {
|
|
129
|
+
static observedAttributes = ["city", "scale", "icon-style", "api-key"];
|
|
130
|
+
#e = null;
|
|
131
|
+
#n = this.attachShadow({ mode: "open" });
|
|
132
|
+
#t = null;
|
|
133
|
+
connectedCallback() {
|
|
134
|
+
G().then(() => {
|
|
135
|
+
this.#o(), this.#i();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
disconnectedCallback() {
|
|
139
|
+
this.#e && this.#e.abort();
|
|
140
|
+
}
|
|
141
|
+
attributeChangedCallback() {
|
|
142
|
+
this.isConnected && this.#i();
|
|
143
|
+
}
|
|
144
|
+
get city() {
|
|
145
|
+
return (this.getAttribute("city") || "").trim();
|
|
146
|
+
}
|
|
147
|
+
get scale() {
|
|
148
|
+
return Q(this.getAttribute("scale"));
|
|
149
|
+
}
|
|
150
|
+
get iconStyle() {
|
|
151
|
+
return V(this.getAttribute("icon-style"));
|
|
152
|
+
}
|
|
153
|
+
get apiKey() {
|
|
154
|
+
return (this.getAttribute("api-key") || "").trim();
|
|
155
|
+
}
|
|
156
|
+
#o() {
|
|
157
|
+
this.#n.innerHTML = `<style>${B}</style>${J}`;
|
|
158
|
+
const t = this.#n.querySelector(".weather-widget");
|
|
159
|
+
t && (this.#t = {
|
|
160
|
+
root: t,
|
|
161
|
+
loading: t.querySelector('[part="loading"]'),
|
|
162
|
+
content: t.querySelector('[part="content"]'),
|
|
163
|
+
city: t.querySelector('[part="city"]'),
|
|
164
|
+
icon: t.querySelector("ion-icon"),
|
|
165
|
+
temp: t.querySelector('[part="temperature"]'),
|
|
166
|
+
error: t.querySelector('[part="error"]')
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
#s() {
|
|
170
|
+
this.#t && (this.#t.loading.hidden = !1, this.#t.content.hidden = !0, this.#t.error.hidden = !0);
|
|
171
|
+
}
|
|
172
|
+
#r(t) {
|
|
173
|
+
this.#t && (this.#t.loading.hidden = !0, this.#t.content.hidden = !0, this.#t.error.hidden = !1, this.#t.error.textContent = t);
|
|
174
|
+
}
|
|
175
|
+
#a({ cityName: t, iconName: n, tempText: r }) {
|
|
176
|
+
this.#t && (this.#t.loading.hidden = !0, this.#t.error.hidden = !0, this.#t.content.hidden = !1, this.#t.city.textContent = t, this.#t.icon.setAttribute("name", n), this.#t.temp.textContent = r);
|
|
177
|
+
}
|
|
178
|
+
async #i() {
|
|
179
|
+
const t = this.city;
|
|
180
|
+
if (!t) {
|
|
181
|
+
this.#r("Missing city");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.#e && this.#e.abort(), this.#e = new AbortController(), this.#s();
|
|
185
|
+
try {
|
|
186
|
+
const n = this.apiKey;
|
|
187
|
+
if (!n) throw new Error("Missing API key");
|
|
188
|
+
const r = new URL("https://api.openweathermap.org/geo/1.0/direct");
|
|
189
|
+
r.searchParams.set("q", t), r.searchParams.set("limit", "1"), r.searchParams.set("appid", n);
|
|
190
|
+
const c = await fetch(r, { signal: this.#e.signal });
|
|
191
|
+
if (!c.ok) throw new Error("City not found");
|
|
192
|
+
const l = await c.json(), o = l && l[0];
|
|
193
|
+
if (!o) throw new Error("City not found");
|
|
194
|
+
const i = new URL(
|
|
195
|
+
"https://api.openweathermap.org/data/2.5/weather"
|
|
196
|
+
);
|
|
197
|
+
i.searchParams.set("lat", String(o.lat)), i.searchParams.set("lon", String(o.lon)), i.searchParams.set("units", "metric"), i.searchParams.set("appid", n);
|
|
198
|
+
const h = await fetch(i, {
|
|
199
|
+
signal: this.#e.signal
|
|
200
|
+
});
|
|
201
|
+
if (!h.ok) throw new Error("Weather not available");
|
|
202
|
+
const s = await h.json(), d = s?.main?.temp, y = this.scale === "F" ? Math.round(d * 9 / 5 + 32) : Math.round(d), u = s?.weather?.[0]?.icon, p = u && Y[u] || "help-circle", m = X(p, this.iconStyle), g = s?.name || t;
|
|
203
|
+
this.#a({
|
|
204
|
+
cityName: g,
|
|
205
|
+
iconName: m,
|
|
206
|
+
tempText: `${y}°${this.scale}`
|
|
207
|
+
});
|
|
208
|
+
} catch (n) {
|
|
209
|
+
if (n instanceof DOMException && n.name === "AbortError") return;
|
|
210
|
+
const r = n instanceof Error ? n.message : "Failed to fetch weather";
|
|
211
|
+
this.#r(r);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
customElements.get("weather-widget") || customElements.define("weather-widget", Z);
|
package/package.json
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danilosimonatto/ionicons-minimal-weather-widget",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Danilo Simonatto",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"dev": "vite build --watch",
|
|
10
|
+
"prepack": "vite build"
|
|
11
|
+
},
|
|
7
12
|
"files": [
|
|
8
|
-
"
|
|
13
|
+
"dist",
|
|
9
14
|
"weather-widget.d.ts",
|
|
10
15
|
"astro",
|
|
16
|
+
"src",
|
|
17
|
+
"vite.config.js",
|
|
11
18
|
"README.md",
|
|
12
19
|
"LICENSE"
|
|
13
20
|
],
|
|
14
21
|
"exports": {
|
|
15
22
|
".": {
|
|
16
23
|
"types": "./weather-widget.d.ts",
|
|
17
|
-
"default": "./weather-widget.js"
|
|
24
|
+
"default": "./dist/weather-widget.js"
|
|
18
25
|
},
|
|
19
26
|
"./astro": "./astro/WeatherWidget.astro"
|
|
20
27
|
},
|
|
21
28
|
"dependencies": {
|
|
22
29
|
"ionicons": "^8.0.13"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"vite": "^7.3.1"
|
|
23
33
|
}
|
|
24
|
-
}
|
|
34
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--color-primary: #000;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.weather-widget {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
color: var(--color-primary, #000);
|
|
11
|
+
text-align: center;
|
|
12
|
+
font-size: 1rem;
|
|
13
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
14
|
+
Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.error {
|
|
18
|
+
color: #ffebee;
|
|
19
|
+
background: rgba(255, 0, 0, 0.2);
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.weather-content {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
gap: 5px;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="weather-widget"
|
|
3
|
+
part="root"
|
|
4
|
+
>
|
|
5
|
+
<div
|
|
6
|
+
class="loading"
|
|
7
|
+
part="loading"
|
|
8
|
+
>
|
|
9
|
+
Loading…
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div
|
|
13
|
+
class="weather-content"
|
|
14
|
+
part="content"
|
|
15
|
+
hidden
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
class="city-name"
|
|
19
|
+
part="city"
|
|
20
|
+
></div>
|
|
21
|
+
<ion-icon
|
|
22
|
+
class="icon"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
></ion-icon>
|
|
25
|
+
<span
|
|
26
|
+
class="temperature"
|
|
27
|
+
part="temperature"
|
|
28
|
+
></span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
class="error"
|
|
33
|
+
part="error"
|
|
34
|
+
hidden
|
|
35
|
+
></div>
|
|
36
|
+
</div>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import styles from "./styles.css?raw";
|
|
2
|
+
import template from "./template.html?raw";
|
|
3
|
+
|
|
1
4
|
import { defineCustomElements } from "ionicons/loader";
|
|
2
5
|
import { addIcons } from "ionicons";
|
|
3
6
|
import {
|
|
@@ -114,20 +117,12 @@ const ICON_CODE_TO_IONICON = {
|
|
|
114
117
|
"13n": "snow",
|
|
115
118
|
};
|
|
116
119
|
|
|
117
|
-
const escapeHtml = (input) => {
|
|
118
|
-
return String(input)
|
|
119
|
-
.replaceAll("&", "&")
|
|
120
|
-
.replaceAll("<", "<")
|
|
121
|
-
.replaceAll(">", ">")
|
|
122
|
-
.replaceAll('"', """)
|
|
123
|
-
.replaceAll("'", "'");
|
|
124
|
-
};
|
|
125
|
-
|
|
126
120
|
class WeatherWidgetElement extends HTMLElement {
|
|
127
121
|
static observedAttributes = ["city", "scale", "icon-style", "api-key"];
|
|
128
122
|
|
|
129
123
|
#abort = null;
|
|
130
124
|
#shadow = this.attachShadow({ mode: "open" });
|
|
125
|
+
#els = null;
|
|
131
126
|
|
|
132
127
|
connectedCallback() {
|
|
133
128
|
ensureIonicons().then(() => {
|
|
@@ -162,89 +157,61 @@ class WeatherWidgetElement extends HTMLElement {
|
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
#renderShell() {
|
|
165
|
-
this.#shadow.innerHTML =
|
|
166
|
-
<style>
|
|
167
|
-
:host {
|
|
168
|
-
display: inline-block;
|
|
169
|
-
}
|
|
170
|
-
.weather-widget {
|
|
171
|
-
display: inline-flex;
|
|
172
|
-
flex-direction: column;
|
|
173
|
-
gap: 8px;
|
|
174
|
-
padding: 1.5rem;
|
|
175
|
-
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
176
|
-
border-radius: 16px;
|
|
177
|
-
color: white;
|
|
178
|
-
text-align: center;
|
|
179
|
-
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
180
|
-
min-width: 250px;
|
|
181
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
182
|
-
}
|
|
183
|
-
.loading,
|
|
184
|
-
.error {
|
|
185
|
-
font-size: 1rem;
|
|
186
|
-
padding: 1rem;
|
|
187
|
-
}
|
|
188
|
-
.error {
|
|
189
|
-
color: #ffebee;
|
|
190
|
-
background: rgba(255, 0, 0, 0.2);
|
|
191
|
-
border-radius: 8px;
|
|
192
|
-
}
|
|
193
|
-
.weather-content {
|
|
194
|
-
display: flex;
|
|
195
|
-
flex-direction: column;
|
|
196
|
-
gap: 12px;
|
|
197
|
-
}
|
|
198
|
-
.city-name {
|
|
199
|
-
font-size: 1.5rem;
|
|
200
|
-
font-weight: 600;
|
|
201
|
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
202
|
-
}
|
|
203
|
-
.weather {
|
|
204
|
-
display: flex;
|
|
205
|
-
gap: 12px;
|
|
206
|
-
align-items: center;
|
|
207
|
-
justify-content: center;
|
|
208
|
-
font-size: 2.5rem;
|
|
209
|
-
font-weight: 700;
|
|
210
|
-
}
|
|
211
|
-
.icon {
|
|
212
|
-
font-size: 3rem;
|
|
213
|
-
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
214
|
-
}
|
|
215
|
-
.temperature {
|
|
216
|
-
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
217
|
-
}
|
|
218
|
-
</style>
|
|
219
|
-
<div class="weather-widget" part="root">
|
|
220
|
-
<div class="loading" part="loading">Loading…</div>
|
|
221
|
-
</div>
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
#setInner(html) {
|
|
160
|
+
this.#shadow.innerHTML = `<style>${styles}</style>${template}`;
|
|
226
161
|
const root = this.#shadow.querySelector(".weather-widget");
|
|
227
162
|
if (!root) return;
|
|
228
|
-
|
|
163
|
+
|
|
164
|
+
this.#els = {
|
|
165
|
+
root,
|
|
166
|
+
loading: root.querySelector('[part="loading"]'),
|
|
167
|
+
content: root.querySelector('[part="content"]'),
|
|
168
|
+
city: root.querySelector('[part="city"]'),
|
|
169
|
+
icon: root.querySelector("ion-icon"),
|
|
170
|
+
temp: root.querySelector('[part="temperature"]'),
|
|
171
|
+
error: root.querySelector('[part="error"]'),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#showLoading() {
|
|
176
|
+
if (!this.#els) return;
|
|
177
|
+
this.#els.loading.hidden = false;
|
|
178
|
+
this.#els.content.hidden = true;
|
|
179
|
+
this.#els.error.hidden = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#showError(message) {
|
|
183
|
+
if (!this.#els) return;
|
|
184
|
+
this.#els.loading.hidden = true;
|
|
185
|
+
this.#els.content.hidden = true;
|
|
186
|
+
this.#els.error.hidden = false;
|
|
187
|
+
this.#els.error.textContent = message;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#showWeather({ cityName, iconName, tempText }) {
|
|
191
|
+
if (!this.#els) return;
|
|
192
|
+
this.#els.loading.hidden = true;
|
|
193
|
+
this.#els.error.hidden = true;
|
|
194
|
+
this.#els.content.hidden = false;
|
|
195
|
+
|
|
196
|
+
// Use textContent for safety; avoid innerHTML for dynamic content.
|
|
197
|
+
this.#els.city.textContent = cityName;
|
|
198
|
+
this.#els.icon.setAttribute("name", iconName);
|
|
199
|
+
this.#els.temp.textContent = tempText;
|
|
229
200
|
}
|
|
230
201
|
|
|
231
202
|
async #load() {
|
|
232
203
|
const city = this.city;
|
|
233
204
|
if (!city) {
|
|
234
|
-
this.#
|
|
205
|
+
this.#showError("Missing city");
|
|
235
206
|
return;
|
|
236
207
|
}
|
|
237
208
|
|
|
238
209
|
if (this.#abort) this.#abort.abort();
|
|
239
210
|
this.#abort = new AbortController();
|
|
240
211
|
|
|
241
|
-
this.#
|
|
212
|
+
this.#showLoading();
|
|
242
213
|
|
|
243
214
|
try {
|
|
244
|
-
// Mirrors the old Vue component's behavior:
|
|
245
|
-
// - OpenWeatherMap geocoding -> lat/lon
|
|
246
|
-
// - OpenWeatherMap weather in metric
|
|
247
|
-
// - Convert to Fahrenheit client-side if needed
|
|
248
215
|
const apiKey = this.apiKey;
|
|
249
216
|
if (!apiKey) throw new Error("Missing API key");
|
|
250
217
|
|
|
@@ -288,22 +255,16 @@ class WeatherWidgetElement extends HTMLElement {
|
|
|
288
255
|
|
|
289
256
|
const cityName = weather?.name || city;
|
|
290
257
|
|
|
291
|
-
this.#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<span class="temperature" part="temperature">${temp}°${this.scale}</span>
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
`);
|
|
258
|
+
this.#showWeather({
|
|
259
|
+
cityName,
|
|
260
|
+
iconName,
|
|
261
|
+
tempText: `${temp}°${this.scale}`,
|
|
262
|
+
});
|
|
300
263
|
} catch (e) {
|
|
301
264
|
if (e instanceof DOMException && e.name === "AbortError") return;
|
|
302
265
|
const message =
|
|
303
266
|
e instanceof Error ? e.message : "Failed to fetch weather";
|
|
304
|
-
this.#
|
|
305
|
-
`<div class="error" part="error">${escapeHtml(message)}</div>`
|
|
306
|
-
);
|
|
267
|
+
this.#showError(message);
|
|
307
268
|
}
|
|
308
269
|
}
|
|
309
270
|
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
// Output into dist/ to avoid overwriting source files.
|
|
7
|
+
outDir: "dist",
|
|
8
|
+
emptyOutDir: true,
|
|
9
|
+
lib: {
|
|
10
|
+
entry: path.resolve(__dirname, "src/weather-widget.entry.js"),
|
|
11
|
+
formats: ["es"],
|
|
12
|
+
fileName: () => "weather-widget.js",
|
|
13
|
+
},
|
|
14
|
+
rollupOptions: {
|
|
15
|
+
// Keep dependency behavior identical to the hand-written module:
|
|
16
|
+
// consumers will still resolve `ionicons` from this package's dependencies.
|
|
17
|
+
external: (id) => id === "ionicons" || id.startsWith("ionicons/"),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|