diff --git a/apps/stage-pocket/src/components/websocket-status-button.vue b/apps/stage-pocket/src/components/websocket-status-button.vue
new file mode 100644
index 000000000..20a19217a
--- /dev/null
+++ b/apps/stage-pocket/src/components/websocket-status-button.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ tooltipLabel }}
+
+
+
+
+
+
+
+
diff --git a/apps/stage-pocket/src/pages/index.vue b/apps/stage-pocket/src/pages/index.vue
index f7f35e97e..ac6d9e317 100644
--- a/apps/stage-pocket/src/pages/index.vue
+++ b/apps/stage-pocket/src/pages/index.vue
@@ -23,6 +23,8 @@ import { breakpointsTailwind, useBreakpoints, useMouse } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
+import WebSocketStatusButton from '../components/websocket-status-button.vue'
+
const paused = ref(false)
function handleSettingsOpen(open: boolean) {
@@ -181,6 +183,7 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
:top-color="sampledColor"
>
+
diff --git a/apps/stage-pocket/src/styles/main.css b/apps/stage-pocket/src/styles/main.css
index 6f8e46919..79f4498d4 100755
--- a/apps/stage-pocket/src/styles/main.css
+++ b/apps/stage-pocket/src/styles/main.css
@@ -1,3 +1,4 @@
+@import '@proj-airi/ui/main.css';
@import './transitions.css';
@import './vue-transitions.css';
diff --git a/apps/stage-tamagotchi/src/renderer/components/stage-islands/status-island/index.vue b/apps/stage-tamagotchi/src/renderer/components/stage-islands/status-island/index.vue
index f0847e210..e48d47c74 100644
--- a/apps/stage-tamagotchi/src/renderer/components/stage-islands/status-island/index.vue
+++ b/apps/stage-tamagotchi/src/renderer/components/stage-islands/status-island/index.vue
@@ -1,8 +1,10 @@
@@ -111,7 +66,7 @@ watch(connected, (isConnected) => {
:title="tooltipLabel"
@click="openSettings({ route: '/settings/connection' })"
>
-
+
{{ tooltipLabel }}
@@ -119,88 +74,3 @@ watch(connected, (isConnected) => {
-
-
diff --git a/apps/stage-tamagotchi/src/renderer/styles/main.css b/apps/stage-tamagotchi/src/renderer/styles/main.css
index 934424299..1bd05c606 100644
--- a/apps/stage-tamagotchi/src/renderer/styles/main.css
+++ b/apps/stage-tamagotchi/src/renderer/styles/main.css
@@ -1,3 +1,4 @@
+@import '@proj-airi/ui/main.css';
@import './transitions.css';
html,
diff --git a/apps/stage-web/src/styles/main.css b/apps/stage-web/src/styles/main.css
index 6f8e46919..79f4498d4 100755
--- a/apps/stage-web/src/styles/main.css
+++ b/apps/stage-web/src/styles/main.css
@@ -1,3 +1,4 @@
+@import '@proj-airi/ui/main.css';
@import './transitions.css';
@import './vue-transitions.css';
diff --git a/apps/ui-server-auth/src/styles/main.css b/apps/ui-server-auth/src/styles/main.css
index 6f8e46919..79f4498d4 100755
--- a/apps/ui-server-auth/src/styles/main.css
+++ b/apps/ui-server-auth/src/styles/main.css
@@ -1,3 +1,4 @@
+@import '@proj-airi/ui/main.css';
@import './transitions.css';
@import './vue-transitions.css';
diff --git a/packages/i18n/src/locales/en/stage.yaml b/packages/i18n/src/locales/en/stage.yaml
index cf43eb220..a2f255668 100644
--- a/packages/i18n/src/locales/en/stage.yaml
+++ b/packages/i18n/src/locales/en/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: Waiting
+websocket-status:
+ connected: WebSocket connected
+ disconnected: WebSocket disconnected
+ open-settings: Open WebSocket settings
diff --git a/packages/i18n/src/locales/en/tamagotchi/stage.yaml b/packages/i18n/src/locales/en/tamagotchi/stage.yaml
index 9d511a777..d14c7ccaa 100644
--- a/packages/i18n/src/locales/en/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/en/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket connected
- disconnected: WebSocket disconnected
- 'open-settings': Open WebSocket settings
notice:
'fade-on-hover':
diff --git a/packages/i18n/src/locales/es/stage.yaml b/packages/i18n/src/locales/es/stage.yaml
index 7ee7f790d..de872ee7a 100644
--- a/packages/i18n/src/locales/es/stage.yaml
+++ b/packages/i18n/src/locales/es/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: Esperando...
+websocket-status:
+ connected: WebSocket conectado
+ disconnected: WebSocket desconectado
+ open-settings: Abrir ajustes de WebSocket
diff --git a/packages/i18n/src/locales/es/tamagotchi/stage.yaml b/packages/i18n/src/locales/es/tamagotchi/stage.yaml
index 079f9dbbf..1ee50fc19 100644
--- a/packages/i18n/src/locales/es/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/es/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket conectado
- disconnected: WebSocket desconectado
- 'open-settings': Abrir ajustes de WebSocket
notice:
'fade-on-hover':
title: Desvanecer al pasar el cursor
diff --git a/packages/i18n/src/locales/fr/stage.yaml b/packages/i18n/src/locales/fr/stage.yaml
index 1ee4c2ed2..91fd21844 100644
--- a/packages/i18n/src/locales/fr/stage.yaml
+++ b/packages/i18n/src/locales/fr/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Ticket de tirage de thème gratuit à la connexion
cta: Voir le thème
waiting: En attente
+websocket-status:
+ connected: WebSocket connecté
+ disconnected: WebSocket déconnecté
+ open-settings: Ouvrir les paramètres WebSocket
diff --git a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml
index ddf9f9217..12756de3f 100644
--- a/packages/i18n/src/locales/fr/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/fr/tamagotchi/stage.yaml
@@ -25,6 +25,10 @@ docs:
'close': Fermer
'expand': Développer
'collapse': Réduire
+ 'login': Sign in
+ 'logging-in': Signing in...
+ 'account': Account
+ 'logout': Sign out
'login': Se connecter
'logging-in': Connexion en cours...
'account': Compte
diff --git a/packages/i18n/src/locales/ja/stage.yaml b/packages/i18n/src/locales/ja/stage.yaml
index 25ac68991..fd37385f8 100644
--- a/packages/i18n/src/locales/ja/stage.yaml
+++ b/packages/i18n/src/locales/ja/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: ログイン時の無料テーマ抽選チケット
cta: テーマを表示
waiting: 待機中
+websocket-status:
+ connected: WebSocket が接続されました
+ disconnected: WebSocket が切断されました
+ open-settings: WebSocketの設定を開く
diff --git a/packages/i18n/src/locales/ja/tamagotchi/stage.yaml b/packages/i18n/src/locales/ja/tamagotchi/stage.yaml
index cd3b0c3f5..e058d76c3 100644
--- a/packages/i18n/src/locales/ja/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/ja/tamagotchi/stage.yaml
@@ -25,6 +25,10 @@ docs:
'close': 閉じる
'expand': 展開
'collapse': 折りたたむ
+ 'login': Sign in
+ 'logging-in': Signing in...
+ 'account': Account
+ 'logout': Sign out
'login': サインイン
'logging-in': サインイン中…
'account': アカウント
diff --git a/packages/i18n/src/locales/ko/stage.yaml b/packages/i18n/src/locales/ko/stage.yaml
index 6b28e1adf..da4040c10 100644
--- a/packages/i18n/src/locales/ko/stage.yaml
+++ b/packages/i18n/src/locales/ko/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: 대기 중
+websocket-status:
+ connected: WebSocket에 연결됨
+ disconnected: WebSocket 연결 끊김
+ open-settings: WebSocket 설정 열기
diff --git a/packages/i18n/src/locales/ko/tamagotchi/stage.yaml b/packages/i18n/src/locales/ko/tamagotchi/stage.yaml
index 0aa14b8f2..2f589b2aa 100644
--- a/packages/i18n/src/locales/ko/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/ko/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket connected
- disconnected: WebSocket disconnected
- 'open-settings': Open WebSocket settings
notice:
'fade-on-hover':
title: 호버 시 페이드
diff --git a/packages/i18n/src/locales/ru/stage.yaml b/packages/i18n/src/locales/ru/stage.yaml
index 8b3536286..dcd110179 100644
--- a/packages/i18n/src/locales/ru/stage.yaml
+++ b/packages/i18n/src/locales/ru/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: Ожидание
+websocket-status:
+ connected: WebSocket подключен
+ disconnected: WebSocket отключен
+ open-settings: Открыть настройки WebSocket
diff --git a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml
index 07fa5b6de..fc9d00275 100644
--- a/packages/i18n/src/locales/ru/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/ru/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket подключен
- disconnected: WebSocket отключен
- 'open-settings': Открыть настройки WebSocket
notice:
'fade-on-hover':
title: Исчезать при наведении
diff --git a/packages/i18n/src/locales/vi/stage.yaml b/packages/i18n/src/locales/vi/stage.yaml
index d72cb8ec6..e76d9cad9 100644
--- a/packages/i18n/src/locales/vi/stage.yaml
+++ b/packages/i18n/src/locales/vi/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: Đang chờ
+websocket-status:
+ connected: WebSocket đã kết nối
+ disconnected: WebSocket đã ngắt kết nối
+ open-settings: Mở cài đặt WebSocket
diff --git a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml
index c8b94346d..9aea540df 100644
--- a/packages/i18n/src/locales/vi/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/vi/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket connected
- disconnected: WebSocket disconnected
- 'open-settings': Open WebSocket settings
notice:
'fade-on-hover':
title: Mờ dần khi rê chuột
diff --git a/packages/i18n/src/locales/zh-Hans/stage.yaml b/packages/i18n/src/locales/zh-Hans/stage.yaml
index 628e11646..14236e962 100644
--- a/packages/i18n/src/locales/zh-Hans/stage.yaml
+++ b/packages/i18n/src/locales/zh-Hans/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: 等待中
+websocket-status:
+ connected: WebSocket 已连接
+ disconnected: WebSocket 已断开
+ open-settings: 打开 WebSocket 设置
diff --git a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml
index 1574543fd..5f9127129 100644
--- a/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/zh-Hans/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket 已连接
- disconnected: WebSocket 已断开
- 'open-settings': 打开 WebSocket 设置
notice:
'fade-on-hover':
title: 悬停淡出
diff --git a/packages/i18n/src/locales/zh-Hant/stage.yaml b/packages/i18n/src/locales/zh-Hant/stage.yaml
index 647da61a1..c2a6e0b46 100644
--- a/packages/i18n/src/locales/zh-Hant/stage.yaml
+++ b/packages/i18n/src/locales/zh-Hant/stage.yaml
@@ -84,3 +84,7 @@ promo-banner:
reward: Free Theme Draw Ticket on Login
cta: View Theme
waiting: 等待中
+websocket-status:
+ connected: WebSocket 已連線
+ disconnected: WebSocket 已中斷
+ open-settings: 開啟 WebSocket 設定
diff --git a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml
index efbfefe1a..4acaaf657 100644
--- a/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml
+++ b/packages/i18n/src/locales/zh-Hant/tamagotchi/stage.yaml
@@ -29,10 +29,6 @@ docs:
'logging-in': Signing in...
'account': Account
'logout': Sign out
-'status-island':
- connected: WebSocket connected
- disconnected: WebSocket disconnected
- 'open-settings': Open WebSocket settings
notice:
'fade-on-hover':
title: 懸停淡出
diff --git a/packages/stage-ui/src/composables/index.ts b/packages/stage-ui/src/composables/index.ts
index 35f775302..e0d405c3d 100644
--- a/packages/stage-ui/src/composables/index.ts
+++ b/packages/stage-ui/src/composables/index.ts
@@ -8,6 +8,7 @@ export * from './use-async-state'
export * from './use-breakpoints'
export * from './use-build-info'
export * from './use-chat-session/summary'
+export * from './use-lamp-flicker-animation'
export * from './use-optimistic'
export * from './use-scroll-to-hash'
export * from './vision'
diff --git a/packages/stage-ui/src/composables/use-lamp-flicker-animation.ts b/packages/stage-ui/src/composables/use-lamp-flicker-animation.ts
new file mode 100644
index 000000000..1f72a1062
--- /dev/null
+++ b/packages/stage-ui/src/composables/use-lamp-flicker-animation.ts
@@ -0,0 +1,64 @@
+import type { MaybeRefOrGetter } from 'vue'
+
+import { lampFlickerAnimationClass } from '@proj-airi/ui'
+import { computed, ref, toValue, watch } from 'vue'
+
+export { lampFlickerAnimationClass }
+
+const LAMP_FLICKER_DELAY_VAR = '--lamp-flicker-delay'
+const LAMP_FLICKER_DURATION_VAR = '--lamp-flicker-duration'
+
+/**
+ * Drives randomized keyframe timing for `.lamp-flicker-animation` from `@proj-airi/ui/main.css` while `flickerActive` is true.
+ * Import `@proj-airi/ui/main.css` from the app global stylesheet (e.g. `styles/main.css`). When inactive, delay resets to 0s.
+ */
+export function useLampFlickerAnimation(flickerActive: MaybeRefOrGetter
) {
+ const flickerDuration = ref('6.4s')
+ const flickerDelay = ref('0s')
+
+ function randomizeFlicker(resetPhase = false) {
+ flickerDuration.value = `${(5.8 + Math.random() * 1.8).toFixed(2)}s`
+
+ if (resetPhase) {
+ flickerDelay.value = `${(-Math.random() * 5.4).toFixed(2)}s`
+ return
+ }
+
+ flickerDelay.value = '0s'
+ }
+
+ function onAnimationIteration() {
+ if (toValue(flickerActive)) {
+ randomizeFlicker()
+ }
+ }
+
+ watch(
+ () => toValue(flickerActive),
+ (active) => {
+ if (!active) {
+ flickerDelay.value = '0s'
+ return
+ }
+
+ randomizeFlicker(true)
+ },
+ { immediate: true },
+ )
+
+ const flickerStyle = computed(() => {
+ if (!toValue(flickerActive)) {
+ return undefined
+ }
+
+ return {
+ [LAMP_FLICKER_DELAY_VAR]: flickerDelay.value,
+ [LAMP_FLICKER_DURATION_VAR]: flickerDuration.value,
+ } as Record
+ })
+
+ return {
+ flickerStyle,
+ onAnimationIteration,
+ }
+}
diff --git a/packages/stage-ui/stories/styles/main.css b/packages/stage-ui/stories/styles/main.css
index 9ddd51918..8bff20b7b 100755
--- a/packages/stage-ui/stories/styles/main.css
+++ b/packages/stage-ui/stories/styles/main.css
@@ -1,3 +1,4 @@
+@import '@proj-airi/ui/main.css';
@import './transitions.css';
html,
diff --git a/packages/ui/package.json b/packages/ui/package.json
index f05fd63ba..beaff17ca 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -16,6 +16,7 @@
},
"exports": {
".": "./src/index.ts",
+ "./main.css": "./src/main.css",
"./components/animations": "./src/components/animations/index.ts",
"./components/form": "./src/components/form/index.ts",
"./components/layouts": "./src/components/layouts/index.ts",
diff --git a/packages/ui/src/constants/lamp-flicker-animation.ts b/packages/ui/src/constants/lamp-flicker-animation.ts
new file mode 100644
index 000000000..7051e4498
--- /dev/null
+++ b/packages/ui/src/constants/lamp-flicker-animation.ts
@@ -0,0 +1,2 @@
+/** Global class from `@proj-airi/ui/main.css`; pairs with useLampFlickerAnimation in stage-ui. */
+export const lampFlickerAnimationClass = 'lamp-flicker-animation' as const
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 7197811ba..704aa0693 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -1,8 +1,7 @@
-import './fallback.css'
-
export * from './components/animations'
export * from './components/form'
export * from './components/layouts'
export * from './components/misc'
export * from './composables/use-deferred-mount'
export * from './composables/use-theme'
+export { lampFlickerAnimationClass } from './constants/lamp-flicker-animation'
diff --git a/packages/ui/src/lamp-flicker.css b/packages/ui/src/lamp-flicker.css
new file mode 100644
index 000000000..24f613c91
--- /dev/null
+++ b/packages/ui/src/lamp-flicker.css
@@ -0,0 +1,88 @@
+/**
+ * Opacity “lamp flicker” for offline / warning icons.
+ * Timing is driven by useLampFlickerAnimation (@proj-airi/stage-ui) via
+ * --lamp-flicker-delay and --lamp-flicker-duration.
+ * Loaded via @proj-airi/ui/main.css (import from app `styles/main.css`).
+ */
+@keyframes lamp-flicker-keyframes {
+ 0%,
+ 6%,
+ 22%,
+ 33%,
+ 52%,
+ 68%,
+ 86%,
+ 100% {
+ opacity: 1;
+ }
+
+ 3% {
+ opacity: 0.74;
+ }
+
+ 9% {
+ opacity: 0.92;
+ }
+
+ 13% {
+ opacity: 0.38;
+ }
+
+ 18% {
+ opacity: 0.58;
+ }
+
+ 27% {
+ opacity: 0.44;
+ }
+
+ 29% {
+ opacity: 0.84;
+ }
+
+ 41% {
+ opacity: 0.42;
+ }
+
+ 45% {
+ opacity: 0.88;
+ }
+
+ 57% {
+ opacity: 0.62;
+ }
+
+ 61% {
+ opacity: 0.8;
+ }
+
+ 73% {
+ opacity: 0.36;
+ }
+
+ 74.4% {
+ opacity: 0.08;
+ }
+
+ 75.2% {
+ opacity: 0.82;
+ }
+
+ 78% {
+ opacity: 0.94;
+ }
+
+ 91% {
+ opacity: 0.52;
+ }
+}
+
+.lamp-flicker-animation {
+ animation-delay: var(--lamp-flicker-delay, 0s);
+ animation-duration: var(--lamp-flicker-duration, 6.4s);
+ animation-iteration-count: infinite;
+ animation-name: lamp-flicker-keyframes;
+ animation-timing-function: ease-in-out;
+ filter: drop-shadow(0 0 0.14rem rgb(251 191 36 / 0.18));
+ will-change: opacity;
+}
diff --git a/packages/ui/src/main.css b/packages/ui/src/main.css
new file mode 100644
index 000000000..a01ca7db9
--- /dev/null
+++ b/packages/ui/src/main.css
@@ -0,0 +1,6 @@
+/**
+ * Global styles for @proj-airi/ui. Import once from the app entry stylesheet
+ * (e.g. `src/styles/main.css`) so CSS is not tied to JS side-effect imports.
+ */
+@import './fallback.css';
+@import './lamp-flicker.css';