feat(stage-pocket): WebSocket status indicator + shared i18n (#1586)
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { useLampFlickerAnimation } from '@proj-airi/stage-ui/composables/use-lamp-flicker-animation'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { lampFlickerAnimationClass } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const { connected } = storeToRefs(useModsServerChannelStore())
|
||||
|
||||
const { flickerStyle, onAnimationIteration } = useLampFlickerAnimation(() => !connected.value)
|
||||
|
||||
const statusSize = {
|
||||
border: 'border-2',
|
||||
icon: 'size-6',
|
||||
padding: 'p-2.5',
|
||||
} as const
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return [
|
||||
statusSize.border,
|
||||
statusSize.padding,
|
||||
'transition-all duration-300 ease-in-out',
|
||||
'border-solid rounded-xl backdrop-blur-md',
|
||||
'w-fit flex items-center self-end justify-center',
|
||||
connected.value
|
||||
? 'border-emerald-200/60 bg-white/85 hover:bg-emerald-50/90 dark:border-emerald-400/15 dark:bg-neutral-900/75 dark:hover:bg-neutral-900/88'
|
||||
: 'border-amber-300/80 bg-amber-50/90 hover:bg-amber-100/90 dark:border-amber-400/30 dark:bg-amber-950/25 dark:hover:bg-amber-950/38',
|
||||
]
|
||||
})
|
||||
|
||||
const iconClasses = computed(() => {
|
||||
return [
|
||||
connected.value ? 'i-ph:wifi-high' : `i-ph:wifi-slash ${lampFlickerAnimationClass}`,
|
||||
statusSize.icon,
|
||||
'shrink-0 transition-colors duration-300 ease-in-out',
|
||||
connected.value
|
||||
? 'text-emerald-600 dark:text-emerald-300'
|
||||
: 'text-amber-600 dark:text-amber-300',
|
||||
]
|
||||
})
|
||||
|
||||
const buttonLabel = computed(() => {
|
||||
return connected.value
|
||||
? t('stage.websocket-status.connected')
|
||||
: t('stage.websocket-status.disconnected')
|
||||
})
|
||||
|
||||
const tooltipLabel = computed(() => {
|
||||
return `${buttonLabel.value}. ${t('stage.websocket-status.open-settings')}`
|
||||
})
|
||||
|
||||
function openConnectionSettings() {
|
||||
void router.push('/settings/connection')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fixed right-3 z-20"
|
||||
:style="{ top: 'max(0.75rem, env(safe-area-inset-top, 0px))' }"
|
||||
>
|
||||
<TooltipProvider :delay-duration="0" :skip-delay-duration="0">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
:class="buttonClass"
|
||||
:aria-label="tooltipLabel"
|
||||
:title="tooltipLabel"
|
||||
@click="openConnectionSettings"
|
||||
>
|
||||
<div
|
||||
:class="iconClasses"
|
||||
:style="flickerStyle"
|
||||
@animationiteration="onAnimationIteration"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<Transition name="pocket-ws-tooltip-fade">
|
||||
<TooltipContent
|
||||
:class="[
|
||||
'border border-solid border-neutral-200/60 dark:border-neutral-800/10',
|
||||
'bg-neutral-50/80 dark:bg-neutral-800/70',
|
||||
'w-fit flex items-center px-1.5 py-1 rounded-lg backdrop-blur-md text-xs',
|
||||
]"
|
||||
side="left"
|
||||
:side-offset="4"
|
||||
>
|
||||
{{ tooltipLabel }}
|
||||
</TooltipContent>
|
||||
</Transition>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pocket-ws-tooltip-fade-enter-active,
|
||||
.pocket-ws-tooltip-fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.pocket-ws-tooltip-fade-enter-from,
|
||||
.pocket-ws-tooltip-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pocket-ws-tooltip-fade-enter-to,
|
||||
.pocket-ws-tooltip-fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -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"
|
||||
>
|
||||
<div flex="~ col" relative z-2 h-100dvh w-100vw of-hidden py-safe>
|
||||
<WebSocketStatusButton />
|
||||
<!-- header -->
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="hidden md:flex" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@proj-airi/ui/main.css';
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
|
||||
+12
-142
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useLampFlickerAnimation } from '@proj-airi/stage-ui/composables/use-lamp-flicker-animation'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { lampFlickerAnimationClass } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ControlButtonTooltip from '../controls-island/control-button-tooltip.vue'
|
||||
@@ -10,11 +12,11 @@ import ControlButton from '../controls-island/control-button.vue'
|
||||
|
||||
import { electronOpenSettings } from '../../../../shared/eventa'
|
||||
|
||||
const { t, te } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const { connected } = storeToRefs(useModsServerChannelStore())
|
||||
const openSettings = useElectronEventaInvoke(electronOpenSettings)
|
||||
const flickerDuration = ref('6.4s')
|
||||
const flickerDelay = ref('0s')
|
||||
|
||||
const { flickerStyle, onAnimationIteration } = useLampFlickerAnimation(() => !connected.value)
|
||||
|
||||
const statusIslandSize = {
|
||||
border: 'border-2',
|
||||
@@ -35,7 +37,7 @@ const buttonStyle = computed(() => {
|
||||
|
||||
const iconClasses = computed(() => {
|
||||
return [
|
||||
connected.value ? 'i-ph:wifi-high' : 'i-ph:wifi-slash status-island-lamp-flicker',
|
||||
connected.value ? 'i-ph:wifi-high' : `i-ph:wifi-slash ${lampFlickerAnimationClass}`,
|
||||
statusIslandSize.icon,
|
||||
'shrink-0 transition-colors duration-300 ease-in-out',
|
||||
connected.value
|
||||
@@ -44,62 +46,15 @@ const iconClasses = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const iconStyle = computed(() => {
|
||||
if (connected.value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
'--status-island-flicker-delay': flickerDelay.value,
|
||||
'--status-island-flicker-duration': flickerDuration.value,
|
||||
}
|
||||
})
|
||||
|
||||
const buttonLabel = computed(() => {
|
||||
if (connected.value) {
|
||||
return te('tamagotchi.stage.status-island.connected')
|
||||
? t('tamagotchi.stage.status-island.connected')
|
||||
: 'WebSocket connected'
|
||||
}
|
||||
|
||||
return te('tamagotchi.stage.status-island.disconnected')
|
||||
? t('tamagotchi.stage.status-island.disconnected')
|
||||
: 'WebSocket disconnected'
|
||||
return connected.value
|
||||
? t('stage.websocket-status.connected')
|
||||
: t('stage.websocket-status.disconnected')
|
||||
})
|
||||
|
||||
const tooltipLabel = computed(() => {
|
||||
const openSettingsLabel = te('tamagotchi.stage.status-island.open-settings')
|
||||
? t('tamagotchi.stage.status-island.open-settings')
|
||||
: 'Open WebSocket settings'
|
||||
|
||||
return `${buttonLabel.value}. ${openSettingsLabel}`
|
||||
return `${buttonLabel.value}. ${t('stage.websocket-status.open-settings')}`
|
||||
})
|
||||
|
||||
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 handleFlickerIteration() {
|
||||
if (!connected.value) {
|
||||
randomizeFlicker()
|
||||
}
|
||||
}
|
||||
|
||||
watch(connected, (isConnected) => {
|
||||
if (isConnected) {
|
||||
flickerDelay.value = '0s'
|
||||
return
|
||||
}
|
||||
|
||||
randomizeFlicker(true)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -111,7 +66,7 @@ watch(connected, (isConnected) => {
|
||||
:title="tooltipLabel"
|
||||
@click="openSettings({ route: '/settings/connection' })"
|
||||
>
|
||||
<div :class="iconClasses" :style="iconStyle" @animationiteration="handleFlickerIteration" />
|
||||
<div :class="iconClasses" :style="flickerStyle" @animationiteration="onAnimationIteration" />
|
||||
</ControlButton>
|
||||
<template #tooltip>
|
||||
{{ tooltipLabel }}
|
||||
@@ -119,88 +74,3 @@ watch(connected, (isConnected) => {
|
||||
</ControlButtonTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes status-island-lamp-flicker {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.status-island-lamp-flicker {
|
||||
animation-delay: var(--status-island-flicker-delay, 0s);
|
||||
animation-duration: var(--status-island-flicker-duration, 6.4s);
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: status-island-lamp-flicker;
|
||||
animation-timing-function: ease-in-out;
|
||||
filter: drop-shadow(0 0 0.14rem rgb(251 191 36 / 0.18));
|
||||
will-change: opacity;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@proj-airi/ui/main.css';
|
||||
@import './transitions.css';
|
||||
|
||||
html,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@proj-airi/ui/main.css';
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@proj-airi/ui/main.css';
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,3 +84,7 @@ promo-banner:
|
||||
reward: ログイン時の無料テーマ抽選チケット
|
||||
cta: テーマを表示
|
||||
waiting: 待機中
|
||||
websocket-status:
|
||||
connected: WebSocket が接続されました
|
||||
disconnected: WebSocket が切断されました
|
||||
open-settings: WebSocketの設定を開く
|
||||
|
||||
@@ -25,6 +25,10 @@ docs:
|
||||
'close': 閉じる
|
||||
'expand': 展開
|
||||
'collapse': 折りたたむ
|
||||
'login': Sign in
|
||||
'logging-in': Signing in...
|
||||
'account': Account
|
||||
'logout': Sign out
|
||||
'login': サインイン
|
||||
'logging-in': サインイン中…
|
||||
'account': アカウント
|
||||
|
||||
@@ -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 설정 열기
|
||||
|
||||
@@ -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: 호버 시 페이드
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: Исчезать при наведении
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 设置
|
||||
|
||||
@@ -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: 悬停淡出
|
||||
|
||||
@@ -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 設定
|
||||
|
||||
@@ -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: 懸停淡出
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<boolean>) {
|
||||
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<string, string>
|
||||
})
|
||||
|
||||
return {
|
||||
flickerStyle,
|
||||
onAnimationIteration,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@proj-airi/ui/main.css';
|
||||
@import './transitions.css';
|
||||
|
||||
html,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user