Merge branch 'main' into luoling8192/feat/official-asr-rewrite-model

This commit is contained in:
Neko
2026-10-28 23:08:46 +08:00
committed by GitHub
26 changed files with 339 additions and 325 deletions
@@ -57,7 +57,8 @@ jobs:
- depot: linux
os: ubuntu-latest
builder-args: --linux --x64
# The depot ships `dist/linux-unpacked`, so electron-builder builds no Linux package.
builder-args: --linux dir --x64
arch: x64
steps:
+43
View File
@@ -296,6 +296,13 @@ jobs:
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_DEVELOPER_TEAM_ID }}
# fpm writes the `.MTREE` file of the pacman package with bsdtar.
- name: Install Linux Packaging Dependencies (Linux Only)
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm'
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libarchive-tools
- name: Build (Linux Only) # Linux
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm'
run: pnpm run -F @proj-airi/stage-tamagotchi build && pnpm -F @proj-airi/stage-tamagotchi exec electron-builder build ${{ matrix.builder-args }} --publish=${{ (inputs.build_only || inputs.artifacts_only) && 'never' || 'onTagOrDraft' }}
@@ -355,6 +362,8 @@ jobs:
run: |
echo "DEB_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename deb)" >> $GITHUB_ENV
echo "RPM_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename rpm)" >> $GITHUB_ENV
echo "PACMAN_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename pacman)" >> $GITHUB_ENV
echo "APPIMAGE_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename AppImage)" >> $GITHUB_ENV
echo "FLATPAK_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename flatpak)" >> $GITHUB_ENV
- name: Upload Artifacts (Nightly + Non-Linux)
@@ -379,6 +388,20 @@ jobs:
name: ${{ env.RPM_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.RPM_BUNDLE_NAME }}
- name: Upload Artifacts (Nightly + Linux pacman)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.PACMAN_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.PACMAN_BUNDLE_NAME }}
- name: Upload Artifacts (Nightly + Linux AppImage)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.APPIMAGE_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.APPIMAGE_BUNDLE_NAME }}
- name: Upload Flatpak Artifact (Nightly + Linux Only)
if: ${{ github.event_name == 'schedule' && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
@@ -486,6 +509,8 @@ jobs:
run: |
echo "DEB_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename deb --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
echo "RPM_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename rpm --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
echo "PACMAN_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename pacman --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
echo "APPIMAGE_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename AppImage --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
echo "FLATPAK_BUNDLE_NAME=$(pnpm exec tsx scripts/artifacts-metadata.ts ${{ matrix.target }} --get-filename flatpak --release ${{ !inputs.build_only && !inputs.artifacts_only }} --tag ${{ inputs.tag }} --auto-tag ${{ !inputs.build_only }})" >> $GITHUB_ENV
- name: Upload Artifacts (Manual + Non-Release + Non-Linux)
@@ -510,6 +535,20 @@ jobs:
name: ${{ env.RPM_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.RPM_BUNDLE_NAME }}
- name: Upload Artifacts (Manual + Non-Release + Linux pacman)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.PACMAN_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.PACMAN_BUNDLE_NAME }}
- name: Upload Artifacts (Manual + Non-Release + Linux AppImage)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
with:
name: ${{ env.APPIMAGE_BUNDLE_NAME }}
path: apps/stage-tamagotchi/bundle/${{ env.APPIMAGE_BUNDLE_NAME }}
- name: Upload Flatpak Artifact (Manual + Non-Release + Linux)
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.build_only && inputs.artifacts_only && (matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm') }}
uses: actions/upload-artifact@v7
@@ -570,6 +609,8 @@ jobs:
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.dmg
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.deb
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.rpm
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.pacman
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.AppImage
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.flatpak
apps/stage-tamagotchi/bundle/latest-*.yml
append_body: true
@@ -683,6 +724,8 @@ jobs:
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.dmg
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.deb
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.rpm
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.pacman
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.AppImage
apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.flatpak
apps/stage-tamagotchi/bundle/latest-*.yml
append_body: true
+1 -1
View File
@@ -23,7 +23,7 @@ API details belong in the [component reference](./docs/ai/context/ui-components.
## Colors
- [UnoCSS configuration](./uno.config.ts) provides Chromatic palettes, fonts, icons, and shared animations.
- [UI fallback variables](./packages/ui/src/fallback.css) define `--chromatic-hue` and shade-specific chroma variables.
- [UI fallback variables](./packages/ui/src/fallback.css) define the `--chromatic-hue` fallback.
- [Theme settings](./packages/stage-ui/src/stores/settings/theme.ts) store the selected hue. The default is `220.44`.
- [The UI stylesheet entry](./packages/ui/src/main.css) includes fallback variables, lamp animation, and component styles.
+5 -18
View File
@@ -82,26 +82,13 @@ function registerAuthenticatedSetup() {
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
}
const primaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`
})
const secondaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 90%, oklch(90% 0 360))`
})
const tertiaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 90%, oklch(90% 0 360))`
})
// Defined in `styles/main.css`, so they follow the theme hue (including `dynamic-hue`) and dark mode.
const primaryColor = 'var(--stage-transition-primary)'
const secondaryColor = 'var(--stage-transition-secondary)'
const tertiaryColor = 'var(--stage-transition-tertiary)'
const colors = computed(() => {
return [primaryColor.value, secondaryColor.value, tertiaryColor.value, isDark.value ? '#121212' : '#FFFFFF']
return [primaryColor, secondaryColor, tertiaryColor, isDark.value ? '#121212' : '#FFFFFF']
})
watch(settings.language, () => {
+19 -2
View File
@@ -43,12 +43,29 @@ html.dark {
color-scheme: dark;
}
/*
* Stage transition colours (StageTransitionGroup in App.vue): pale tints of the theme. They live in CSS so they follow
* `--chromatic-hue`, including the `dynamic-hue` animation, and dark mode.
*/
:root {
--stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360));
--stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 90%, oklch(90% 0 360));
/* primary + 60°: a third of the way from primary (h) to complementary (h + 180°), going up in hue */
--stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 90%, oklch(90% 0 360));
}
.dark {
--stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360));
--stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 70%, oklch(50% 0 360));
--stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 70%, oklch(50% 0 360));
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360));
background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360));
opacity: 0.75;
position: fixed;
z-index: 1031;
@@ -59,7 +76,7 @@ html.dark {
}
.dark #nprogress .bar {
background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`;
background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360));
}
@media (display-mode: standalone) {
@@ -248,9 +248,12 @@ export default {
artifactName: '${productName}-${version}-darwin-${arch}.${ext}',
},
linux: {
// AUR packages and Arch users install the `pacman` package. Other distributions can run the AppImage.
target: [
'deb',
'rpm',
'pacman',
'AppImage',
],
// NOTICE: Same channel rule as Windows/macOS. Keep `${arch}` to avoid x64/arm64 feed collisions on Linux.
publish: {
@@ -269,6 +272,38 @@ export default {
appImage: {
artifactName: '${productName}-${version}-linux-${arch}.${ext}',
},
pacman: {
// NOTICE:
// The default depends name `http-parser` and `libappindicator-gtk3`. Arch removed both, so `pacman -U` fails.
// Source: getDefaultDepends in app-builder-lib/out/targets/FpmTarget.js.
// Remove it when electron-builder updates its pacman defaults.
// This list comes from `ldd`, plus the libraries that Electron loads at run time.
depends: [
'alsa-lib',
'at-spi2-core',
'gtk3',
'libcups',
'libnotify',
'libsecret',
'libxkbcommon',
'libxss',
'libxt',
'libxtst',
'mesa',
'nspr',
'nss',
'util-linux-libs',
'xdg-utils',
],
},
rpm: {
// NOTICE:
// Two Electron rpm packages conflict on the same `/usr/lib/.build-id` links, so one of them fails to install.
// rpmbuild creates these links for each ELF file. Electron ships the same Chromium binaries in each app.
// https://github.com/jordansissel/fpm/issues/1503, https://github.com/jgraph/drawio-desktop/issues/259
// Remove it when fpm stops the creation of build-id links by default.
fpm: ['--rpm-rpmbuild-define', '_build_id_links none'],
},
npmRebuild: false,
} satisfies Configuration
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import packageJSON from '../package.json' with { type: 'json' }
import { getFilenames } from './utils'
const release = { release: true, autoTag: false, tag: ['v0.13.1'] }
describe('getFilenames', () => {
// electron-builder names the architecture differently for each package format.
// See `getArtifactArchName` in `builder-util/out/arch.js`.
it.each([
{
target: 'x86_64-unknown-linux-gnu',
expected: {
'deb': 'AIRI-0.13.1-linux-amd64.deb',
'rpm': 'AIRI-0.13.1-linux-x86_64.rpm',
'pacman': 'AIRI-0.13.1-linux-x64.pacman',
'AppImage': 'AIRI-0.13.1-linux-x86_64.AppImage',
'flatpak': 'AIRI-0.13.1-linux-x64.flatpak',
'latest-x64-linux.yml': 'latest-x64-linux.yml',
},
},
{
target: 'aarch64-unknown-linux-gnu',
expected: {
'deb': 'AIRI-0.13.1-linux-arm64.deb',
'rpm': 'AIRI-0.13.1-linux-aarch64.rpm',
'pacman': 'AIRI-0.13.1-linux-aarch64.pacman',
'AppImage': 'AIRI-0.13.1-linux-arm64.AppImage',
'flatpak': 'AIRI-0.13.1-linux-arm64.flatpak',
'latest-arm64-linux-arm64.yml': 'latest-arm64-linux-arm64.yml',
},
},
])('names each Linux package of $target as electron-builder does', async ({ target, expected }) => {
const filenames = await getFilenames(target, release)
expect(Object.fromEntries(filenames.map(filename => [filename.extension, filename.releaseArtifactFilename]))).toEqual(expected)
})
it('finds each Linux build output under the version of package.json', async () => {
const filenames = await getFilenames('x86_64-unknown-linux-gnu', release)
expect(filenames.find(filename => filename.extension === 'AppImage')?.outputFilename)
.toBe(`AIRI-${packageJSON.version}-linux-x86_64.AppImage`)
})
it('marks only the update feed as optional', async () => {
const filenames = await getFilenames('x86_64-unknown-linux-gnu', release)
expect(filenames.filter(filename => filename.optional).map(filename => filename.extension)).toEqual(['latest-x64-linux.yml'])
})
})
+53 -203
View File
@@ -84,22 +84,24 @@ interface FilenameOutputEntry {
optional?: boolean
}
/**
* Returns the architecture name that electron-builder writes into the artifact name of `ext`.
*
* The names differ by package format. See `getArtifactArchName` in `builder-util/out/arch.js`.
*/
export function mapArchFor(
target: string,
ext: string,
): string {
switch (true) {
case target === 'aarch64-unknown-linux-gnu':
if (ext === 'rpm') {
if (ext === 'rpm' || ext === 'pacman') {
return 'aarch64'
}
if (ext === 'deb') {
return 'arm64'
}
return 'arm64'
case target === 'x86_64-unknown-linux-gnu':
if (ext === 'rpm') {
if (ext === 'rpm' || ext === 'AppImage') {
return 'x86_64'
}
if (ext === 'deb') {
@@ -140,6 +142,51 @@ function getMacZipFilename(productName: string, version: string, target: string)
return `${productName}-${version}-${archPrefix}mac.zip`
}
/** The Linux package formats that electron-builder builds, in the order of `linux.target`. */
const LINUX_ELECTRON_BUILDER_EXTENSIONS = ['deb', 'rpm', 'pacman', 'AppImage']
function getLinuxFilenames(
target: string,
electronBuilder: Configuration,
productName: string,
beforeVersion: string,
version: string,
): FilenameOutputEntry[] {
const linux = electronBuilder.linux
const artifacts: FilenameOutputEntry[] = []
if (linux?.artifactName) {
const targets = [linux.target ?? []].flat().map(entry => typeof entry === 'string' ? entry : entry.target)
// Flatpak is built outside electron-builder, but it follows the Linux name template.
const extensions = [...LINUX_ELECTRON_BUILDER_EXTENSIONS.filter(extension => targets.includes(extension)), 'flatpak']
for (const extension of extensions) {
artifacts.push({
target,
extension,
outputFilename: applyTemplateOfArtifactName(linux.artifactName, productName, beforeVersion, mapArchFor(target, extension), extension),
releaseArtifactFilename: applyTemplateOfArtifactName(linux.artifactName, productName, version, mapArchFor(target, extension), extension),
productName,
version,
})
}
}
const latestUpdateFilename = getLatestUpdateFilename(target)
if (latestUpdateFilename) {
artifacts.push({
target,
extension: latestUpdateFilename,
outputFilename: latestUpdateFilename,
releaseArtifactFilename: latestUpdateFilename,
productName,
version,
optional: true,
})
}
return artifacts
}
export async function getFilenames(target: string, options: { release: boolean, autoTag: boolean, tag: string[] }): Promise<FilenameOutputEntry[]> {
const electronBuilder = await getElectronBuilderConfig()
const version = await getVersion(options)
@@ -187,205 +234,8 @@ export async function getFilenames(target: string, options: { release: boolean,
},
]
case 'x86_64-unknown-linux-gnu':
{
const artifacts: FilenameOutputEntry[] = []
if (electronBuilder.linux?.artifactName) {
if (
(Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('deb'))
|| electronBuilder.linux.target === 'deb'
) {
artifacts.push(
{
target: 'x86_64-unknown-linux-gnu',
extension: 'deb',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'deb'),
'deb',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'deb'),
'deb',
),
productName,
version,
},
)
}
if (
(Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('rpm'))
|| electronBuilder.linux.target === 'rpm'
) {
artifacts.push(
{
target: 'x86_64-unknown-linux-gnu',
extension: 'rpm',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'rpm'),
'rpm',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'rpm'),
'rpm',
),
productName,
version,
},
)
}
// Flatpak artifact (built outside electron-builder, but we follow linux template)
artifacts.push(
{
target: 'x86_64-unknown-linux-gnu',
extension: 'flatpak',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'flatpak'),
'flatpak',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'flatpak'),
'flatpak',
),
productName,
version,
},
)
}
const latestUpdateFilename = getLatestUpdateFilename(target)
if (latestUpdateFilename) {
artifacts.push({
target: 'x86_64-unknown-linux-gnu',
extension: latestUpdateFilename,
outputFilename: latestUpdateFilename,
releaseArtifactFilename: latestUpdateFilename,
productName,
version,
optional: true,
})
}
return artifacts
}
case 'aarch64-unknown-linux-gnu':
{
const artifacts: FilenameOutputEntry[] = []
if (electronBuilder.linux?.artifactName) {
if (
(Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('deb'))
|| electronBuilder.linux.target === 'deb'
) {
artifacts.push(
{
target: 'aarch64-unknown-linux-gnu',
extension: 'deb',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'deb'),
'deb',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'deb'),
'deb',
),
productName,
version,
},
)
}
if (
(Array.isArray(electronBuilder.linux.target) && electronBuilder.linux.target.includes('rpm'))
|| electronBuilder.linux.target === 'rpm'
) {
artifacts.push(
{
target: 'aarch64-unknown-linux-gnu',
extension: 'rpm',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'rpm'),
'rpm',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'rpm'),
'rpm',
),
productName,
version,
},
)
}
// Flatpak artifact (built outside electron-builder, but we follow linux template)
artifacts.push(
{
target: 'aarch64-unknown-linux-gnu',
extension: 'flatpak',
outputFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
beforeVersion,
mapArchFor(target, 'flatpak'),
'flatpak',
),
releaseArtifactFilename: applyTemplateOfArtifactName(
electronBuilder.linux.artifactName!,
productName,
version,
mapArchFor(target, 'flatpak'),
'flatpak',
),
productName,
version,
},
)
}
const latestUpdateFilename = getLatestUpdateFilename(target)
if (latestUpdateFilename) {
artifacts.push({
target: 'aarch64-unknown-linux-gnu',
extension: latestUpdateFilename,
outputFilename: latestUpdateFilename,
releaseArtifactFilename: latestUpdateFilename,
productName,
version,
optional: true,
})
}
return artifacts
}
return getLinuxFilenames(target, electronBuilder, productName, beforeVersion, version)
case 'aarch64-apple-darwin':
{
const artifacts: FilenameOutputEntry[] = [
@@ -293,6 +293,21 @@ describe('setupAutoUpdater', () => {
expect(service.state.status).toBe('not-available')
})
// An install with its own user data directory, for example a test build, must not write into the log of another install.
it('writes the updater log in the user data directory and reports that path', async () => {
const { appendFile } = await import('node:fs/promises')
const { join } = await import('node:path')
const fake = createFakeUpdater()
const { setupAutoUpdater } = await import('./auto-updater')
const service = setupAutoUpdater({ updater: fake.updater })
const logFile = join('/tmp/userData', 'logs', 'updater-log.txt')
fake.setStatus({ status: 'error', error: { code: 'network-error', message: 'Offline.' } })
await vi.waitFor(() => expect(appendFile).toHaveBeenCalledWith(logFile, expect.stringContaining('network-error')))
expect(service.state.diagnostics?.logFilePath).toBe(logFile)
})
it('does not touch the updater when the launcher turned it off', async () => {
const fake = createFakeUpdater({ status: 'disabled', reason: 'store-build' })
const { setupAutoUpdater } = await import('./auto-updater')
@@ -28,14 +28,12 @@ import { channelOfVersion, updateChannels } from '../../../shared/update-channel
const UPDATE_CHANNEL_ENV_KEY = 'AIRI_UPDATE_CHANNEL'
function getCacheRoot() {
// NOTICE: Electron resolves the cache directory per platform/app, but the
// shipped type definitions here do not expose `cache`, so we cast the key.
return app.getPath('cache' as Parameters<typeof app.getPath>[0])
}
/**
* The updater log is next to the session logs of `app/file-logger.ts`, in the user data directory. So an install with
* its own user data directory, for example a test build, writes its own log.
*/
function updaterLogFile() {
return join(getCacheRoot(), 'stage-tamagotchi-updater', 'updater-log.txt')
return join(app.getPath('userData'), 'logs', 'updater-log.txt')
}
async function logToFile(level: 'INFO' | 'WARN' | 'ERROR', message: string) {
+5 -18
View File
@@ -90,26 +90,13 @@ watch(showingSetup, (visible) => {
startupOnboarding.value = false
})
const primaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`
})
const secondaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${180})) 90%, oklch(90% 0 360))`
})
const tertiaryColor = computed(() => {
return isDark.value
? `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 70%, oklch(50% 0 360))`
: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${60})) 90%, oklch(90% 0 360))`
})
// Defined in `styles/main.css`, so they follow the theme hue (including `dynamic-hue`) and dark mode.
const primaryColor = 'var(--stage-transition-primary)'
const secondaryColor = 'var(--stage-transition-secondary)'
const tertiaryColor = 'var(--stage-transition-tertiary)'
const colors = computed(() => {
return [primaryColor.value, secondaryColor.value, tertiaryColor.value, isDark.value ? '#121212' : '#FFFFFF']
return [primaryColor, secondaryColor, tertiaryColor, isDark.value ? '#121212' : '#FFFFFF']
})
router.beforeEach((to, from) => {
+19 -2
View File
@@ -37,12 +37,29 @@ html.dark {
color-scheme: dark;
}
/*
* Stage transition colours (StageTransitionGroup in App.vue): pale tints of the theme. They live in CSS so they follow
* `--chromatic-hue`, including the `dynamic-hue` animation, and dark mode.
*/
:root {
--stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360));
--stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 90%, oklch(90% 0 360));
/* primary + 60°: a third of the way from primary (h) to complementary (h + 180°), going up in hue */
--stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 90%, oklch(90% 0 360));
}
.dark {
--stage-transition-primary: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360));
--stage-transition-secondary: color-mix(in srgb, theme('colors.complementary.200') 70%, oklch(50% 0 360));
--stage-transition-tertiary: color-mix(in srgb, color-mix(in oklch increasing hue, theme('colors.primary.200') 66.667%, theme('colors.complementary.200')) 70%, oklch(50% 0 360));
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360));
background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360));
opacity: 0.75;
position: fixed;
z-index: 1031;
@@ -53,7 +70,7 @@ html.dark {
}
.dark #nprogress .bar {
background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`;
background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360));
}
@media (display-mode: standalone) {
+2 -2
View File
@@ -42,7 +42,7 @@ html.dark {
}
#nprogress .bar {
background: color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 70%, oklch(50% 0 360));
background: color-mix(in srgb, theme('colors.primary.200') 70%, oklch(50% 0 360));
opacity: 0.75;
position: fixed;
z-index: 1031;
@@ -53,7 +53,7 @@ html.dark {
}
.dark #nprogress .bar {
background: `color-mix(in srgb, oklch(95% var(--chromatic-chroma-900) calc(var(--chromatic-hue) + ${0})) 90%, oklch(90% 0 360))`;
background: color-mix(in srgb, theme('colors.primary.200') 90%, oklch(90% 0 360));
}
@media (display-mode: standalone) {
+1 -1
View File
@@ -1 +1 @@
sha256-f/u7/wIlSBHUpOhZlD4zoSQuI0ENofreBe/H8oZNag4=
sha256-ynePLbeSYhTg8eWw8p86x7P9lJuEyw85NzsqwAQ6rIw=
@@ -1,20 +1,25 @@
<script setup lang="ts">
import { useTheme } from '@proj-airi/ui'
import { PartAnimatedWave, PatternCross } from '.'
const { isDark } = useTheme()
</script>
<template>
<PatternCross>
<PatternCross class="default-background">
<PartAnimatedWave
:fill-color="isDark
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
fill-color="var(--default-background-wave)"
class="h-full w-full"
>
<slot />
</PartAnimatedWave>
</PatternCross>
</template>
<style scoped>
/* In CSS, so the wave follows the theme hue (including `dynamic-hue`) and dark mode. */
.default-background {
--default-background-wave: color-mix(in srgb, theme('colors.primary.100') 80%, oklch(100% 0 360));
}
.dark .default-background {
--default-background-wave: theme('colors.primary.900');
}
</style>
@@ -11,7 +11,7 @@ https://www.magicpattern.design/tools/css-backgrounds
*/
.cross-background-container {
background-color: #ffffff;
--cross-color: oklch(0.85 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue) / 0.2);
--cross-color: theme('colors.primary.200 / 20%');
background:
radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent),
radial-gradient(circle, transparent 20%, #ffffff 20%, #ffffff 80%, transparent 80%, transparent) 25px 25px,
@@ -26,7 +26,7 @@ https://www.magicpattern.design/tools/css-backgrounds
.dark .cross-background-container {
background-color: #121212;
--cross-color: oklch(0.2 calc(var(--chromatic-chroma) * 0.3) var(--chromatic-hue));
--cross-color: theme('colors.primary.950');
background:
radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent),
radial-gradient(circle, transparent 20%, #121212 20%, #121212 80%, transparent 80%, transparent) 25px 25px,
@@ -1,20 +1,25 @@
<script setup lang="ts">
import { useTheme } from '@proj-airi/ui'
import { PartAnimatedWave, PatternCross } from '.'
const { isDark } = useTheme()
</script>
<template>
<PatternCross>
<PatternCross class="default-background">
<PartAnimatedWave
:fill-color="isDark
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
fill-color="var(--default-background-wave)"
class="h-full w-full"
>
<div class="relative h-full w-full from-black/10 via-black/0 to-black/0 bg-gradient-to-b" />
</PartAnimatedWave>
</PatternCross>
</template>
<style scoped>
/* In CSS, so the wave follows the theme hue (including `dynamic-hue`) and dark mode. */
.default-background {
--default-background-wave: color-mix(in srgb, theme('colors.primary.100') 80%, oklch(100% 0 360));
}
.dark .default-background {
--default-background-wave: theme('colors.primary.900');
}
</style>
@@ -5,6 +5,7 @@ import { createI18n } from 'vue-i18n'
import Workbench from './workbench.vue'
import '@proj-airi/ui/main.css'
import 'virtual:uno.css'
function createTestI18n() {
return createI18n({
@@ -24,6 +25,10 @@ describe('live2d motion workbench', () => {
//
// The workbench used an undefined --color-primary-400 variable. Dockview
// created its drop target, but the browser resolved its background to transparent.
//
// The accent is now `theme('colors.primary.400')`, which reads the chromatic preset's
// custom properties from the UnoCSS preflight, so the test loads `virtual:uno.css`
// like the apps do.
await render(Workbench, {
attrs: { style: 'width: 1200px; height: 800px' },
global: { plugins: [createTestI18n()] },
@@ -105,7 +105,7 @@ function handleReady({ api }: DockviewReadyEvent) {
@import 'dockview-vue/dist/styles/dockview.css';
.dockview-theme-airi {
--motion-workbench-accent: oklch(clamp(0%, calc(74% * var(--chromatic-bri, 1)), 100%) calc(var(--chromatic-chroma-400) * var(--chromatic-sat, 1)) calc(var(--chromatic-hue) + 0));
--motion-workbench-accent: theme('colors.primary.400');
--dv-tabs-and-actions-container-height: 2.25rem;
--dv-tabs-and-actions-container-background-color: rgb(250 250 250 / 0.92);
--dv-group-view-background-color: rgb(255 255 255 / 0.68);
@@ -252,6 +252,27 @@ describe('wakeWordDetector', () => {
expect(harness.spotter.setKeywords).not.toHaveBeenCalled()
})
it('becomes ready when the same catalog arrives again while the spotter is loading', async () => {
let finishLoad!: () => void
const loaded = new Promise<void>(resolve => finishLoad = resolve)
const harness = createHarness()
harness.createSpotter.mockImplementationOnce(async () => {
await loaded
return harness.spotter
})
const catalog = () => pronunciations([{ characterId: 'alice', text: 'Alice', tokens: [['AE1', 'L', 'IH0', 'S']] }])
// A reactive catalog can emit an equal list again, for example after the card store hydrates.
const first = harness.detector.setPronunciations(catalog())
const second = harness.detector.setPronunciations(catalog())
finishLoad()
await Promise.all([first, second])
expect(harness.createSpotter).toHaveBeenCalledTimes(1)
expect(harness.detector.ready).toBe(true)
expect(harness.onPreparationChange).toHaveBeenLastCalledWith('ready')
})
it('does not load a model for pronunciations of another model', async () => {
const harness = createHarness()
@@ -152,7 +152,8 @@ export class WakeWordDetector {
const keywords = this.keywordsOf([...next.values()])
const update = this.updates.then(async () => {
// A newer list or a stop replaced this one while an earlier update ran.
if (generation !== this.generation || next !== this.pronunciations)
// An equal list replaces `pronunciations` without a new generation, so only the generation tells a newer list.
if (generation !== this.generation)
return
if (keywords.length === 0) {
@@ -184,7 +185,7 @@ export class WakeWordDetector {
// A keyword update starts a new stream in the Worker.
this.streamSampleRate = undefined
this.resetStream = false
if (generation !== this.generation || next !== this.pronunciations)
if (generation !== this.generation)
return
this.keywordsReady = true
this.options.onPreparationChange?.('ready')
-7
View File
@@ -37,13 +37,6 @@ const audioTestAPI = createAudioTestAPI<
}]
},
async execute({ plan, task, invokeHandler, runPreflight }) {
// NOTICE:
// CI wake word cases time out while preparation stays `preparing`. The root cause is not confirmed.
// Context: audio-web wake word cases time out in the CI Unit Test job.
// Remove this skip after the preparation failure has a verified fix on CI.
if (env.CI && plan.definition.pipelines?.includes('kws'))
task.context.skip(true, 'Wake word preparation times out on CI. The root cause is not confirmed.')
// Electron bundles the wake word model. Web downloads it, so the download must finish before the recording starts.
if (plan.definition.pipelines?.includes('kws') && inject('fakemicRuntime').kind === 'web')
await prepareWakeWordModel()
@@ -84,9 +84,10 @@ https://toughengineer.github.io/demo/slider-styler*/
--thumb-box-shadow: 0 0 0px #e6e6e6;
--thumb-border: none;
--thumb-border-radius: 999px;
--thumb-background: oklch(80% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
--thumb-background-hover: oklch(90% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
--thumb-background-active: oklch(70% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
/* The defaults keep @proj-airi/ui buildable where the chromatic preset is not configured (e.g. ui-loading-screens). */
--thumb-background: theme('colors.primary.300', oklch(85% 0.09 var(--chromatic-hue)));
--thumb-background-hover: theme('colors.primary.200', oklch(90% 0.07 var(--chromatic-hue)));
--thumb-background-active: theme('colors.primary.400', oklch(77% 0.12 var(--chromatic-hue)));
--track-height: calc(var(--height) - var(--track-value-padding) * 2);
--track-box-shadow: none;
@@ -103,9 +104,9 @@ https://toughengineer.github.io/demo/slider-styler*/
}
.dark .form_input-range {
--thumb-background: oklch(70% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
--thumb-background-hover: oklch(90% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
--thumb-background-active: oklch(80% var(--chromatic-chroma-200) calc(var(--chromatic-hue) + 0));
--thumb-background: theme('colors.primary.400', oklch(77% 0.12 var(--chromatic-hue)));
--thumb-background-hover: theme('colors.primary.200', oklch(90% 0.07 var(--chromatic-hue)));
--thumb-background-active: theme('colors.primary.300', oklch(85% 0.09 var(--chromatic-hue)));
--track-border: solid 2px rgb(44, 44, 44);
--track-background: rgba(44, 44, 44, 0.7);
-20
View File
@@ -1,25 +1,5 @@
/*
* Chroma variables in the shape of @proj-airi/unocss-preset-chromatic 1.x (`0.18 + 0.04 · cos(hue)`, scaled per shade).
*
* NOTICE: since chromatic 2 the preset no longer emits these (its default `v2` model uses per-colour variables), so
* this file is now their only source. They back the hand-made tints that pick their own lightness (stage transition
* colours, nprogress, default backgrounds, range thumbs, the motion workbench). Keep them until those sites move to
* chromatic utilities (`~` / `*` modifiers) or to `chromaticColorFrom()`.
*/
:root {
--chromatic-hue: 220.44;
--chromatic-chroma: calc(0.18 + (cos(var(--chromatic-hue) * 3.14159265 / 180) * 0.04));
--chromatic-chroma-50: calc(var(--chromatic-chroma) * 0.3);
--chromatic-chroma-100: calc(var(--chromatic-chroma) * 0.5);
--chromatic-chroma-200: calc(var(--chromatic-chroma) * 0.6);
--chromatic-chroma-300: calc(var(--chromatic-chroma) * 0.75);
--chromatic-chroma-400: calc(var(--chromatic-chroma) * 0.85);
--chromatic-chroma-500: var(--chromatic-chroma);
--chromatic-chroma-600: calc(var(--chromatic-chroma) * 1.15);
--chromatic-chroma-700: calc(var(--chromatic-chroma) * 1.1);
--chromatic-chroma-800: calc(var(--chromatic-chroma) * 0.85);
--chromatic-chroma-900: calc(var(--chromatic-chroma) * 0.7);
--chromatic-chroma-950: calc(var(--chromatic-chroma) * 0.5);
}
.is-interacting {
+14 -14
View File
@@ -403,8 +403,8 @@ catalogs:
specifier: ^1000.1.5
version: 1000.1.5
'@proj-airi/chromatic':
specifier: ^2.0.0
version: 2.0.0
specifier: ^2.0.1
version: 2.0.1
'@proj-airi/drizzle-duckdb-wasm':
specifier: ^0.6.0
version: 0.6.0
@@ -415,8 +415,8 @@ catalogs:
specifier: ^1.0.19
version: 1.0.19
'@proj-airi/unocss-preset-chromatic':
specifier: ^2.0.0
version: 2.0.0
specifier: ^2.0.1
version: 2.0.1
'@proj-airi/unplugin-fetch':
specifier: ^0.2.3
version: 0.2.3
@@ -1307,7 +1307,7 @@ importers:
version: 0.1.0-beta.19(@antfu/eslint-config@8.2.0(@typescript-eslint/typescript-estree@8.67.0(supports-color@10.2.2)(typescript@6.0.3))(@typescript-eslint/utils@8.67.0(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@unocss/eslint-plugin@66.7.5(@typescript-eslint/types@8.67.0)(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint-plugin-format@2.0.1(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.79.0)(supports-color@10.2.2)(typescript@6.0.3)(vitest@4.1.11))(eslint-plugin-oxlint@1.79.0(oxlint@1.79.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
'@proj-airi/unocss-preset-chromatic':
specifier: 'catalog:'
version: 2.0.0
version: 2.0.1
'@types/node':
specifier: 'catalog:'
version: 26.2.0
@@ -5097,7 +5097,7 @@ importers:
version: link:../ccc
'@proj-airi/chromatic':
specifier: 'catalog:'
version: 2.0.0
version: 2.0.1
'@proj-airi/cloud-client':
specifier: workspace:^
version: link:../cloud-client
@@ -6205,7 +6205,7 @@ importers:
version: link:../../packages/stage-ui
'@proj-airi/unocss-preset-chromatic':
specifier: 'catalog:'
version: 2.0.0
version: 2.0.1
'@unocss/reset':
specifier: 'catalog:'
version: 66.7.5
@@ -11555,8 +11555,8 @@ packages:
prisma:
optional: true
'@proj-airi/chromatic@2.0.0':
resolution: {integrity: sha512-QAu6aqg/ZacEADRvnevakt5ksUneqt748IkRXn6sj0kQqU7EruFfmbv2O52lIK/ysiw5MNFs4qUuyYjg2z7GwQ==}
'@proj-airi/chromatic@2.0.1':
resolution: {integrity: sha512-CTZMkkqYEpUdWM1eFqQcJE2FdFKWY7NyrNYfzD4908GYcSVQaJAliVTEGVajxUfOJP0aD13BSyN7vo1PmsuW/g==}
'@proj-airi/drizzle-duckdb-wasm@0.6.0':
resolution: {integrity: sha512-iW7u6eGg7uHXQ/9MsyVfgqcoa07wcdaOp5v63crJnQx8XXdF2uus3yVtx7NXzHXWF6TF9IeybMNBM4CHLhjDGQ==}
@@ -11582,8 +11582,8 @@ packages:
'@proj-airi/lobe-icons@1.0.19':
resolution: {integrity: sha512-hBB/MUYqzckzMXYDLZ7/4HRuldqEvLKhMzTcVPLUOdXR0sn9y4vk+k14tynwgeePCrUKSUTP/bnPdETqvWJzqw==}
'@proj-airi/unocss-preset-chromatic@2.0.0':
resolution: {integrity: sha512-Msnsu3JFi0kxFnsf45LT2ZL5eT1bd9/inibv9/r826lScnZvQ1i8GD2RMVJUwoM9IiHqnEUVZn3ycKhxRcEqzQ==}
'@proj-airi/unocss-preset-chromatic@2.0.1':
resolution: {integrity: sha512-mXzCMxkO5113nhlG4dILCnh3WZgWBn45TEG5tQJzcOSpgqVmJNM9nLbLzB+JDDZ2sMXAo77WrRqSbvWoOlCWbQ==}
'@proj-airi/unplugin-fetch@0.2.3':
resolution: {integrity: sha512-90I6CfbC7xMSm9/0gl+DJvHmg2Hw9E/7L5rtfXpqMmrRQqd/jhfD1Sb5LjywqTZ3rlFfnb39d051yFGeYigi9A==}
@@ -26252,7 +26252,7 @@ snapshots:
'@prisma/client@5.22.0': {}
'@proj-airi/chromatic@2.0.0':
'@proj-airi/chromatic@2.0.1':
dependencies:
culori: 4.0.2
@@ -26290,9 +26290,9 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@proj-airi/unocss-preset-chromatic@2.0.0':
'@proj-airi/unocss-preset-chromatic@2.0.1':
dependencies:
'@proj-airi/chromatic': 2.0.0
'@proj-airi/chromatic': 2.0.1
'@proj-airi/unplugin-fetch@0.2.3(vite@8.3.4(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@10.2.2))(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.1))':
dependencies:
+2 -2
View File
@@ -196,11 +196,11 @@ catalog:
'@pixiv/three-vrm-animation': ^3.5.5
'@pixiv/three-vrm-core': ^3.5.5
'@pnpm/find-workspace-dir': ^1000.1.5
'@proj-airi/chromatic': ^2.0.0
'@proj-airi/chromatic': ^2.0.1
'@proj-airi/drizzle-duckdb-wasm': ^0.6.0
'@proj-airi/iconify-meteocons': ^0.1.5
'@proj-airi/lobe-icons': ^1.0.19
'@proj-airi/unocss-preset-chromatic': ^2.0.0
'@proj-airi/unocss-preset-chromatic': ^2.0.1
'@proj-airi/unplugin-fetch': ^0.2.3
'@proj-airi/unplugin-live2d-sdk': ^0.1.7
'@revenuecat/purchases-js': ^1.15.0