refactor(stage-*): improve transcription UX and DX (#1685)

This commit is contained in:
DrHuangMHT
2026-05-09 19:09:39 +08:00
committed by GitHub
parent 06ab58408b
commit 24da875aa6
11 changed files with 921 additions and 409 deletions
@@ -11,7 +11,7 @@ import { onMounted, onUnmounted, watch } from 'vue'
const show = defineModel('show', { type: Boolean, default: false })
const settingsAudioDeviceStore = useSettingsAudioDevice()
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDeviceStore)
const { enabled, stream } = storeToRefs(settingsAudioDeviceStore)
const getMediaAccessStatus = useElectronEventaInvoke(electron.systemPreferences.getMediaAccessStatus)
const { state: mediaAccessStatus, execute: refreshMediaAccessStatus } = useAsyncState(() => getMediaAccessStatus(['microphone']), 'not-determined')
@@ -58,10 +58,7 @@ onUnmounted(async () => {
<template>
<HearingConfigDialog
v-model:show="show"
v-model:enabled="enabled"
v-model:selected-audio-input="selectedAudioInput"
:granted="mediaAccessStatus !== 'denied' && mediaAccessStatus !== 'restricted'"
:audio-inputs="audioInputs"
:volume-level="volumeLevel"
>
<slot />
+3
View File
@@ -62,8 +62,11 @@
"@proj-airi/stage-shared": "workspace:^",
"@types/audioworklet": "catalog:",
"@types/three": "^0.184.0",
"@vue/test-utils": "catalog:",
"@webgpu/types": "catalog:",
"jsdom": "catalog:",
"unplugin-info": "catalog:",
"vitest": "catalog:",
"vue-tsc": "^3.2.6"
}
}
@@ -2,6 +2,7 @@
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
import type { ChatProvider } from '@xsai-ext/providers/utils'
import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { useThreeViewControl } from '@proj-airi/stage-ui-three'
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
import { ChatSessionsDrawer } from '@proj-airi/stage-ui/components/scenarios/chat'
@@ -26,6 +27,7 @@ import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
import ActionAbout from './InteractiveArea/Actions/About.vue'
import { useTranscriptions } from '../../composables/use-transcriptions'
import { BackgroundDialogPicker } from '../Backgrounds'
const { isDark, toggleDark } = useTheme()
@@ -68,6 +70,14 @@ function isMobileDevice() {
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}
const { isListening } = useTranscriptions(
{
messageInputRef: messageInput,
sendMessage: handleSend,
isStageTamagotchi,
},
)
async function handleSubmit() {
if (!isMobileDevice()) {
await handleSend()
@@ -123,7 +133,7 @@ async function setupAnalyzer() {
analyzerSource.connect(analyser)
}
watch([hearingDialogOpen, enabled, stream], () => {
watch([enabled, stream], () => {
setupAnalyzer()
}, { immediate: true })
@@ -200,7 +210,7 @@ onMounted(() => {
title="Hearing"
>
<Transition name="fade" mode="out-in">
<IndicatorMicVolume v-if="enabled" size-5 color-class="text-neutral-500 dark:text-neutral-400" />
<IndicatorMicVolume v-if="enabled" size-5 :color-class="isListening ? undefined : 'text-neutral-500 dark:text-neutral-400'" />
<div v-else i-solar:microphone-3-outline size-5 text="neutral-500 dark:neutral-400" />
</Transition>
</button>
@@ -4,28 +4,29 @@ import type { ChatProvider } from '@xsai-ext/providers/utils'
import { errorMessageFrom } from '@moeru/std'
import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { ChatSessionsDrawer } from '@proj-airi/stage-ui/components/scenarios/chat'
import { HearingConfig } from '@proj-airi/stage-ui/components/scenarios/dialogs/audio-input/index'
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { BasicTextarea, FieldCombobox } from '@proj-airi/ui'
import { until, useLocalStorage } from '@vueuse/core'
import { BasicTextarea } from '@proj-airi/ui'
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger, PopoverContent, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IndicatorMicVolume from './IndicatorMicVolume.vue'
const messageInput = ref('')
import { useTranscriptions } from '../../composables/use-transcriptions'
const messageInput = ref<string>('')
const hearingPopoverOpen = ref(false)
const sessionsDrawerOpen = ref(false)
const isComposing = ref(false)
const isListening = ref(false) // Transcription listening state (separate from microphone enabled)
const DOUBLE_ENTER_INTERVAL_MS = 300
const TRAILING_NEWLINES_REGEX = /[\r\n]+$/
const SEND_MODES = ['enter', 'ctrl-enter', 'double-enter'] as const
@@ -37,8 +38,8 @@ const providersStore = useProvidersStore()
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
const { themeColorsHueDynamic } = storeToRefs(useSettings())
const { askPermission, startStream } = useSettingsAudioDevice()
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(useSettingsAudioDevice())
const { askPermission } = useSettingsAudioDevice()
const { enabled, stream } = storeToRefs(useSettingsAudioDevice())
const chatOrchestrator = useChatOrchestratorStore()
const chatSession = useChatSessionStore()
const { ingest, onAfterMessageComposed } = chatOrchestrator
@@ -51,73 +52,13 @@ const sendModeLabels = computed<Record<SendMode, string>>(() => ({
'double-enter': t('stage.send-mode.double-enter'),
}))
// Transcription pipeline
const hearingStore = useHearingStore()
const hearingPipeline = useHearingSpeechInputPipeline()
const { transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline
const { supportsStreamInput } = storeToRefs(hearingPipeline)
const { configured: hearingConfigured, autoSendEnabled, autoSendDelay } = storeToRefs(hearingStore)
const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!stream.value)
// Auto-send logic
let autoSendTimeout: ReturnType<typeof setTimeout> | undefined
const pendingAutoSendText = ref('')
function clearPendingAutoSend() {
if (autoSendTimeout) {
clearTimeout(autoSendTimeout)
autoSendTimeout = undefined
}
pendingAutoSendText.value = ''
}
async function debouncedAutoSend(text: string) {
// Double-check auto-send is enabled before proceeding
if (!autoSendEnabled.value) {
clearPendingAutoSend()
return
}
// Add text to pending buffer
pendingAutoSendText.value = pendingAutoSendText.value ? `${pendingAutoSendText.value} ${text}` : text
// Clear existing timeout
if (autoSendTimeout) {
clearTimeout(autoSendTimeout)
}
// Set new timeout
autoSendTimeout = setTimeout(async () => {
// Final check before sending - auto-send might have been disabled while waiting
if (!autoSendEnabled.value) {
clearPendingAutoSend()
return
}
const textToSend = pendingAutoSendText.value.trim()
if (textToSend && autoSendEnabled.value) {
try {
// `ingest()` resolves only after the full assistant turn finishes; clear UI/buffer now so
// the next SentenceEnd during streaming does not append to the message we already committed.
messageInput.value = ''
pendingAutoSendText.value = ''
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
await ingest(textToSend, {
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
model: activeModel.value,
providerConfig,
})
}
catch (err) {
console.error('[ChatArea] Auto-send error:', err)
// Preserve any transcription that arrived while ingest was in flight (see PR review).
messageInput.value = [textToSend, messageInput.value.trim()].filter(Boolean).join(' ')
pendingAutoSendText.value = [textToSend, pendingAutoSendText.value.trim()].filter(Boolean).join(' ')
}
}
autoSendTimeout = undefined
}, autoSendDelay.value)
}
const { isListening, startStreamingTranscription, stopStreamingTranscription, autoSendEnabled } = useTranscriptions(
{
messageInputRef: messageInput,
sendMessage: handleSend,
isStageTamagotchi,
},
)
async function handleSend() {
if (!messageInput.value.trim() || isComposing.value) {
@@ -137,7 +78,8 @@ async function handleSend() {
})
}
catch (error) {
messageInput.value = textToSend
// preserve any user input when failed to send the message
messageInput.value = [textToSend, messageInput.value.trim()].filter(Boolean).join(' ')
chatSession.setSessionMessages(chatSession.activeSessionId, [
...messages.value.slice(0, -1),
{
@@ -223,231 +165,12 @@ async function setupAnalyzer() {
analyzerSource.connect(analyser)
}
watch([hearingPopoverOpen, enabled, stream], () => {
watch([enabled], () => {
setupAnalyzer()
}, { immediate: true })
onUnmounted(() => {
teardownAnalyzer()
stopListening()
// Clear auto-send timeout on unmount
if (autoSendTimeout) {
clearTimeout(autoSendTimeout)
autoSendTimeout = undefined
}
})
// Transcription listening functions
async function startListening() {
// Allow calling this even if already listening - transcribeForMediaStream will handle session reuse/restart
try {
console.info('[ChatArea] Starting listening...', {
enabled: enabled.value,
hasStream: !!stream.value,
supportsStreamInput: supportsStreamInput.value,
hearingConfigured: hearingConfigured.value,
})
// Auto-configure Web Speech API as default if no provider is configured
if (!hearingConfigured.value) {
// Check if Web Speech API is available in the browser
// Web Speech API is NOT available in Electron (stage-tamagotchi) - it requires Google's embedded API keys
// which are not available in Electron, causing it to fail at runtime
const isWebSpeechAvailable = typeof window !== 'undefined'
&& !isStageTamagotchi() // Explicitly exclude Electron
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
if (isWebSpeechAvailable) {
console.info('[ChatArea] No transcription provider configured. Auto-configuring Web Speech API as default...')
// Initialize the provider in the providers store first
try {
providersStore.initializeProvider('browser-web-speech-api')
}
catch (err) {
console.warn('[ChatArea] Error initializing Web Speech API provider:', err)
}
// Set as active provider
hearingStore.activeTranscriptionProvider = 'browser-web-speech-api'
// Wait for reactivity to update
await nextTick()
// Verify the provider was set correctly
if (hearingStore.activeTranscriptionProvider === 'browser-web-speech-api') {
console.info('[ChatArea] Web Speech API configured as default provider')
// Continue with transcription - Web Speech API is ready
}
else {
console.error('[ChatArea] Failed to set Web Speech API as default provider')
isListening.value = false
return
}
}
else {
console.error('[ChatArea] Web Speech API not available. No transcription provider configured and Web Speech API is not available in this browser. Please go to Settings > Modules > Hearing to configure a transcription provider. Browser support:', {
hasWindow: typeof window !== 'undefined',
hasWebkitSpeechRecognition: typeof window !== 'undefined' && 'webkitSpeechRecognition' in window,
hasSpeechRecognition: typeof window !== 'undefined' && 'SpeechRecognition' in window,
})
isListening.value = false
return
}
}
// Request microphone permission if needed (microphone should already be enabled by the user)
if (!stream.value) {
console.info('[ChatArea] Requesting microphone permission...')
await askPermission()
// If still no stream, try starting it manually
if (!stream.value && enabled.value) {
console.info('[ChatArea] Attempting to start stream manually...')
startStream()
// Wait for the stream to become available with a timeout.
try {
await until(stream).toBeTruthy({ timeout: 3000, throwOnTimeout: true })
}
catch {
console.error('[ChatArea] Timed out waiting for audio stream.')
isListening.value = false
return
}
}
}
if (!stream.value) {
const errorMsg = 'Failed to get audio stream for transcription. Please check microphone permissions and ensure a device is selected.'
console.error('[ChatArea]', errorMsg)
isListening.value = false
return
}
// Check if streaming input is supported
if (!shouldUseStreamInput.value) {
const errorMsg = 'Streaming input not supported by the selected transcription provider. Please select a provider that supports streaming (e.g., Web Speech API).'
console.warn('[ChatArea]', errorMsg)
// Clean up any existing sessions from other pages (e.g., test page) that might interfere
await stopStreamingTranscription(true)
isListening.value = false
return
}
console.info('[ChatArea] Starting streaming transcription with stream:', stream.value.id)
// Call transcribeForMediaStream - it's async so we await it
// Set listening state AFTER successful call
try {
await transcribeForMediaStream(stream.value, {
onSentenceEnd: (delta) => {
if (delta && delta.trim()) {
// Append transcribed text to message input
const currentText = messageInput.value.trim()
messageInput.value = currentText ? `${currentText} ${delta}` : delta
console.info('[ChatArea] Received transcription delta:', delta)
// Auto-send if enabled - check the current value (not captured in closure)
// This ensures we always respect the current setting, even if callbacks are reused
if (autoSendEnabled.value) {
debouncedAutoSend(delta)
}
else {
// If auto-send is disabled, clear any pending auto-send text to prevent accidental sends
clearPendingAutoSend()
}
}
},
// Omit onSpeechEnd to avoid re-adding user-deleted text; use sentence deltas only.
})
// Only set listening to true if transcription started successfully
// (transcribeForMediaStream might return early if session already exists)
isListening.value = true
console.info('[ChatArea] Streaming transcription initiated successfully')
}
catch (err) {
console.error('[ChatArea] Transcription error:', err)
isListening.value = false
throw err // Re-throw to be caught by outer catch
}
}
catch (err) {
console.error('[ChatArea] Failed to start transcription:', err)
isListening.value = false
}
}
async function stopListening() {
if (!isListening.value)
return
try {
console.info('[ChatArea] Stopping transcription...')
// Clear auto-send timeout
clearPendingAutoSend()
// Send any pending text immediately if auto-send is enabled
if (autoSendEnabled.value && pendingAutoSendText.value.trim()) {
const textToSend = pendingAutoSendText.value.trim()
pendingAutoSendText.value = ''
try {
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
await ingest(textToSend, {
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
model: activeModel.value,
providerConfig,
})
messageInput.value = ''
}
catch (err) {
console.error('[ChatArea] Auto-send error on stop:', err)
}
}
await stopStreamingTranscription(true)
isListening.value = false
console.info('[ChatArea] Transcription stopped')
}
catch (err) {
console.error('[ChatArea] Error stopping transcription:', err)
isListening.value = false
}
}
// Start listening when microphone is enabled and stream is available
watch(enabled, async (val) => {
if (val && stream.value) {
// Microphone was just enabled and we have a stream, start transcription
await startListening()
}
else if (!val && isListening.value) {
// Microphone was disabled, stop transcription
await stopListening()
}
})
// Start listening when stream becomes available (if microphone is enabled)
watch(stream, async (val) => {
if (val && enabled.value && !isListening.value) {
// Stream became available and microphone is enabled, start transcription
await startListening()
}
else if (!val && isListening.value) {
// Stream was lost, stop transcription
await stopListening()
}
})
// Watch for auto-send setting changes and clear pending sends if disabled
watch(autoSendEnabled, (enabled) => {
if (!enabled) {
// Auto-send was disabled - clear any pending auto-send
clearPendingAutoSend()
console.info('[ChatArea] Auto-send disabled, cleared pending text')
}
})
watch(sendMode, () => {
@@ -553,7 +276,7 @@ watch(sendMode, () => {
:title="t('settings.hearing.title')"
>
<Transition name="fade" mode="out-in">
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" />
<IndicatorMicVolume v-if="enabled" class="h-5 w-5" :color-class="isListening ? undefined : 'text-neutral-500 dark:text-neutral-400'" />
<div v-else class="i-ph:microphone-slash h-5 w-5" />
</Transition>
</button>
@@ -567,45 +290,12 @@ watch(sendMode, () => {
'flex flex-col gap-3',
]"
>
<div class="flex flex-col items-center justify-center">
<div class="relative h-28 w-28 select-none">
<div
class="absolute left-1/2 top-1/2 h-20 w-20 rounded-full transition-all duration-150 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1 + normalizedVolume * 0.35})`, opacity: String(0.25 + normalizedVolume * 0.25) }"
:class="enabled ? 'bg-primary-500/15 dark:bg-primary-600/20' : 'bg-neutral-300/20 dark:bg-neutral-700/20'"
/>
<div
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + normalizedVolume * 0.55})`, opacity: String(0.15 + normalizedVolume * 0.2) }"
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
/>
<div
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + normalizedVolume * 0.8})`, opacity: String(0.08 + normalizedVolume * 0.15) }"
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
/>
<button
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:class="enabled
? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95'
: 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200'"
@click="enabled = !enabled"
>
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
</button>
</div>
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
{{ enabled ? 'Microphone enabled' : 'Microphone disabled' }}
</p>
</div>
<FieldCombobox
v-model="selectedAudioInput"
label="Input device"
description="Select the microphone you want to use."
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
layout="vertical"
placeholder="Select microphone"
<HearingConfig
v-model:auto-send="autoSendEnabled"
:transcription="isListening"
:granted="true"
:volume-level="normalizedVolume"
@toggle-transcription="() => isListening ? stopStreamingTranscription() : startStreamingTranscription()"
/>
</PopoverContent>
</PopoverRoot>
@@ -0,0 +1,377 @@
import type { Ref } from 'vue'
import { mount } from '@vue/test-utils'
import { until } from '@vueuse/core'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useTranscriptions } from './use-transcriptions'
function createMockStore() {
return {
activeTranscriptionProvider: undefined,
configured: ref(false),
autoSendEnabled: ref(true),
autoSendDelay: ref(2000),
initializeProvider: vi.fn(),
}
}
const mockTranscribedContent = 'test content'
function createMockPipeline() {
return {
transcribeForMediaStream: vi.fn().mockImplementation((_stream, options: { onSentenceEnd: (delta: string) => void }) => {
options.onSentenceEnd(mockTranscribedContent)
}),
stopStreamingTranscription: vi.fn().mockResolvedValue(undefined),
supportsStreamInput: ref(true),
}
}
function createMockAudioDevice() {
const instance = {
enabled: ref(false),
stream: ref(null),
askPermission: vi.fn().mockResolvedValue(undefined),
startStream: vi.fn(),
}
return instance
}
let mockHearingStore: ReturnType<typeof createMockStore>
let mockHearingPipeline: ReturnType<typeof createMockPipeline>
let mockAudioDevice: ReturnType<typeof createMockAudioDevice>
let mockProvidersStore: ReturnType<typeof createMockStore>
// Mock the modules
vi.mock('@proj-airi/stage-ui/stores/modules/hearing', () => ({
useHearingStore: vi.fn().mockImplementation(() => mockHearingStore),
useHearingSpeechInputPipeline: vi.fn().mockImplementation(() => mockHearingPipeline),
}))
vi.mock('@proj-airi/stage-ui/stores/providers', () => ({
useProvidersStore: vi.fn().mockImplementation(() => mockProvidersStore),
}))
vi.mock('@proj-airi/stage-ui/stores/settings', () => ({
useSettingsAudioDevice: vi.fn().mockImplementation(() => mockAudioDevice),
}))
vi.mock('pinia', () => ({
storeToRefs: vi.fn().mockImplementation((val: any) => val),
}))
vi.mock('@vueuse/core', () => ({
until: vi.fn(),
}))
// Global setup for jsdom environment
beforeAll(() => {
// Ensure window is available
if (typeof window === 'undefined') {
;(globalThis as any).window = {
webkitSpeechRecognition: undefined,
SpeechRecognition: undefined,
}
}
})
afterAll(() => {
vi.clearAllMocks()
})
describe('useTranscriptions', () => {
// Setup mutable instances before each test
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers() // Use fake timers for auto-send tests
mockHearingStore = createMockStore()
mockHearingPipeline = createMockPipeline()
mockAudioDevice = createMockAudioDevice()
mockProvidersStore = createMockStore()
// Mock 'until' to resolve immediately for stream checks
;(until as any).mockImplementation((_source: Ref) => ({
toBeTruthy: vi.fn().mockResolvedValue(undefined),
}))
// Mock SpeechRecognition for browser tests
if (typeof window !== 'undefined') {
(window as any).SpeechRecognition = function () {
this.start = vi.fn()
this.stop = vi.fn()
this.onresult = null
}
}
})
afterEach(() => {
vi.clearAllMocks()
vi.restoreAllMocks()
})
const createOptions = (isTamagotchi = false) => ({
messageInputRef: ref(''),
sendMessage: vi.fn(),
isStageTamagotchi: ref(isTamagotchi),
})
describe('initialization', () => {
it('should initialize with isListening false', () => {
const { isListening } = useTranscriptions(createOptions())
expect(isListening.value).toBe(false)
})
it('should expose startListening and stopListening', () => {
const { startStreamingTranscription, stopStreamingTranscription } = useTranscriptions(createOptions())
expect(startStreamingTranscription).toBeInstanceOf(Function)
expect(stopStreamingTranscription).toBeInstanceOf(Function)
})
})
describe('auto-Configuration (Web Speech API)', () => {
it('should auto-configure Web Speech API if no provider is set', async () => {
mockHearingStore.configured.value = false
mockAudioDevice.enabled.value = true
const { startStreamingTranscription }
= useTranscriptions(createOptions())
await startStreamingTranscription()
expect(mockProvidersStore.initializeProvider).toHaveBeenCalledWith('browser-web-speech-api')
expect(mockHearingStore.activeTranscriptionProvider).toBe('browser-web-speech-api')
})
it('should fail gracefully if Web Speech API is not available', async () => {
// Setup: Tamagotchi mode or no API
if (typeof window !== 'undefined') {
delete (window as any).SpeechRecognition
delete (window as any).webkitSpeechRecognition
}
mockHearingStore.configured.value = false
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
const { isListening, startStreamingTranscription }
= useTranscriptions(createOptions())
await startStreamingTranscription()
expect(isListening.value).toBe(false)
expect(mockHearingPipeline.transcribeForMediaStream).not.toHaveBeenCalled()
})
it('should handle tamagotchi', async () => {
mockHearingStore.configured.value = false
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
const { isListening, stopStreamingTranscription }
= useTranscriptions(createOptions(true))
await stopStreamingTranscription()
expect(isListening.value).toBe(false)
})
})
describe('streaming Logic', () => {
it('should start streaming if stream exists and provider supports it', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
await startStreamingTranscription()
await nextTick()
expect(isListening.value).toBe(true)
expect(mockHearingPipeline.transcribeForMediaStream).toHaveBeenCalledWith(
expect.objectContaining({ id: 'stream-1' }),
expect.any(Object),
)
})
it('should request permission if stream is missing', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = null
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { startStreamingTranscription } = useTranscriptions(createOptions())
await startStreamingTranscription()
await nextTick()
expect(mockAudioDevice.askPermission).toHaveBeenCalled()
expect(mockAudioDevice.startStream).toHaveBeenCalled()
})
it('should stop streaming if stream is missing after permission', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = null
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true;
// simulate failure (stream never appears)
(until as any).mockImplementation(() => ({
toBeTruthy: vi.fn().mockRejectedValue(new Error('Timeout')),
}))
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
await startStreamingTranscription()
await nextTick()
expect(isListening.value).toBe(false)
})
})
describe('transcription & Input', () => {
it('should append transcribed text to messageInputRef', async () => {
const mockInput = ref('')
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { startStreamingTranscription }
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput })
await startStreamingTranscription()
await nextTick()
expect(mockInput.value).toBe(mockTranscribedContent)
})
it('should append transcribed text with a space when input contains value', async () => {
const prependText = 'prepend text'
const mockInput = ref(prependText)
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { startStreamingTranscription }
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput })
await startStreamingTranscription()
await nextTick()
expect(mockInput.value).toBe(`${prependText} ${mockTranscribedContent}`)
})
it('should trigger auto-send after delay', async () => {
const mockInput = ref('')
const mockSendMessage = vi.fn()
mockHearingStore.autoSendDelay.value = 500
mockHearingStore.configured.value = true
mockHearingStore.autoSendEnabled.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { startStreamingTranscription }
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput, sendMessage: mockSendMessage })
await startStreamingTranscription()
await nextTick()
expect(mockSendMessage).not.toHaveBeenCalled()
vi.advanceTimersByTime(1000)
expect(mockSendMessage).toHaveBeenCalled()
})
it('should clear pending auto-send if disabled', async () => {
const mockInput = ref('')
const mockSendMessage = vi.fn()
mockHearingStore.autoSendDelay.value = 500
mockHearingStore.configured.value = true
mockHearingStore.autoSendEnabled.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { startStreamingTranscription }
= useTranscriptions({ ...createOptions(), messageInputRef: mockInput, sendMessage: mockSendMessage })
await startStreamingTranscription()
await nextTick()
// Disable auto-send before timeout
mockHearingStore.autoSendEnabled.value = false
vi.advanceTimersByTime(1000)
expect(mockSendMessage).not.toHaveBeenCalled()
})
})
describe('cleanup', () => {
it('should stop streaming and clear timeout', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { isListening, startStreamingTranscription, stopStreamingTranscription } = useTranscriptions(createOptions())
await startStreamingTranscription()
await nextTick()
expect(isListening.value).toBe(true)
await stopStreamingTranscription()
await nextTick()
expect(isListening.value).toBe(false)
expect(mockHearingPipeline.stopStreamingTranscription).toHaveBeenCalledWith(true)
})
it('should stop streaming on unmount', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const app = mount({
setup() {
const { startStreamingTranscription } = useTranscriptions(createOptions())
startStreamingTranscription()
},
template: '<div></div>',
})
await nextTick()
expect(mockHearingPipeline.transcribeForMediaStream).toHaveBeenCalled()
app.unmount()
await nextTick()
expect(mockHearingPipeline.stopStreamingTranscription).toHaveBeenCalled()
})
})
describe('reactive watchers', () => {
it('should stop listening if microphone is disabled', async () => {
mockHearingStore.configured.value = true
mockAudioDevice.stream.value = { id: 'stream-1' } as any
mockAudioDevice.enabled.value = true
mockHearingPipeline.supportsStreamInput.value = true
const { isListening, startStreamingTranscription } = useTranscriptions(createOptions())
await startStreamingTranscription()
await nextTick()
expect(isListening.value).toBe(true)
mockAudioDevice.enabled.value = false
await nextTick()
expect(isListening.value).toBe(false)
})
})
})
@@ -0,0 +1,229 @@
import type { MaybeRefOrGetter, Ref } from 'vue'
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { nextTick, onScopeDispose, ref, toValue, watch } from 'vue'
interface TranscriptionOptions {
messageInputRef: Ref<string>
sendMessage: () => void
isStageTamagotchi: MaybeRefOrGetter<boolean>
}
export function useTranscriptions(options: TranscriptionOptions) {
const { messageInputRef: messageInput, sendMessage, isStageTamagotchi } = options
const hearingStore = useHearingStore()
const hearingPipeline = useHearingSpeechInputPipeline()
const { transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline
const { supportsStreamInput } = storeToRefs(hearingPipeline)
const { configured: hearingConfigured, autoSendEnabled, autoSendDelay } = storeToRefs(hearingStore)
const { enabled: hearingEnabled, stream } = storeToRefs(useSettingsAudioDevice())
const providersStore = useProvidersStore()
const { askPermission, startStream } = useSettingsAudioDevice()
const isListening = ref(false)
// Auto-send logic
let autoSendTimeout: ReturnType<typeof setTimeout> | undefined
function clearPendingAutoSend() {
if (autoSendTimeout) {
clearTimeout(autoSendTimeout)
autoSendTimeout = undefined
}
}
async function debouncedAutoSend() {
// Double-check auto-send is enabled before proceeding
if (!autoSendEnabled.value) {
clearPendingAutoSend()
return
}
if (autoSendTimeout) {
clearTimeout(autoSendTimeout)
}
autoSendTimeout = setTimeout(async () => {
// Final check before sending - auto-send might have been disabled while waiting
if (!autoSendEnabled.value) {
clearPendingAutoSend()
return
}
sendMessage()
autoSendTimeout = undefined
}, autoSendDelay.value)
}
const stopStreaming = async () => {
if (!isListening.value)
return
try {
console.info('Stopping transcription...', { source: 'useTranscriptions' })
clearPendingAutoSend()
await stopStreamingTranscription(true)
isListening.value = false
console.info('Transcription stopped', { source: 'useTranscriptions' })
}
catch (err) {
console.error('Error stopping transcription:', err, { source: 'useTranscriptions' })
isListening.value = false
}
}
const startStreaming = async () => {
console.info('Starting streaming transcription', {
enabled: hearingEnabled.value,
hasStream: !!stream.value,
supportsStreamInput: supportsStreamInput.value,
hearingConfigured: hearingConfigured.value,
}, { source: 'useTranscriptions' })
// Auto-configure Web Speech API as default if no provider is configured
if (!hearingConfigured.value) {
console.info('No transcription provider configured. Auto-configuring Web Speech API as default', { source: 'useTranscriptions' })
// Check if Web Speech API is available in the browser
// Web Speech API is NOT available in Electron (stage-tamagotchi) - it requires Google's embedded API keys
// which are not available in Electron, causing it to fail at runtime
const isWebSpeechAvailable = typeof window !== 'undefined'
&& !toValue(isStageTamagotchi) // Explicitly exclude Electron
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
if (!isWebSpeechAvailable) {
// TODO: also propagate to user
const errorMsg = 'Web Speech API is not available and no transcription provider is configured. Please go to Settings > Modules > Hearing to configure a transcription provider. '
console.error(errorMsg, 'Browser support:', {
hasWindow: typeof window !== 'undefined',
hasWebkitSpeechRecognition: typeof window !== 'undefined' && 'webkitSpeechRecognition' in window,
hasSpeechRecognition: typeof window !== 'undefined' && 'SpeechRecognition' in window,
}, { source: 'useTranscriptions' })
isListening.value = false
return
}
// Initialize the provider in the providers store first
try {
providersStore.initializeProvider('browser-web-speech-api')
hearingStore.activeTranscriptionProvider = 'browser-web-speech-api'
}
catch (err) {
console.warn('Error initializing Web Speech API provider:', err, { source: 'useTranscriptions' })
}
// Wait for reactivity to update
await nextTick()
// Verify the provider was set to Web Speech API
if (hearingStore.activeTranscriptionProvider !== 'browser-web-speech-api') {
console.error('Failed to set Web Speech API as default provider', { source: 'useTranscriptions' })
isListening.value = false
return
}
console.info('Web Speech API configured as default provider', { source: 'useTranscriptions' })
}
// Check if streaming input is supported
// TODO: implement non-streaming transcription
if (!supportsStreamInput.value) {
const errorMsg = 'Streaming input not supported by the selected transcription provider. Please select a provider that supports streaming (e.g., Web Speech API).'
console.warn(errorMsg, { source: 'useTranscriptions' })
// Clean up any existing sessions from other pages (e.g., test page) that might interfere
await stopStreamingTranscription(true)
isListening.value = false
return
}
try {
// Request microphone permission if needed (microphone should already be enabled by the user)
if (!stream.value) {
console.info('Requesting microphone permission', { source: 'useTranscriptions' })
await askPermission()
// If still no stream, try starting it manually
if (!stream.value && hearingEnabled.value) {
console.info('Attempting to start stream manually', { source: 'useTranscriptions' })
startStream()
// Wait for the stream to become available with a timeout.
try {
await until(stream).toBeTruthy({ timeout: 3000, throwOnTimeout: true })
}
catch {
console.error('Timed out waiting for audio stream. Stopping transcription.', { source: 'useTranscriptions' })
isListening.value = false
return
}
}
}
}
catch (err) {
console.error('Failed to request microphone permission:', err, { source: 'useTranscriptions' })
isListening.value = false
}
if (!stream.value) {
const errorMsg = 'Failed to get audio stream for transcription. Please check microphone permissions and ensure a device is selected.'
console.error(errorMsg, { source: 'useTranscriptions' })
isListening.value = false
return
}
console.info('Starting streaming transcription with stream:', stream.value.id, { source: 'useTranscriptions' })
// Allow calling this even if already listening - transcribeForMediaStream will handle session reuse/restart
// Call transcribeForMediaStream - it's async so we await it
// Set listening state AFTER successful call
try {
await transcribeForMediaStream(stream.value, {
onSentenceEnd: (delta) => {
if (delta && delta.trim()) {
console.info('Received transcription delta:', delta, { source: 'useTranscriptions' })
// Append transcribed text to message input
const currentText = messageInput.value.trim()
messageInput.value = currentText ? `${currentText} ${delta}` : delta
debouncedAutoSend()
}
},
// Omit onSpeechEnd to avoid re-adding user-deleted text; use sentence deltas only.
})
// Only set listening to true if transcription started successfully
// (transcribeForMediaStream might return early if session already exists)
isListening.value = true
console.info('Streaming transcription initiated successfully', { source: 'useTranscriptions' })
}
catch (err) {
console.error('Transcription error:', err, { source: 'useTranscriptions' })
isListening.value = false
throw err
}
}
// Watch for auto-send setting changes and clear pending sends if disabled
watch(autoSendEnabled, (enabled) => {
if (!enabled) {
clearPendingAutoSend()
console.info('Auto-send disabled', { source: 'useTranscriptions' })
}
})
// Watch for auto-send setting changes and clear pending sends if disabled
watch(hearingEnabled, async (enabled) => {
if (!enabled) {
await stopStreaming()
console.info('Stopping streaming transcription because hearing is disabled.', { source: 'useTranscriptions' })
}
})
onScopeDispose(() => {
clearPendingAutoSend()
stopStreaming()
})
return {
startStreamingTranscription: startStreaming,
stopStreamingTranscription: stopStreaming,
isListening,
autoSendEnabled,
}
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom',
include: ['src/**/*.test.ts'],
},
})
@@ -12,13 +12,10 @@ const props = defineProps<{
overlayDim?: boolean
overlayBlur?: boolean
granted?: boolean
audioInputs?: MediaDeviceInfo[]
volumeLevel?: number
}>()
const showDialog = defineModel('show', { type: Boolean, default: false, required: false })
const selectedAudioInput = defineModel<string>('selectedAudioInput')
const enabled = defineModel<boolean>('enabled', { default: false })
const { isDesktop } = useBreakpoints()
const screenSafeArea = useScreenSafeArea()
@@ -45,9 +42,6 @@ onMounted(() => screenSafeArea.update())
<DialogTitle>Hearing Input</DialogTitle>
</VisuallyHidden>
<HearingConfig
v-model:enabled="enabled"
v-model:selected-audio-input="selectedAudioInput"
:audio-inputs="props.audioInputs"
:granted="props.granted"
:volume-level="props.volumeLevel"
/>
@@ -78,9 +72,6 @@ onMounted(() => screenSafeArea.update())
]"
/>
<HearingConfig
v-model:enabled="enabled"
v-model:selected-audio-input="selectedAudioInput"
:audio-inputs="props.audioInputs"
:granted="props.granted"
:volume-level="props.volumeLevel"
/>
@@ -1,26 +1,35 @@
<script setup lang="ts">
import { Callout, FieldCombobox } from '@proj-airi/ui'
import { Button, Callout, FieldCombobox } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useSettingsAudioDevice } from '../../../../stores'
const props = withDefaults(defineProps<{
enabled?: boolean
granted?: boolean
audioInputs?: MediaDeviceInfo[]
volumeLevel?: number
transcription?: boolean
}>(), {
enabled: false,
granted: false,
audioInputs: () => [],
volumeLevel: 0,
})
const enabled = defineModel<boolean>('enabled')
const selectedAudioInput = defineModel<string>('selectedAudioInput')
const emit = defineEmits(['toggleTranscription'])
const { enabled, selectedAudioInput, audioInputs } = storeToRefs(useSettingsAudioDevice())
const autoSend = defineModel<boolean>('autoSend')
const ringEnabledClass = computed(() => enabled.value
? 'bg-primary-500/15 dark:bg-primary-600/20'
: 'bg-neutral-300/20 dark:bg-neutral-700/20',
)
function toggleHearingEnabled() {
if (enabled.value)
return enabled.value = false
if (!enabled.value && selectedAudioInput.value !== '')
enabled.value = true
}
</script>
<template>
@@ -37,21 +46,21 @@ const ringEnabledClass = computed(() => enabled.value
<div
class="absolute left-1/2 top-1/2 h-24 w-24 rounded-full transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.2 + (props.volumeLevel / 100) * 0.55})`, opacity: String(0.15 + (props.volumeLevel / 100) * 0.2) }"
:class="props.enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
:class="enabled ? 'bg-primary-500/10 dark:bg-primary-600/15' : 'bg-neutral-300/10 dark:bg-neutral-700/10'"
/>
<div
class="absolute left-1/2 top-1/2 h-28 w-28 rounded-full transition-all duration-300 -translate-x-1/2 -translate-y-1/2"
:style="{ transform: `translate(-50%, -50%) scale(${1.5 + (props.volumeLevel / 100) * 0.8})`, opacity: String(0.08 + (props.volumeLevel / 100) * 0.15) }"
:class="props.enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
:class="enabled ? 'bg-primary-500/5 dark:bg-primary-600/10' : 'bg-neutral-300/5 dark:bg-neutral-700/5'"
/>
<!-- Mic icon button -->
<button
class="absolute left-1/2 top-1/2 grid h-16 w-16 place-items-center rounded-full shadow-md outline-none transition-all duration-200 -translate-x-1/2 -translate-y-1/2"
:class="[
props.enabled ? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95' : 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200',
enabled ? 'bg-primary-500 text-white hover:bg-primary-600 active:scale-95' : 'bg-neutral-200 text-neutral-600 hover:bg-neutral-300 active:scale-95 dark:bg-neutral-700 dark:text-neutral-200',
]"
@click="() => enabled = !enabled"
@click="toggleHearingEnabled"
>
<div :class="enabled ? 'i-ph:microphone' : 'i-ph:microphone-slash'" class="h-6 w-6" />
</button>
@@ -70,13 +79,30 @@ const ringEnabledClass = computed(() => enabled.value
</div>
</div>
<div class="flex flex-wrap gap-2">
<Button
v-if="props.transcription !== undefined"
label="Transcription"
:variant="props.transcription ? 'primary' : 'secondary'"
flex-1
@click="() => emit('toggleTranscription')"
/>
<Button
v-if="autoSend !== undefined"
label="Auto send"
:variant="autoSend ? 'primary' : 'secondary'"
flex-1
@click="autoSend = !autoSend"
/>
</div>
<!-- Always-visible device selector -->
<div class="mt-3 w-full">
<FieldCombobox
v-model="selectedAudioInput"
label="Input device"
description="Select the microphone you want to use."
:options="props.audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
placeholder="Select microphone"
layout="vertical"
/>
+224 -46
View File
@@ -138,6 +138,9 @@ catalogs:
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
'@vue/test-utils':
specifier: ^2.4.6
version: 2.4.6
'@vueuse/core':
specifier: 14.1.0
version: 14.1.0
@@ -255,6 +258,9 @@ catalogs:
jose:
specifier: ^6.2.2
version: 6.2.2
jsdom:
specifier: ^29.0.2
version: 29.0.2
knip:
specifier: ^6.4.1
version: 6.4.1
@@ -339,6 +345,9 @@ catalogs:
vite-plugin-mkcert:
specifier: ^2.0.0
version: 2.0.0
vitest:
specifier: ^4.1.4
version: 4.1.4
vitest-browser-vue:
specifier: ^2.1.0
version: 2.1.0
@@ -543,7 +552,7 @@ importers:
version: 12.0.0-beta.1(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))
vitest:
specifier: catalog:vitest
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest-browser-vue:
specifier: 'catalog:'
version: 2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3))
@@ -2491,7 +2500,7 @@ importers:
version: 5.1.9
vieval:
specifier: 'catalog:'
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
@@ -2940,12 +2949,21 @@ importers:
'@types/three':
specifier: ^0.184.0
version: 0.184.0
'@vue/test-utils':
specifier: 'catalog:'
version: 2.4.6
'@webgpu/types':
specifier: 'catalog:'
version: 0.1.69
jsdom:
specifier: 'catalog:'
version: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
unplugin-info:
specifier: 'catalog:'
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest:
specifier: 'catalog:'
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vue-tsc:
specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3)
@@ -3431,7 +3449,7 @@ importers:
version: 5.2.10
'@histoire/plugin-vue':
specifier: 'catalog:'
version: 1.0.0-beta.1(histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
version: 1.0.0-beta.1(histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
'@iconify-json/carbon':
specifier: ^1.2.20
version: 1.2.20
@@ -3503,7 +3521,7 @@ importers:
version: 3.2.3
histoire:
specifier: 'catalog:'
version: 1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
version: 1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
is-network-error:
specifier: 'catalog:'
version: 1.3.1
@@ -3994,7 +4012,7 @@ importers:
version: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vieval:
specifier: 'catalog:'
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
@@ -4579,9 +4597,21 @@ packages:
'@asamuzakjp/css-color@4.1.1':
resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}
'@asamuzakjp/css-color@5.1.11':
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/dom-selector@6.7.6':
resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}
'@asamuzakjp/dom-selector@7.1.1':
resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/generational-cache@1.0.1':
resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
@@ -5314,6 +5344,10 @@ packages:
'@braidai/lang@1.1.2':
resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==}
'@bramus/specificity@2.4.2':
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
'@bufbuild/protobuf@2.10.2':
resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==}
@@ -5405,6 +5439,10 @@ packages:
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
'@csstools/color-helpers@6.0.2':
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
engines: {node: '>=20.19.0'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
@@ -5412,6 +5450,13 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-calc@3.2.0':
resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-color-parser@3.1.0':
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
engines: {node: '>=18'}
@@ -5419,20 +5464,41 @@ packages:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@4.1.0':
resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-syntax-patches-for-csstree@1.0.25':
resolution: {integrity: sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==}
engines: {node: '>=18'}
'@csstools/css-parser-algorithms@4.0.0':
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-syntax-patches-for-csstree@1.1.3':
resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==}
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@csstools/css-tokenizer@4.0.0':
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
@@ -6132,13 +6198,13 @@ packages:
resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@exodus/bytes@1.8.0':
resolution: {integrity: sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==}
'@exodus/bytes@1.15.0':
resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
'@exodus/crypto': ^1.0.0-rc.4
'@noble/hashes': ^1.8.0 || ^2.0.0
peerDependenciesMeta:
'@exodus/crypto':
'@noble/hashes':
optional: true
'@ffmpeg-installer/darwin-arm64@4.1.5':
@@ -11908,6 +11974,10 @@ packages:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'}
data-urls@7.0.0:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@@ -14070,6 +14140,15 @@ packages:
canvas:
optional: true
jsdom@29.0.2:
resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -16899,8 +16978,8 @@ packages:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
engines: {node: '>=0.8'}
tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
tough-cookie@6.0.1:
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
engines: {node: '>=16'}
tr46@0.0.3:
@@ -17132,6 +17211,10 @@ packages:
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
engines: {node: '>=18.17'}
undici@7.25.0:
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
engines: {node: '>=20.18.1'}
undici@8.1.0:
resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==}
engines: {node: '>=22.19.0'}
@@ -18022,6 +18105,10 @@ packages:
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=20'}
whatwg-url@16.0.1:
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -18500,6 +18587,14 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
lru-cache: 11.3.5
'@asamuzakjp/css-color@5.1.11':
dependencies:
'@asamuzakjp/generational-cache': 1.0.1
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@asamuzakjp/dom-selector@6.7.6':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
@@ -18508,6 +18603,16 @@ snapshots:
is-potential-custom-element-name: 1.0.1
lru-cache: 11.3.5
'@asamuzakjp/dom-selector@7.1.1':
dependencies:
'@asamuzakjp/generational-cache': 1.0.1
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
css-tree: 3.2.1
is-potential-custom-element-name: 1.0.1
'@asamuzakjp/generational-cache@1.0.1': {}
'@asamuzakjp/nwsapi@2.3.9': {}
'@ax-llm/ax@19.0.45(zod@4.3.6)':
@@ -19471,6 +19576,10 @@ snapshots:
'@braidai/lang@1.1.2': {}
'@bramus/specificity@2.4.2':
dependencies:
css-tree: 3.2.1
'@bufbuild/protobuf@2.10.2': {}
'@capacitor/android@8.3.1(@capacitor/core@8.3.1)':
@@ -19610,11 +19719,18 @@ snapshots:
'@csstools/color-helpers@5.1.0': {}
'@csstools/color-helpers@6.0.2': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.1.0
@@ -19622,14 +19738,29 @@ snapshots:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/color-helpers': 6.0.2
'@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-syntax-patches-for-csstree@1.0.25': {}
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
'@csstools/css-tokenizer@3.0.4': {}
'@csstools/css-tokenizer@4.0.0': {}
'@date-fns/tz@1.4.1': {}
'@date-fns/utc@2.1.1': {}
@@ -20225,7 +20356,9 @@ snapshots:
'@eslint/core': 1.2.1
levn: 0.4.1
'@exodus/bytes@1.8.0': {}
'@exodus/bytes@1.15.0(@noble/hashes@2.0.1)':
optionalDependencies:
'@noble/hashes': 2.0.1
'@ffmpeg-installer/darwin-arm64@4.1.5':
optional: true
@@ -20361,14 +20494,14 @@ snapshots:
transitivePeerDependencies:
- vite
'@histoire/plugin-vue@1.0.0-beta.1(histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
'@histoire/plugin-vue@1.0.0-beta.1(histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@histoire/controls': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@histoire/shared': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@histoire/vendors': 1.0.0-beta.1
change-case: 5.4.4
globby: 14.1.0
histoire: 1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
histoire: 1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
launch-editor: 2.12.0
pathe: 1.1.2
vue: 3.5.32(typescript@5.9.3)
@@ -24330,7 +24463,7 @@ snapshots:
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
playwright: 1.59.1
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- bufferutil
- msw
@@ -24343,7 +24476,7 @@ snapshots:
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
playwright: 1.59.1
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- bufferutil
- msw
@@ -24360,7 +24493,7 @@ snapshots:
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
@@ -24377,7 +24510,7 @@ snapshots:
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
@@ -24398,7 +24531,7 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
optionalDependencies:
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
@@ -24410,7 +24543,7 @@ snapshots:
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.51.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
typescript: 5.9.3
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- supports-color
@@ -25559,7 +25692,7 @@ snapshots:
drizzle-orm: 0.41.0(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9)
pg: 8.20.0
react: 19.2.3
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vue: 3.5.32(typescript@5.9.3)
better-auth@1.6.5(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(better-sqlite3@12.5.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9))(pg@8.20.0)(react@19.2.3)(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3)):
@@ -25588,7 +25721,7 @@ snapshots:
drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.28.14)(pg@8.20.0)(postgres@3.4.9)
pg: 8.20.0
react: 19.2.3
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vue: 3.5.32(typescript@5.9.3)
transitivePeerDependencies:
- '@cloudflare/workers-types'
@@ -26239,7 +26372,7 @@ snapshots:
cssstyle@5.3.7:
dependencies:
'@asamuzakjp/css-color': 4.1.1
'@csstools/css-syntax-patches-for-csstree': 1.0.25
'@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
css-tree: 3.2.1
lru-cache: 11.3.5
@@ -26415,6 +26548,13 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 15.1.0
data-urls@7.0.0(@noble/hashes@2.0.1):
dependencies:
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.1(@noble/hashes@2.0.1)
transitivePeerDependencies:
- '@noble/hashes'
date-fns@4.1.0: {}
dayjs@1.11.20: {}
@@ -28326,7 +28466,7 @@ snapshots:
gray-matter: 4.0.3
unplugin: 3.0.0
histoire@1.0.0-beta.1(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3):
histoire@1.0.0-beta.1(@noble/hashes@2.0.1)(@types/node@25.6.0)(bufferutil@4.1.0)(canvas@3.2.3)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(utf-8-validate@5.0.10)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3):
dependencies:
'@akryum/tinypool': 0.3.1
'@histoire/app': 1.0.0-beta.1(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
@@ -28344,7 +28484,7 @@ snapshots:
globby: 14.1.0
gray-matter: 4.0.3
jiti: 2.6.1
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
jsdom: 27.4.0(@noble/hashes@2.0.1)(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
markdown-it: 14.1.1
markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1)
markdown-it-attrs: 4.3.1(markdown-it@14.1.1)
@@ -28359,7 +28499,7 @@ snapshots:
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- '@exodus/crypto'
- '@noble/hashes'
- '@types/node'
- bufferutil
- canvas
@@ -28393,11 +28533,11 @@ snapshots:
dependencies:
lru-cache: 6.0.0
html-encoding-sniffer@6.0.0:
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
dependencies:
'@exodus/bytes': 1.8.0
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
transitivePeerDependencies:
- '@exodus/crypto'
- '@noble/hashes'
html-entities@2.6.0: {}
@@ -28812,22 +28952,22 @@ snapshots:
jsdoc-type-pratt-parser@7.2.0: {}
jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10):
jsdom@27.4.0(@noble/hashes@2.0.1)(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10):
dependencies:
'@acemir/cssom': 0.9.31
'@asamuzakjp/dom-selector': 6.7.6
'@exodus/bytes': 1.8.0
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
cssstyle: 5.3.7
data-urls: 6.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 6.0.0
html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
parse5: 8.0.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.0
tough-cookie: 6.0.1
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 4.0.0
@@ -28837,11 +28977,39 @@ snapshots:
optionalDependencies:
canvas: 3.2.3
transitivePeerDependencies:
- '@exodus/crypto'
- '@noble/hashes'
- bufferutil
- supports-color
- utf-8-validate
jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3):
dependencies:
'@asamuzakjp/css-color': 5.1.11
'@asamuzakjp/dom-selector': 7.1.1
'@bramus/specificity': 2.4.2
'@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1)
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
css-tree: 3.2.1
data-urls: 7.0.0(@noble/hashes@2.0.1)
decimal.js: 10.6.0
html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1)
is-potential-custom-element-name: 1.0.1
lru-cache: 11.3.5
parse5: 8.0.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.1
undici: 7.25.0
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.1(@noble/hashes@2.0.1)
xml-name-validator: 5.0.0
optionalDependencies:
canvas: 3.2.3
transitivePeerDependencies:
- '@noble/hashes'
jsesc@3.1.0: {}
json-bigint@1.0.0:
@@ -32460,7 +32628,7 @@ snapshots:
psl: 1.15.0
punycode: 2.3.1
tough-cookie@6.0.0:
tough-cookie@6.0.1:
dependencies:
tldts: 7.0.19
@@ -32675,6 +32843,8 @@ snapshots:
undici@6.24.1: {}
undici@7.25.0: {}
undici@8.1.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -33204,7 +33374,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vieval@0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
vieval@0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
'@moeru/std': 0.1.0-beta.17
'@pnpm/find-workspace-dir': 1000.1.5
@@ -33219,7 +33389,7 @@ snapshots:
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- '@edge-runtime/vm'
- '@opentelemetry/api'
@@ -33591,10 +33761,10 @@ snapshots:
vitest-browser-vue@2.1.0(vitest@4.1.4)(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/test-utils': 2.4.6
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vue: 3.5.32(typescript@5.9.3)
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
@@ -33621,11 +33791,11 @@ snapshots:
'@types/node': 24.12.2
'@vitest/browser-playwright': 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
'@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
transitivePeerDependencies:
- msw
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
@@ -33652,7 +33822,7 @@ snapshots:
'@types/node': 25.6.0
'@vitest/browser-playwright': 4.1.4(bufferutil@4.1.0)(playwright@1.59.1)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
'@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4)
jsdom: 27.4.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10)
jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3)
transitivePeerDependencies:
- msw
@@ -33972,6 +34142,14 @@ snapshots:
tr46: 6.0.0
webidl-conversions: 8.0.1
whatwg-url@16.0.1(@noble/hashes@2.0.1):
dependencies:
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
tr46: 6.0.0
webidl-conversions: 8.0.1
transitivePeerDependencies:
- '@noble/hashes'
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
+3
View File
@@ -73,6 +73,7 @@ catalog:
'@types/unist': ^3.0.3
'@types/whatwg-mimetype': ^5.0.0
'@types/ws': ^8.18.1
'@vue/test-utils': ^2.4.6
'@vueuse/core': 14.1.0
'@webgpu/types': ^0.1.69
'@xsai-ext/providers': 0.5.0-beta.2
@@ -112,6 +113,7 @@ catalog:
is-network-error: ^1.3.1
isolated-vm: ^6.1.2
jose: ^6.2.2
jsdom: ^29.0.2
knip: ^6.4.1
meow: ^14.1.0
mkcert: ^3.2.0
@@ -140,6 +142,7 @@ catalog:
vite: ^8.0.8
vite-plugin-inspect: 12.0.0-beta.1
vite-plugin-mkcert: ^2.0.0
vitest: ^4.1.4
vitest-browser-vue: ^2.1.0
vue: ^3.5.32
vue-router: ^5.0.4