Skip to content

Commit 51b91b8

Browse files
committed
feat(stage-tamagotchi): improved weather widget
1 parent c69efa6 commit 51b91b8

File tree

10 files changed

+315
-27
lines changed

10 files changed

+315
-27
lines changed

apps/stage-pocket/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"@iconify-json/svg-spinners": "^1.2.4",
112112
"@iconify-json/vscode-icons": "^1.2.37",
113113
"@intlify/unplugin-vue-i18n": "^11.0.1",
114+
"@proj-airi/iconify-meteocons": "catalog:",
114115
"@proj-airi/lobe-icons": "^1.0.18",
115116
"@proj-airi/unplugin-fetch": "^0.2.1",
116117
"@proj-airi/unplugin-live2d-sdk": "^0.1.6",

apps/stage-tamagotchi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"@iconify-json/vscode-icons": "^1.2.37",
137137
"@iconify/utils": "^3.1.0",
138138
"@intlify/unplugin-vue-i18n": "^11.0.1",
139+
"@proj-airi/iconify-meteocons": "catalog:",
139140
"@proj-airi/lobe-icons": "^1.0.18",
140141
"@proj-airi/stage-shared": "workspace:^",
141142
"@proj-airi/ui-transitions": "workspace:^",

apps/stage-tamagotchi/src/renderer/pages/devtools/widgets-calling.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ const clearWidgets = useElectronEventaInvoke(widgetsClear)
2727
const defaultWeatherProps = {
2828
city: 'Tokyo',
2929
temperature: '15°C',
30-
condition: 'Sunny',
30+
condition: 'Light rain',
31+
high: '18°C',
32+
low: '12°C',
33+
humidity: '72%',
34+
wind: '3 m/s',
35+
precipitation: '40%',
36+
}
3137
}
3238
3339
const form = reactive<FormState>({

apps/stage-tamagotchi/src/renderer/pages/widgets.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function handleClose() {
195195
</script>
196196

197197
<template>
198-
<div class="h-full w-full p-3">
198+
<div class="h-full w-full">
199199
<div v-if="!widgetId" class="h-full flex items-center justify-center">
200200
<div class="border border-neutral-200/20 rounded-xl bg-neutral-900/40 px-4 py-3 text-sm text-neutral-200/80 backdrop-blur">
201201
Missing widget id. Launch the window via a component call to populate this view.
@@ -214,6 +214,7 @@ function handleClose() {
214214
:key="widget.id"
215215
:title="widget.componentName"
216216
:model-value="widget.componentProps"
217+
:size="widget.size"
217218
v-bind="widget.componentProps"
218219
/>
219220
</div>
@@ -223,7 +224,7 @@ function handleClose() {
223224
</div>
224225
</div>
225226
</div>
226-
<div class="[-webkit-app-region:drag] pointer-events-none absolute left-1/2 top-1 h-[14px] w-[36px] rounded-[10px] bg-[rgba(125,125,125,0.28)] backdrop-blur-[6px] -translate-x-1/2">
227+
<div class="[-webkit-app-region:drag] pointer-events-none absolute left-1/2 top-2 h-[14px] w-[36px] rounded-[10px] bg-[rgba(125,125,125,0.28)] backdrop-blur-[6px] -translate-x-1/2">
227228
<div class="absolute left-1/2 top-1/2 h-[3px] w-4 rounded-full bg-[rgba(255,255,255,0.85)] -translate-x-1/2 -translate-y-1/2" />
228229
</div>
229230
</template>
Lines changed: 269 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,285 @@
11
<script setup lang="ts">
2-
import ClearDay from '../assets/clear-day.svg'
2+
import { computed } from 'vue'
3+
34
import Skeleton from './Skeleton.vue'
45
5-
const props = defineProps<{
6-
propsLoading: boolean
6+
type WeatherEffect = 'rain' | 'snow' | 'thunder' | 'fog' | 'cloudy' | 'none'
7+
type SizePreset = 's' | 'm' | 'l' | { cols?: number, rows?: number }
8+
9+
const props = withDefaults(defineProps<{
10+
propsLoading?: boolean
711
812
city?: string
913
temperature?: string
1014
condition?: string
11-
}>()
15+
icon?: string
16+
conditionCode?: string
17+
isNight?: boolean
18+
effect?: WeatherEffect
19+
size?: SizePreset
20+
high?: string
21+
low?: string
22+
feelsLike?: string
23+
humidity?: string
24+
wind?: string
25+
precipitation?: string
26+
}>(), {
27+
propsLoading: false,
28+
isNight: false,
29+
})
30+
31+
const weatherIconMap = {
32+
'clear-day': { icon: 'i-iconify-meteocons:clear-day-fill', effect: 'none', nightKey: 'clear-night' },
33+
'clear-night': { icon: 'i-iconify-meteocons:clear-night-fill', effect: 'none' },
34+
'partly-cloudy-day': { icon: 'i-iconify-meteocons:partly-cloudy-day-fill', effect: 'cloudy', nightKey: 'partly-cloudy-night' },
35+
'partly-cloudy-night': { icon: 'i-iconify-meteocons:partly-cloudy-night-fill', effect: 'cloudy' },
36+
'cloudy': { icon: 'i-iconify-meteocons:cloudy-fill', effect: 'cloudy' },
37+
'overcast': { icon: 'i-iconify-meteocons:overcast-fill', effect: 'cloudy' },
38+
'rain': { icon: 'i-iconify-meteocons:rain-fill', effect: 'rain' },
39+
'drizzle': { icon: 'i-iconify-meteocons:drizzle-fill', effect: 'rain' },
40+
'extreme-rain': { icon: 'i-iconify-meteocons:extreme-rain-fill', effect: 'rain' },
41+
'thunderstorm': { icon: 'i-iconify-meteocons:thunderstorms-fill', effect: 'thunder' },
42+
'snow': { icon: 'i-iconify-meteocons:snow-fill', effect: 'snow' },
43+
'extreme-snow': { icon: 'i-iconify-meteocons:extreme-snow-fill', effect: 'snow' },
44+
'sleet': { icon: 'i-iconify-meteocons:sleet-fill', effect: 'snow' },
45+
'hail': { icon: 'i-iconify-meteocons:hail-fill', effect: 'snow' },
46+
'fog': { icon: 'i-iconify-meteocons:fog-fill', effect: 'fog' },
47+
'mist': { icon: 'i-iconify-meteocons:mist-fill', effect: 'fog' },
48+
'haze': { icon: 'i-iconify-meteocons:haze-fill', effect: 'fog' },
49+
'dust': { icon: 'i-iconify-meteocons:dust-fill', effect: 'fog' },
50+
'smoke': { icon: 'i-iconify-meteocons:smoke-fill', effect: 'fog' },
51+
'wind': { icon: 'i-iconify-meteocons:wind-fill', effect: 'cloudy' },
52+
'tornado': { icon: 'i-iconify-meteocons:tornado-fill', effect: 'thunder' },
53+
'hurricane': { icon: 'i-iconify-meteocons:hurricane-fill', effect: 'thunder' },
54+
} as Record<string, { icon: string, effect?: WeatherEffect, nightKey?: string }>
55+
56+
const normalizedCondition = computed(() => (props.condition ?? '').toLowerCase().trim())
57+
const normalizedCode = computed(() => (props.conditionCode ?? '').toLowerCase().trim())
58+
const isLarge = computed(() => props.size === 'l')
59+
60+
const conditionMappings: Array<{ keywords: string[], key: string }> = [
61+
{ keywords: ['thunder', 'storm', 'lightning'], key: 'thunderstorm' },
62+
{ keywords: ['hurricane', 'typhoon', 'cyclone'], key: 'hurricane' },
63+
{ keywords: ['tornado'], key: 'tornado' },
64+
{ keywords: ['hail'], key: 'hail' },
65+
{ keywords: ['sleet', 'ice'], key: 'sleet' },
66+
{ keywords: ['snow', 'blizzard', 'flurries'], key: 'snow' },
67+
{ keywords: ['extreme rain', 'heavy rain', 'downpour'], key: 'extreme-rain' },
68+
{ keywords: ['rain', 'shower'], key: 'rain' },
69+
{ keywords: ['drizzle', 'sprinkle'], key: 'drizzle' },
70+
{ keywords: ['fog'], key: 'fog' },
71+
{ keywords: ['mist'], key: 'mist' },
72+
{ keywords: ['haze'], key: 'haze' },
73+
{ keywords: ['dust', 'sand'], key: 'dust' },
74+
{ keywords: ['smoke'], key: 'smoke' },
75+
{ keywords: ['overcast'], key: 'overcast' },
76+
{ keywords: ['cloud'], key: 'cloudy' },
77+
{ keywords: ['partly', 'mostly', 'scattered'], key: 'partly-cloudy-day' },
78+
{ keywords: ['wind', 'breezy', 'gust'], key: 'wind' },
79+
{ keywords: ['clear', 'sun', 'fair'], key: 'clear-day' },
80+
]
81+
82+
function isWeatherIconKey(input: string): input is string {
83+
return input in weatherIconMap
84+
}
85+
86+
const resolvedIconKey = computed<string>(() => {
87+
if (props.icon)
88+
return props.icon
89+
90+
const direct = normalizedCode.value
91+
if (direct && isWeatherIconKey(direct))
92+
return direct
93+
94+
const match = conditionMappings.find(({ keywords }) => keywords.some(keyword => normalizedCondition.value.includes(keyword)))
95+
const fallback: string = props.isNight ? 'clear-night' : 'clear-day'
96+
const baseKey = match?.key ?? fallback
97+
const config = weatherIconMap[baseKey]
98+
99+
if (props.isNight && config.nightKey)
100+
return config.nightKey
101+
102+
return baseKey
103+
})
104+
105+
const resolvedIconClass = computed(() => weatherIconMap[resolvedIconKey.value].icon)
12106
</script>
13107

14108
<template>
15-
<div h-full w-full>
16-
<Skeleton v-if="props.propsLoading" rounded-2xl py-2 pl-3 pr-1 class="grid grid-cols-4 grid-rows-3 max-h-35 gap-2">
17-
<Skeleton animation="wave" class="grid-col-span-3 h-[1lh] w-20% rounded-2xl" />
18-
<div class="col-span-1 row-span-2 h-20 w-20 justify-self-end" />
19-
<Skeleton animation="wave" class="col-span-2 row-span-2 h-full w-20% inline-flex items-end rounded-2xl text-gray-600 font-thin dark:text-gray-300" />
20-
<Skeleton animation="wave" class="col-span-2 row-span-1 h-full w-20% inline-flex items-end justify-self-end rounded-2xl pr-4 text-gray-500 dark:text-gray-400" />
109+
<div :class="['relative', 'h-full', 'w-full', 'font-sans']">
110+
<Skeleton
111+
v-if="props.propsLoading"
112+
:class="['grid', 'grid-cols-4', 'grid-rows-3', 'max-h-35', 'gap-2', 'rounded-2xl', 'py-2', 'pl-3', 'pr-1']"
113+
>
114+
<Skeleton
115+
animation="wave"
116+
:class="['col-span-3', 'h-[1lh]', 'w-20%', 'rounded-2xl']"
117+
/>
118+
<div
119+
:class="['col-span-1', 'row-span-2', 'h-20', 'w-20', 'justify-self-end']"
120+
/>
121+
<Skeleton
122+
animation="wave"
123+
:class="['col-span-2', 'row-span-2', 'h-full', 'w-20%', 'inline-flex', 'items-end', 'rounded-2xl', 'text-gray-600', 'font-thin', 'dark:text-gray-300']"
124+
/>
125+
<Skeleton
126+
animation="wave"
127+
:class="['col-span-2', 'row-span-1', 'h-full', 'w-20%', 'inline-flex', 'items-end', 'justify-self-end', 'rounded-2xl', 'pr-4', 'text-gray-500', 'dark:text-gray-400']"
128+
/>
21129
</Skeleton>
22-
<div v-else bg="blue-100 dark:blue-900" rounded-2xl py-2 pl-3 pr-1 class="grid grid-cols-4 grid-rows-3 h-full gap-2">
23-
<div class="grid-col-span-2 text-lg font-semibold">
24-
{{ props.city }}
130+
<div
131+
v-else
132+
:class="[
133+
'relative',
134+
'flex',
135+
'h-full',
136+
'flex-col',
137+
'justify-between',
138+
'gap-3',
139+
'overflow-x-hidden overflow-y-scroll',
140+
'rounded-2xl',
141+
'p-3',
142+
'bg-gradient-to-br',
143+
'from-neutral-950',
144+
'via-neutral-900',
145+
'to-neutral-800',
146+
'text-white',
147+
]"
148+
>
149+
<div
150+
aria-hidden="true"
151+
:class="[
152+
'pointer-events-none',
153+
'absolute',
154+
'-right-8',
155+
'-top-10',
156+
'size-32',
157+
'rounded-full',
158+
'bg-gradient-to-br',
159+
'from-white/30',
160+
'via-white/5',
161+
'to-transparent',
162+
'blur-xl',
163+
]"
164+
/>
165+
<div v-if="isLarge" :class="['relative', 'z-1', 'flex', 'h-full', 'flex-col', 'justify-between', 'gap-4']">
166+
<div :class="['flex', 'items-center', 'justify-center']">
167+
<div
168+
aria-hidden="true"
169+
:class="[
170+
'text-[6rem]',
171+
'leading-none',
172+
'text-white/95',
173+
'drop-shadow-[0_12px_30px_rgba(0,0,0,0.55)]',
174+
resolvedIconClass,
175+
]"
176+
/>
177+
</div>
178+
<div :class="['flex', 'flex-col', 'items-center', 'gap-2']">
179+
<div :class="['text-[4rem]', 'font-semibold', 'leading-none']">
180+
{{ props.temperature ?? '--' }}
181+
</div>
182+
<div :class="['text-lg', 'text-white/85']">
183+
{{ props.condition ?? '—' }}
184+
</div>
185+
<div :class="['text-sm', 'text-white/55']">
186+
{{ props.city ?? 'Unknown' }}
187+
</div>
188+
</div>
189+
<div :class="['h-px', 'w-full', 'bg-white/10']" />
190+
<div :class="['grid', 'grid-cols-3', 'gap-3', 'text-white/80']">
191+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
192+
<div :class="['text-4xl', 'leading-none', 'i-iconify-meteocons:wind-fill-static']" />
193+
<div :class="['text-sm', 'font-semibold']">
194+
{{ props.wind ?? '--' }}
195+
</div>
196+
<div :class="['text-xs', 'text-white/60']">
197+
Wind
198+
</div>
199+
</div>
200+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
201+
<div :class="['text-4xl', 'leading-none', 'i-iconify-meteocons:raindrops-fill-static']" />
202+
<div :class="['text-sm', 'font-semibold']">
203+
{{ props.precipitation ?? '--' }}
204+
</div>
205+
<div :class="['text-xs', 'text-white/60']">
206+
Chance of rain
207+
</div>
208+
</div>
209+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
210+
<div :class="['text-4xl', 'leading-none', 'i-iconify-meteocons:humidity-fill-static']" />
211+
<div :class="['text-sm', 'font-semibold']">
212+
{{ props.humidity ?? '--' }}
213+
</div>
214+
<div :class="['text-xs', 'text-white/60']">
215+
Humidity
216+
</div>
217+
</div>
218+
</div>
25219
</div>
26-
<img :src="ClearDay" alt="Weather Icon" class="col-span-2 row-span-2 h-full w-auto justify-self-end">
27-
<div class="col-span-2 row-span-2 h-full inline-flex items-end text-gray-600 font-thin dark:text-gray-300">
28-
<span class="text-[3.5rem] font-thin leading-[1]">{{ props.temperature }}</span>
29-
</div>
30-
<div class="col-span-2 row-span-2 h-full w-full inline-flex items-end justify-end pr-4 text-gray-500 dark:text-gray-400">
31-
<span>{{ props.condition }}</span>
220+
<div v-else :class="['relative', 'z-1', 'flex', 'flex-1', 'flex-col', 'gap-3']">
221+
<div :class="['flex', 'items-start', 'justify-between', 'gap-4', 'flex-1', 'p-2']">
222+
<div
223+
aria-hidden="true"
224+
:class="[
225+
'size-40 scale-120',
226+
'leading-none',
227+
'text-white/90',
228+
'drop-shadow-[0_10px_22px_rgba(0,0,0,0.5)]',
229+
resolvedIconClass,
230+
]"
231+
/>
232+
<div :class="['flex', 'flex-col', 'items-end', 'gap-2']">
233+
<div :class="['text-sm', 'tracking-wide', 'text-white/80']">
234+
{{ props.condition ?? '—' }}
235+
</div>
236+
<div :class="['text-[3.5rem]', 'font-semibold', 'leading-none']">
237+
{{ props.temperature ?? '--' }}
238+
</div>
239+
<div :class="['text-xs', 'text-white/60']">
240+
{{ props.city ?? 'Unknown' }}
241+
</div>
242+
</div>
243+
</div>
244+
<div :class="['h-px', 'w-full', 'bg-white/10']" />
245+
<div :class="['grid', 'grid-cols-3', 'gap-2', 'text-white/80', 'flex-shrink-0', 'pb-4']">
246+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
247+
<div :class="['text-4xl scale-120', 'leading-none', 'i-iconify-meteocons:wind-fill-static']" />
248+
<div :class="['text-sm', 'font-semibold']">
249+
{{ props.wind ?? '--' }}
250+
</div>
251+
<div :class="['text-xs', 'text-white/60']">
252+
Wind
253+
</div>
254+
</div>
255+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
256+
<div :class="['text-4xl scale-120', 'leading-none', 'i-iconify-meteocons:raindrops-fill-static']" />
257+
<div :class="['text-sm', 'font-semibold']">
258+
{{ props.precipitation ?? '--' }}
259+
</div>
260+
<div :class="['text-xs', 'text-white/60']">
261+
Chance of rain
262+
</div>
263+
</div>
264+
<div :class="['flex', 'flex-col', 'items-center', 'gap-1']">
265+
<div :class="['text-4xl scale-120', 'leading-none', 'i-iconify-meteocons:humidity-fill-static']" />
266+
<div :class="['text-sm', 'font-semibold']">
267+
{{ props.humidity ?? '--' }}
268+
</div>
269+
<div :class="['text-xs', 'text-white/60']">
270+
Humidity
271+
</div>
272+
</div>
273+
</div>
32274
</div>
33275
</div>
34276
</div>
35277
</template>
278+
279+
<style scoped>
280+
</style>
281+
282+
<route lang="yaml">
283+
meta:
284+
layout: plain
285+
</route>

apps/stage-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"@iconify-json/tabler": "catalog:",
112112
"@iconify-json/vscode-icons": "^1.2.37",
113113
"@intlify/unplugin-vue-i18n": "^11.0.1",
114+
"@proj-airi/iconify-meteocons": "catalog:",
114115
"@proj-airi/lobe-icons": "^1.0.18",
115116
"@proj-airi/unplugin-fetch": "^0.2.1",
116117
"@proj-airi/unplugin-live2d-sdk": "^0.1.6",

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ words:
164164
- mediabunny
165165
- mediapipe
166166
- MeshToonMaterial
167+
- meteocons
167168
- micvad
168169
- mineflayer
169170
- mingcute

0 commit comments

Comments
 (0)