feat(stage-tamagotchi): system tray (#32)

* fix(tamagotchi): cannot use default select copy and paste
* style(tamagotchi): scrollbar
* fix(tamagotchi): set motion
* feat(tamagotchi): system tray
* fix: linter issue
* fix: typecheck
* fix: electron declaration
This commit is contained in:
LemonNeko
2025-02-23 22:54:24 +08:00
committed by GitHub
parent a15457a50d
commit 636371e971
16 changed files with 271 additions and 210 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -107,6 +107,7 @@
"electron-builder": "24.13.3",
"electron-vite": "^2.3.0",
"markdown-it-link-attributes": "^4.0.1",
"unocss-preset-scrollbar": "^3.2.0",
"unplugin-auto-import": "^19.1.0",
"unplugin-vue-components": "^28.4.0",
"unplugin-vue-macros": "^2.14.2",
+61 -195
View File
@@ -1,24 +1,54 @@
import { join } from 'node:path'
import { env, platform } from 'node:process'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } from 'electron'
import { inertia } from 'popmotion'
import { nativeImage, shell } from 'electron/common'
import { app, BrowserWindow, dialog, ipcMain, Tray } from 'electron/main'
import trayIconMacos from '../../build/icon-tray-macos.png?asset'
import icon from '../../build/icon.png?asset'
import { createI18n } from './locales'
import { createApplicationMenu, createTrayMenu } from './menu'
// FIXME: electron i18n
let globalMouseTracker: ReturnType<typeof setInterval> | null = null
let mainWindow: BrowserWindow
let currentAnimationX: { stop: () => void } | null = null
let currentAnimationY: { stop: () => void } | null = null
let isDragging = false
let lastMousePosition = { x: 0, y: 0 }
let lastMouseTime = Date.now()
let currentVelocity = { x: 0, y: 0 }
let dragOffset = { x: 0, y: 0 }
let tray: Tray
function createWindow(): void {
const i18n = createI18n()
function showQuitDialog() {
dialog.showMessageBox({
type: 'info',
title: i18n.t('menu.quit'),
message: i18n.t('quitDialog.message'),
buttons: [i18n.t('quitDialog.buttons.quit'), i18n.t('quitDialog.buttons.cancel')],
}).then((result) => {
if (result.response === 0) {
mainWindow.webContents.send('before-quit')
setTimeout(() => {
app.quit()
}, 2000)
}
})
}
function rebuildTrayMenu() {
if (mainWindow.isVisible()) {
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
mainWindow.webContents.send('before-hide')
setTimeout(() => {
mainWindow.hide()
rebuildTrayMenu()
}, 2000)
}, createSettingsWindow, showQuitDialog))
return
}
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
mainWindow.show()
mainWindow.webContents.send('after-show')
rebuildTrayMenu()
}, createSettingsWindow, showQuitDialog))
}
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 300 * 1.5,
@@ -57,115 +87,11 @@ function createWindow(): void {
else {
mainWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'))
}
ipcMain.on('start-window-drag', (_) => {
isDragging = true
const mousePos = screen.getCursorScreenPoint()
const [windowX, windowY] = mainWindow.getPosition()
// Calculate the offset between cursor and window position
dragOffset = {
x: mousePos.x - windowX,
y: mousePos.y - windowY,
}
// Stop any existing animations
if (currentAnimationX) {
currentAnimationX.stop()
currentAnimationX = null
}
if (currentAnimationY) {
currentAnimationY.stop()
currentAnimationY = null
}
// Initialize last position for velocity tracking
lastMousePosition = { x: mousePos.x, y: mousePos.y }
lastMouseTime = Date.now()
currentVelocity = { x: 0, y: 0 }
// Start global mouse tracking
if (!globalMouseTracker) {
globalMouseTracker = setInterval(() => {
const mousePos = screen.getCursorScreenPoint()
if (isDragging) {
handleWindowMove(mousePos.x, mousePos.y)
}
}, 16) // ~60fps
}
})
ipcMain.on('end-window-drag', () => {
isDragging = false
if (globalMouseTracker) {
clearInterval(globalMouseTracker)
globalMouseTracker = null
}
// Apply inertia animation when drag ends
const [currentX, currentY] = mainWindow.getPosition()
let latestX = currentX
let latestY = currentY
const inertiaConfig = {
power: 0.4, // Reduced from 0.6 for stronger resistance
timeConstant: 250, // Reduced from 400 for quicker deceleration
modifyTarget: (v: number) => v,
min: 0,
max: Infinity,
}
// Clamp velocity to reasonable values
const clampVelocity = (v: number) => {
const maxVelocity = 500 // Reduced from 800 for less momentum
const minVelocity = -500
return Math.min(Math.max(v, minVelocity), maxVelocity)
}
// Reduce velocity amplification and clamp values
const amplifiedVelocity = {
x: clampVelocity(currentVelocity.x * 0.2), // Reduced from 0.3 for less momentum
y: clampVelocity(currentVelocity.y * 0.2),
}
// Ignore very small movements
if (Math.abs(amplifiedVelocity.x) > 35 || Math.abs(amplifiedVelocity.y) > 35) {
currentAnimationX = inertia({
from: currentX,
velocity: amplifiedVelocity.x,
...inertiaConfig,
onUpdate: (x) => {
latestX = Math.round(x)
mainWindow.setPosition(latestX, Math.round(latestY))
},
onComplete: () => {
currentAnimationX = null
},
})
currentAnimationY = inertia({
from: currentY,
velocity: amplifiedVelocity.y,
...inertiaConfig,
onUpdate: (y) => {
latestY = Math.round(y)
mainWindow.setPosition(Math.round(latestX), latestY)
},
onComplete: () => {
currentAnimationY = null
},
})
}
})
ipcMain.on('move-window', (_, cursorX: number, cursorY: number) => {
handleWindowMove(cursorX, cursorY)
})
}
let settingsWindow: BrowserWindow | null = null
function createSettingsWindow(): void {
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.show()
return
@@ -210,30 +136,7 @@ function createSettingsWindow(): void {
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Menu
const menu = Menu.buildFromTemplate([
{
label: 'airi',
role: 'appMenu',
submenu: [
{
role: 'about',
},
{
role: 'toggleDevTools',
},
{
label: 'Settings',
click: () => createSettingsWindow(),
},
{
label: 'Quit',
click: () => app.quit(),
},
],
},
])
Menu.setApplicationMenu(menu)
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
// Set app user model id for windows
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
@@ -246,19 +149,7 @@ app.whenReady().then(() => {
})
// IPC test
// TODO: i18n
ipcMain.on('quit', () => {
dialog.showMessageBox({
type: 'info',
title: 'Quit',
message: 'Are you sure you want to quit?',
buttons: ['Quit', 'Cancel'],
}).then((result) => {
if (result.response === 0) {
app.quit()
}
})
})
ipcMain.on('quit', showQuitDialog)
ipcMain.on('open-settings', () => createSettingsWindow())
@@ -271,44 +162,19 @@ app.whenReady().then(() => {
createWindow()
}
})
const trayIcon = platform === 'darwin'
? nativeImage.createFromPath(trayIconMacos).resize({ width: 16, height: 16 })
: nativeImage.createFromPath(icon).resize({ width: 16, height: 16 })
tray = new Tray(trayIcon)
tray.setToolTip('Airi')
rebuildTrayMenu()
app.dock.hide()
ipcMain.on('locale-changed', (_, language: string) => {
i18n.setLocale(language)
rebuildTrayMenu()
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
function handleWindowMove(cursorX: number, cursorY: number) {
if (!isDragging)
return
// Calculate actual velocity based on mouse movement
const currentTime = Date.now()
const deltaTime = currentTime - lastMouseTime
if (deltaTime > 0) {
// Smooth out velocity calculation with some averaging
const newVelocityX = (cursorX - lastMousePosition.x) / deltaTime * 1000
const newVelocityY = (cursorY - lastMousePosition.y) / deltaTime * 1000
currentVelocity = {
x: currentVelocity.x * 0.8 + newVelocityX * 0.2, // Smooth velocity
y: currentVelocity.y * 0.8 + newVelocityY * 0.2,
}
}
// Update window position based on cursor position and offset
const newX = cursorX - dragOffset.x
const newY = cursorY - dragOffset.y
mainWindow.setPosition(Math.round(newX), Math.round(newY))
lastMousePosition = { x: cursorX, y: cursorY }
lastMouseTime = currentTime
}
@@ -4,10 +4,15 @@ export default {
quit: 'Quit',
about: 'About',
toggleDevTools: 'Toggle Developer Tools',
show: 'Show airi',
hide: 'Hide airi',
},
quitDialog: {
title: 'Quit',
message: 'Are you sure you want to quit?',
buttons: ['Quit', 'Cancel'],
buttons: {
quit: 'Quit',
cancel: 'Cancel',
},
},
}
@@ -10,6 +10,7 @@ describe('createI18n', () => {
it('should return key if the key is not found', () => {
const { t } = createI18n()
// @ts-expect-error for test
expect(t('menu.not.found')).toBe('menu.not.found')
})
@@ -21,7 +22,7 @@ describe('createI18n', () => {
it('should return the correct locale in array', () => {
const { t } = createI18n()
expect(t('quitDialog.buttons.0')).toBe('Quit')
expect(t('quitDialog.buttons.1')).toBe('Cancel')
expect(t('quitDialog.buttons.quit')).toBe('Quit')
expect(t('quitDialog.buttons.cancel')).toBe('Cancel')
})
})
@@ -7,11 +7,17 @@ const locales = {
'zh-CN': zhCN,
}
type Message = typeof locales['en-US']
type KeyOfExcludeSymbol<T> = Exclude<keyof T, symbol>
type ValueOf<T> = T[KeyOfExcludeSymbol<T>]
type PathOf<T, Root extends boolean = true> = T extends Array<any> ? number : T extends string ? '' : Root extends true ? `${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}` : `.${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}`
type LocalePath = PathOf<Message>
export function createI18n() {
let locale = 'en-US'
let messages = locales['en-US']
function t(key: string) {
function t(key: LocalePath) {
const path = key.split('.')
let current = messages
let result = ''
@@ -44,3 +50,5 @@ export function createI18n() {
locale,
}
}
export type I18n = ReturnType<typeof createI18n>
@@ -4,10 +4,15 @@ export default {
quit: '退出',
about: '关于',
toggleDevTools: '切换开发者工具',
show: '显示 airi',
hide: '隐藏 airi',
},
quitDialog: {
title: '退出',
message: '确定要退出吗?',
buttons: ['退出', '取消'],
buttons: {
quit: '退出',
cancel: '取消',
},
},
}
+65
View File
@@ -0,0 +1,65 @@
import type { I18n } from './locales'
import { Menu } from 'electron/main'
export function createBasicMenu(
{ t }: I18n,
createSettingsWindow: () => void,
onQuitClick: () => void,
): Array<(Electron.MenuItemConstructorOptions)> {
return [
{
role: 'about',
label: t('menu.about'),
},
{
label: t('menu.settings'),
click: createSettingsWindow,
},
{
label: t('menu.quit'),
click: onQuitClick,
},
]
}
export function createTrayMenu(
i18n: I18n,
isVisible: boolean,
onVisibleChange: () => void,
createSettingsWindow: () => void,
onQuitClick: () => void,
) {
const menu = createBasicMenu(i18n, createSettingsWindow, onQuitClick)
if (isVisible) {
menu.push({
label: i18n.t('menu.hide'),
click: onVisibleChange,
})
}
else {
menu.push({
label: i18n.t('menu.show'),
click: onVisibleChange,
})
}
return Menu.buildFromTemplate(menu)
}
export function createApplicationMenu(
i18n: I18n,
onQuitClick: () => void,
createSettingsWindow: () => void,
) {
const menu = Menu.buildFromTemplate([
{
label: 'airi',
role: 'appMenu',
submenu: createBasicMenu(i18n, createSettingsWindow, onQuitClick),
},
{
role: 'editMenu',
},
])
Menu.setApplicationMenu(menu)
}
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import { watch } from 'vue'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
@@ -11,6 +11,11 @@ watch(() => settings.language, (language) => {
i18n.locale.value = language
window.electron.ipcRenderer.send('locale-changed', language)
})
// FIXME: store settings to file
onMounted(() => {
window.electron.ipcRenderer.send('locale-changed', settings.language)
})
</script>
<template>
@@ -34,7 +34,7 @@ onTokenLiteral(async () => {
<template>
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
<div flex-1 /> <!-- spacer -->
<div ref="chatHistoryRef" v-auto-animate h-full w-full max-h="30vh" flex="~ col" overflow-scroll>
<div ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col" scrollbar="~ w-2 track-color-transparent thumb-color-pink-300 rounded" h-full w-full overflow-scroll>
<div flex-1 /> <!-- spacer -->
<div v-for="(message, index) in messages" :key="index" mb-2>
<div v-if="message.role === 'assistant'" flex mr="12">
@@ -89,7 +89,7 @@ onMounted(() => {
<template>
<div>
<div relative w-full flex gap-1>
<TamagotchiChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" w-full />
<TamagotchiChatHistory transform="translate-y-[-100%]" absolute left-0 top-0 w-full />
<div flex flex-1>
<BasicTextarea
v-model="messageInput"
@@ -60,7 +60,7 @@ const modeIndicatorClass = computed(() => {
>
<div
v-if="windowStore.controlMode === WindowControlMode.MOVE"
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center"
class="drag-region absolute left-0 top-0 z-999 h-full w-full flex items-center justify-center overflow-hidden"
>
<div class="absolute h-32 w-full flex items-center justify-center b-2 b-pink bg-white">
<div class="wall absolute top-0 h-8" />
+2
View File
@@ -9,6 +9,7 @@ import {
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
import { presetScrollbar } from 'unocss-preset-scrollbar'
export default defineConfig({
presets: [
@@ -30,6 +31,7 @@ export default defineConfig({
...createExternalPackageIconLoader('@proj-airi/lobe-icons'),
},
}),
presetScrollbar(),
],
transformers: [
transformerDirectives(),
@@ -4,7 +4,7 @@ import type { Emotion } from '../../constants/emotions'
import { generateSpeech } from '@xsai/generate-speech'
import { createUnElevenLabs } from '@xsai/providers'
import { storeToRefs } from 'pinia'
import { onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMarkdown } from '../../composables/markdown'
@@ -12,7 +12,7 @@ import { useQueue } from '../../composables/queue'
import { useDelayMessageQueue, useEmotionsMessageQueue, useMessageContentQueue } from '../../composables/queues'
import { llmInferenceEndToken } from '../../constants'
import { Voice, voiceMap } from '../../constants/elevenlabs'
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '../../constants/emotions'
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionAngryMotionName, EmotionHappyMotionName, EmotionThinkMotionName } from '../../constants/emotions'
import { useAudioContext, useSpeakingStore } from '../../stores/audio'
import { useChatStore } from '../../stores/chat'
import { useSettings } from '../../stores/settings'
@@ -21,9 +21,10 @@ import VRMScene from '../Scenes/VRM.vue'
import '../../utils/live2d-zip-loader'
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
const motion = ref('')
const { stageView, elevenLabsApiKey, elevenlabsVoiceEnglish, elevenlabsVoiceJapanese } = storeToRefs(useSettings())
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
const { audioContext, calculateVolume } = useAudioContext()
@@ -115,7 +116,7 @@ const emotionsQueue = useQueue<Emotion>({
await vrmViewerRef.value!.setExpression(value)
}
else if (stageView.value === '2d') {
await live2DViewerRef.value!.setMotion(EMOTION_EmotionMotionName_value[ctx.data])
motion.value = EMOTION_EmotionMotionName_value[ctx.data]
}
},
],
@@ -160,7 +161,7 @@ onBeforeMessageComposed(async () => {
})
onBeforeSend(async () => {
live2DViewerRef.value?.setMotion(EmotionThinkMotionName)
motion.value = EmotionThinkMotionName
})
onTokenLiteral(async (literal) => {
@@ -178,6 +179,21 @@ onStreamEnd(async () => {
onUnmounted(() => {
lipSyncStarted.value = false
window.electron?.ipcRenderer.removeAllListeners('before-hide')
window.electron?.ipcRenderer.removeAllListeners('after-show')
window.electron?.ipcRenderer.removeAllListeners('before-quit')
})
onMounted(() => {
window.electron?.ipcRenderer.on('before-hide', () => {
motion.value = EmotionAngryMotionName
})
window.electron?.ipcRenderer.on('after-show', () => {
motion.value = EmotionHappyMotionName
})
window.electron?.ipcRenderer.on('before-quit', () => {
motion.value = EmotionThinkMotionName
})
})
</script>
@@ -186,7 +202,7 @@ onUnmounted(() => {
<div h-full w-full>
<Live2DScene
v-if="stageView === '2d'"
ref="live2DViewerRef"
v-model:motion="motion"
:mouth-open-size="mouthOpenSize"
model="./assets/live2d/models/hiyori_pro_zh.zip"
min-w="50% <lg:full" min-h="100 sm:100" h-full w-full flex-1
+8
View File
@@ -0,0 +1,8 @@
import type { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron?: ElectronAPI
api?: unknown
}
}
+79
View File
@@ -409,6 +409,9 @@ importers:
markdown-it-link-attributes:
specifier: ^4.0.1
version: 4.0.1
unocss-preset-scrollbar:
specifier: ^3.2.0
version: 3.2.0(unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))
unplugin-auto-import:
specifier: ^19.1.0
version: 19.1.0(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.7.3))
@@ -4458,6 +4461,9 @@ packages:
resolution: {integrity: sha512-nFRGop/guBa4jLkrgXjaRDm5JPz4x3YpP10m5IQkHpHwlnHUVn1L9smyPl04ohYWhYn9ZcAHgR28Ih2jwta8hw==}
engines: {node: '>=14'}
'@unocss/core@65.5.0':
resolution: {integrity: sha512-XYWdS09M2XOjZNDotGhI2TIW/6duLNiyssopwjCbv4AlPklF0bZI86SKI55syYDBt6GRadoQbuvUkhSiTV/hzQ==}
'@unocss/core@66.0.0':
resolution: {integrity: sha512-PdVbSMHNDDkr++9nkqzsZRAkaU84gxMTEgYbqI7dt2p1DXp/5tomVtmMsr2/whXGYKRiUc0xZ3p4Pzraz8TcXA==}
@@ -4469,6 +4475,9 @@ packages:
resolution: {integrity: sha512-KTP6uK0loH9+PkUjL2F4eyuMcUZRiVYkg4zJfqVWNctE1yGkuTUzCvm6ORRvLakajAU8G/Zzvuo1pE94zyZQbw==}
engines: {node: '>=14'}
'@unocss/extractor-arbitrary-variants@65.5.0':
resolution: {integrity: sha512-7K3gftOdkv9jbWvbkExTcx6/FDP2Xk/NSsOYTvR9oITLnLjmdQvp+9276WSnNfKF3frBl8ZcqpkC2EsuL2Yutw==}
'@unocss/extractor-arbitrary-variants@66.0.0':
resolution: {integrity: sha512-vlkOIOuwBfaFBJcN6o7+obXjigjOlzVFN/jT6pG1WXbQDTRZ021jeF3i9INdb9D/0cQHSeDvNgi1TJ5oUxfiow==}
@@ -4487,6 +4496,9 @@ packages:
'@unocss/preset-icons@66.0.0':
resolution: {integrity: sha512-6ObwTvEGuPBbKWRoMMiDioHtwwQTFI5oojFLJ32Y8tW6TdXvBLkO88d7qpgQxEjgVt4nJrqF1WEfR4niRgBm0Q==}
'@unocss/preset-mini@65.5.0':
resolution: {integrity: sha512-oD2INmEgTOxmFsVceflv4Zs67vz9PRbpg3+CMsJLWgfX4UdQ1H4jZms72+g3N1hhXBvOFwvGvqGaMnrVMRk54g==}
'@unocss/preset-mini@66.0.0':
resolution: {integrity: sha512-d62eACnuKtR0dwCFOQXgvw5VLh5YSyK56xCzpHkh0j0GstgfDLfKTys0T/XVAAvdSvAy/8A8vhSNJ4PlIc9V2A==}
@@ -4511,6 +4523,10 @@ packages:
'@unocss/reset@66.0.0':
resolution: {integrity: sha512-YLFz/5yT7mFJC8JSmIUA5+bS3CBCJbtztOw+8rWzjQr/BEVSGuihWUUpI2Df6VVxXIXxKanZR6mIl59yvf+GEA==}
'@unocss/rule-utils@65.5.0':
resolution: {integrity: sha512-xT4N0EY1dl1mqY5gTKD0H/Fg6xApe7xbfNTUwctOu02DMeJhqv9BTqfoAihH/hzGSI69+FtzVtz7hUxTypfehA==}
engines: {node: '>=14'}
'@unocss/rule-utils@66.0.0':
resolution: {integrity: sha512-UJ51YHbwxYTGyj35ugsPlOT4gaa7tCbXdywZ3m5Nn0JgywwIqGmBFyiN9ZjHBHfJuDxmmPd6lxojoBscih/WMQ==}
engines: {node: '>=14'}
@@ -10492,6 +10508,11 @@ packages:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
unocss-preset-scrollbar@3.2.0:
resolution: {integrity: sha512-j8BOoh2RgPm2U8XqEjMQ+XQk4YWYPH4T+yzv3fndxS+VpdizQinMvHmfsZGLN3yMv7I4O5Qi8fVTlQDhETyzbA==}
peerDependencies:
unocss: '>= 0.31.13'
unocss@66.0.0:
resolution: {integrity: sha512-SHstiv1s7zGPSjzOsADzlwRhQM+6817+OqQE3Fv+N/nn2QLNx1bi3WXybFfz5tWkzBtyTZlwdPmeecsIs1yOCA==}
engines: {node: '>=14'}
@@ -14688,6 +14709,8 @@ snapshots:
'@unocss/core': 66.0.0
unconfig: 7.0.0
'@unocss/core@65.5.0': {}
'@unocss/core@66.0.0': {}
'@unocss/eslint-config@66.0.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)':
@@ -14711,6 +14734,10 @@ snapshots:
- supports-color
- typescript
'@unocss/extractor-arbitrary-variants@65.5.0':
dependencies:
'@unocss/core': 65.5.0
'@unocss/extractor-arbitrary-variants@66.0.0':
dependencies:
'@unocss/core': 66.0.0
@@ -14735,6 +14762,15 @@ snapshots:
postcss: 8.4.49
tinyglobby: 0.2.11
'@unocss/postcss@66.0.0(postcss@8.5.3)':
dependencies:
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/rule-utils': 66.0.0
css-tree: 3.1.0
postcss: 8.5.3
tinyglobby: 0.2.11
'@unocss/preset-attributify@66.0.0':
dependencies:
'@unocss/core': 66.0.0
@@ -14747,6 +14783,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@unocss/preset-mini@65.5.0':
dependencies:
'@unocss/core': 65.5.0
'@unocss/extractor-arbitrary-variants': 65.5.0
'@unocss/rule-utils': 65.5.0
'@unocss/preset-mini@66.0.0':
dependencies:
'@unocss/core': 66.0.0
@@ -14786,6 +14828,11 @@ snapshots:
'@unocss/reset@66.0.0': {}
'@unocss/rule-utils@65.5.0':
dependencies:
'@unocss/core': 65.5.0
magic-string: 0.30.17
'@unocss/rule-utils@66.0.0':
dependencies:
'@unocss/core': 66.0.0
@@ -22675,6 +22722,11 @@ snapshots:
universalify@2.0.0: {}
unocss-preset-scrollbar@3.2.0(unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))):
dependencies:
'@unocss/preset-mini': 65.5.0
unocss: 66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
unocss@66.0.0(postcss@8.4.49)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
@@ -22702,6 +22754,33 @@ snapshots:
- supports-color
- vue
unocss@66.0.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
'@unocss/cli': 66.0.0
'@unocss/core': 66.0.0
'@unocss/postcss': 66.0.0(postcss@8.5.3)
'@unocss/preset-attributify': 66.0.0
'@unocss/preset-icons': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/preset-tagify': 66.0.0
'@unocss/preset-typography': 66.0.0
'@unocss/preset-uno': 66.0.0
'@unocss/preset-web-fonts': 66.0.0
'@unocss/preset-wind': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/transformer-attributify-jsx': 66.0.0
'@unocss/transformer-compile-class': 66.0.0
'@unocss/transformer-directives': 66.0.0
'@unocss/transformer-variant-group': 66.0.0
'@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vite: 6.1.1(@types/node@22.13.4)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
transitivePeerDependencies:
- postcss
- supports-color
- vue
unpipe@1.0.0: {}
unplugin-auto-import@19.1.0(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.7.3)):