diff --git a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/index.vue b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/index.vue
index b1d122fc6..4197274b2 100644
--- a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/index.vue
+++ b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/index.vue
@@ -1,87 +1,85 @@
+
+
+
+ Click Select Model to import different formats of
+ models into catalog, currently, .zip (Live2D) and .vrm (VRM) are supported.
+
+
+ Neuro-sama uses 2D model driven by Live2D Inc. developed framework.
+ While Grok Ani (first female character announced in Grok Companion)
+ uses 3D model that is driven by VRM / MMD open formats.
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/packages/stage-ui/src/components/Scenes/Live2D.vue b/packages/stage-ui/src/components/Scenes/Live2D.vue
index e8b77c744..df9bb7392 100644
--- a/packages/stage-ui/src/components/Scenes/Live2D.vue
+++ b/packages/stage-ui/src/components/Scenes/Live2D.vue
@@ -7,7 +7,6 @@ import '../../utils/live2d-zip-loader'
withDefaults(defineProps<{
modelSrc?: string
- modelFile?: File | null
paused?: boolean
mouthOpenSize?: number
@@ -35,7 +34,6 @@ withDefaults(defineProps<{
>
props.modelSrc)
-const modelFileRef = toRef(() => props.modelFile)
-const modelFileSrc = useObjectUrl(modelFileRef)
-const modelSrcNormalized = computed(() => {
- if (modelFileSrc.value)
- return modelFileSrc.value
-
- if (modelSrcRef.value)
- return modelSrcRef.value
-
- return ''
-})
const modelLoading = ref(false)
@@ -127,7 +113,6 @@ function setScaleAndPosition() {
}
const {
- modelFile,
currentMotion,
availableMotions,
motionMap,
@@ -155,7 +140,7 @@ async function loadModel() {
model.value.destroy()
model.value = undefined
}
- if (!modelSrcNormalized.value) {
+ if (!modelSrcRef.value) {
console.warn('No Live2D model source provided.')
modelLoading.value = false
return
@@ -163,11 +148,13 @@ async function loadModel() {
try {
const modelInstance = new Live2DModel()
- if (modelFile.value) {
- await Live2DFactory.setupLive2DModel(modelInstance, [modelFile.value], { autoInteract: false })
+ if (modelSrcRef.value.startsWith('blob:')) {
+ const res = await fetch(modelSrcRef.value)
+ const blob = await res.blob()
+ await Live2DFactory.setupLive2DModel(modelInstance, [blob], { autoInteract: false })
}
else {
- await Live2DFactory.setupLive2DModel(modelInstance, modelSrcNormalized.value, { autoInteract: false })
+ await Live2DFactory.setupLive2DModel(modelInstance, modelSrcRef.value, { autoInteract: false })
}
availableMotions.value.forEach((motion) => {
@@ -268,11 +255,6 @@ async function loadModel() {
localCurrentMotion.value = { group, index }
})
- // save to indexdb
- if (modelFile.value) {
- await localforage.setItem('live2dModel', modelFile.value)
- }
-
emits('modelLoaded')
}
finally {
@@ -299,7 +281,7 @@ function updateDropShadowFilter() {
}
watch([() => props.width, () => props.height], () => handleResize())
-watch(modelSrcNormalized, async () => await loadModel(), { immediate: true })
+watch(modelSrcRef, async () => await loadModel(), { immediate: true })
watch(dark, updateDropShadowFilter, { immediate: true })
watch([model, themeColorsHue], updateDropShadowFilter)
watch(offset, setScaleAndPosition)
diff --git a/packages/stage-ui/src/components/Scenes/Stage.vue b/packages/stage-ui/src/components/Scenes/Stage.vue
index 5fc2511e7..9d055ba4a 100644
--- a/packages/stage-ui/src/components/Scenes/Stage.vue
+++ b/packages/stage-ui/src/components/Scenes/Stage.vue
@@ -27,7 +27,6 @@ import { useLive2d } from '../../stores/live2d'
import { useSpeechStore } from '../../stores/modules/speech'
import { useProvidersStore } from '../../stores/providers'
import { useSettings } from '../../stores/settings'
-import { useVRM } from '../../stores/vrm'
withDefaults(defineProps<{
paused?: boolean
@@ -42,13 +41,11 @@ const db = ref()
const vrmViewerRef = ref<{ setExpression: (expression: string) => void }>()
-const { stageView, stageViewControlsEnabled, live2dDisableFocus } = storeToRefs(useSettings())
+const { stageView, stageViewControlsEnabled, live2dDisableFocus, stageModelSelectedUrl } = storeToRefs(useSettings())
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
const { audioContext, calculateVolume } = useAudioContext()
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd, onAssistantResponseEnd } = useChatStore()
const providersStore = useProvidersStore()
-const { modelFile, modelUrl } = storeToRefs(useLive2d())
-const { modelFile: vrmModelFile, modelUrl: vrmModelUrl } = storeToRefs(useVRM())
const audioAnalyser = ref()
const nowSpeaking = ref(false)
@@ -140,14 +137,14 @@ const { currentMotion } = storeToRefs(useLive2d())
const emotionsQueue = useQueue({
handlers: [
async (ctx) => {
- if (stageView.value === '3d') {
+ if (stageView.value === 'vrm') {
const value = EMOTION_VRMExpressionName_value[ctx.data]
if (!value)
return
await vrmViewerRef.value!.setExpression(value)
}
- else if (stageView.value === '2d') {
+ else if (stageView.value === 'live2d') {
currentMotion.value = { group: EMOTION_EmotionMotionName_value[ctx.data] }
}
},
@@ -232,10 +229,9 @@ onMounted(async () => {
{
:disable-focus-at="live2dDisableFocus"
/>
()
const emit = defineEmits<{
@@ -292,7 +291,6 @@ defineExpose({
emit('loadModelProgress', val)"
diff --git a/packages/stage-ui/src/components/Scenes/VRM/Model.vue b/packages/stage-ui/src/components/Scenes/VRM/Model.vue
index ab54f296a..4eddae58b 100644
--- a/packages/stage-ui/src/components/Scenes/VRM/Model.vue
+++ b/packages/stage-ui/src/components/Scenes/VRM/Model.vue
@@ -4,7 +4,7 @@ import type { AnimationClip, Group } from 'three'
import { VRMUtils } from '@pixiv/three-vrm'
import { useLoop, useTresContext } from '@tresjs/core'
-import { until, useObjectUrl } from '@vueuse/core'
+import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { AnimationMixer, MathUtils, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, Quaternion, Vector3, VectorKeyframeTrack } from 'three'
import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
@@ -16,7 +16,6 @@ import { useVRM } from '../../../stores/vrm'
const props = defineProps<{
modelSrc?: string
- modelFile?: File | null
idleAnimation: string
loadAnimations?: string[]
@@ -31,20 +30,9 @@ const emit = defineEmits<{
let disposeBeforeRenderLoop: (() => void | undefined)
-const modelCreating = ref(false)
+const modelLoading = ref(false)
const modelLoaded = ref(false)
const modelSrcRef = toRef(() => props.modelSrc)
-const modelFileRef = toRef(() => props.modelFile)
-const modelFileSrc = useObjectUrl(modelFileRef)
-const modelSrcNormalized = computed(() => {
- if (modelFileSrc.value)
- return modelFileSrc.value
-
- if (modelSrcRef.value)
- return modelSrcRef.value
-
- return ''
-})
const vrm = ref()
const vrmAnimationMixer = ref()
@@ -68,8 +56,8 @@ const vrmGroup = ref()
const idleEyeSaccades = useIdleEyeSaccades()
async function loadModel() {
- await until(modelCreating).not.toBeTruthy()
- modelCreating.value = true
+ await until(modelLoading).not.toBeTruthy()
+ modelLoading.value = true
modelLoaded.value = false
try {
@@ -80,9 +68,12 @@ async function loadModel() {
if (vrm.value) {
componentCleanUp()
}
+ if (!modelSrcRef.value) {
+ return
+ }
try {
- const _vrmInfo = await loadVrm(modelSrcNormalized.value, {
+ const _vrmInfo = await loadVrm(modelSrcRef.value, {
scene: scene.value,
lookAt: true,
onProgress: progress => emit('loadModelProgress', Number((100 * progress.loaded / progress.total).toFixed(2))),
@@ -255,7 +246,7 @@ async function loadModel() {
console.error(err)
}
finally {
- modelCreating.value = false
+ modelLoading.value = false
}
}
@@ -276,7 +267,7 @@ watch(modelRotationY, (newRotationY) => {
})
// watch if the model needs to be reloaded
-watch(modelSrcNormalized, (newSrc, oldSrc) => {
+watch(modelSrcRef, (newSrc, oldSrc) => {
if (newSrc !== oldSrc) {
loadModel()
}
diff --git a/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.story.vue b/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.story.vue
new file mode 100644
index 000000000..ccb563e3e
--- /dev/null
+++ b/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.story.vue
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.vue b/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.vue
new file mode 100644
index 000000000..d9e10b0ab
--- /dev/null
+++ b/packages/stage-ui/src/components/Widgets/PoppinText/PoppinMarkdown.web.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/stores/display-models.ts b/packages/stage-ui/src/stores/display-models.ts
index 205f03302..95900950a 100644
--- a/packages/stage-ui/src/stores/display-models.ts
+++ b/packages/stage-ui/src/stores/display-models.ts
@@ -1,5 +1,111 @@
+import localforage from 'localforage'
+
+import { until } from '@vueuse/core'
+import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
+import { ref } from 'vue'
-export const useDisplayModelsStore = defineStore('display-models', {
+export enum DisplayModelFormat {
+ Live2dZip = 'live2d-zip',
+ Live2dDirectory = 'live2d-directory',
+ VRM = 'vrm',
+ PMXZip = 'pmx-zip',
+ PMXDirectory = 'pmx-directory',
+ PMD = 'pmd',
+}
+export type DisplayModel
+ = | DisplayModelFile
+ | DisplayModelURL
+
+export interface DisplayModelFile {
+ id: string
+ format: DisplayModelFormat
+ type: 'file'
+ file: File
+ name: string
+ previewImage?: string
+}
+
+export interface DisplayModelURL {
+ id: string
+ format: DisplayModelFormat
+ type: 'url'
+ url: string
+ name: string
+ previewImage?: string
+}
+
+const displayModelsPresets: DisplayModel[] = [
+ { id: 'preset-live2d-1', format: DisplayModelFormat.Live2dZip, type: 'url', url: '/assets/live2d/models/hiyori_pro_zh.zip', name: 'Hiyori (Pro)', previewImage: '/assets/live2d/models/hiyori_free_zh/avatar.png' },
+ { id: 'preset-live2d-2', format: DisplayModelFormat.Live2dZip, type: 'url', url: '/assets/live2d/models/hiyori_free_zh.zip', name: 'Hiyori (Free)', previewImage: '/assets/live2d/models/hiyori_free_zh/avatar.png' },
+ { id: 'preset-live2d-3', format: DisplayModelFormat.Live2dZip, type: 'url', url: '/assets/live2d/models/hiyori_free_zh.zip', name: 'Hiyori (Free)' },
+ { id: 'preset-live2d-4', format: DisplayModelFormat.Live2dZip, type: 'url', url: '/assets/live2d/models/hiyori_free_zh.zip', name: 'Hiyori (Free)', previewImage: '/assets/live2d/models/hiyori_free_zh/avatar.png' },
+]
+
+export const useDisplayModelsStore = defineStore('display-models', () => {
+ const displayModels = ref([])
+
+ const displayModelsFromIndexedDBLoading = ref(false)
+
+ async function loadDisplayModelsFromIndexedDB() {
+ await until(displayModelsFromIndexedDBLoading).toBe(false)
+
+ displayModelsFromIndexedDBLoading.value = true
+ const models = [...displayModelsPresets]
+
+ try {
+ await localforage.iterate<{ format: DisplayModelFormat, file: File }, void>((val, key) => {
+ if (key.startsWith('display-model-')) {
+ models.push({ id: key, format: val.format, type: 'file', file: val.file, name: val.file.name })
+ }
+ })
+ }
+ catch (err) {
+ console.error(err)
+ }
+
+ displayModels.value = models
+ displayModelsFromIndexedDBLoading.value = false
+ }
+
+ async function getDisplayModel(id: string) {
+ await until(displayModelsFromIndexedDBLoading).toBe(false)
+ return displayModels.value.find(model => model.id === id)
+ }
+
+ async function addDisplayModel(format: DisplayModelFormat, file: File) {
+ await until(displayModelsFromIndexedDBLoading).toBe(false)
+ const newDisplayModel: DisplayModelFile = { id: `display-model-${nanoid()}`, format, type: 'file', file, name: file.name }
+ displayModels.value.push(newDisplayModel)
+
+ localforage.setItem(newDisplayModel.id, newDisplayModel)
+ .catch(err => console.error(err))
+ }
+
+ async function renameDisplayModel(id: string, name: string) {
+ await until(displayModelsFromIndexedDBLoading).toBe(false)
+ const displayModel = await localforage.getItem(id)
+ if (!displayModel)
+ return
+
+ displayModel.name = name
+ }
+
+ async function removeDisplayModel(id: string) {
+ await until(displayModelsFromIndexedDBLoading).toBe(false)
+ await localforage.removeItem(id)
+ displayModels.value = displayModels.value.filter(model => model.id !== id)
+ }
+
+ return {
+ displayModels,
+ displayModelsFromIndexedDBLoading,
+
+ loadDisplayModelsFromIndexedDB,
+ getDisplayModel,
+ addDisplayModel,
+ renameDisplayModel,
+ removeDisplayModel,
+ }
})
diff --git a/packages/stage-ui/src/stores/live2d.ts b/packages/stage-ui/src/stores/live2d.ts
index 0e8db8f46..b9bd609a0 100644
--- a/packages/stage-ui/src/stores/live2d.ts
+++ b/packages/stage-ui/src/stores/live2d.ts
@@ -1,8 +1,6 @@
-import localforage from 'localforage'
-
import { useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
-import { computed, onMounted, ref, watch } from 'vue'
+import { computed, ref, watch } from 'vue'
type BroadcastChannelEvents
= | BroadcastChannelEventShouldUpdateView
@@ -30,37 +28,6 @@ export const useLive2d = defineStore('live2d', () => {
}
})
- const indexedDbModelFile = ref(null)
-
- onMounted(async () => await loadModelFileFromIndexedDb())
- onShouldUpdateView(async () => await loadModelFileFromIndexedDb())
-
- async function loadModelFileFromIndexedDb() {
- const file = await localforage.getItem('assets-models-live2d')
- if (file) {
- indexedDbModelFile.value = file
- }
- }
-
- const modelFile = computed({
- get: () => {
- return indexedDbModelFile.value
- },
- set: (file: File | null) => {
- if (file) {
- localforage.setItem('assets-models-live2d', file)
- }
- else {
- localforage.removeItem('assets-models-live2d')
- }
-
- indexedDbModelFile.value = file
- },
- })
-
- const defaultModelUrl = '/assets/live2d/models/hiyori_pro_zh.zip'
- const modelUrl = useLocalStorage('settings/live2d/model-src', defaultModelUrl)
-
const position = useLocalStorage('settings/live2d/position', { x: 0, y: 0 }) // position is relative to the center of the screen, units are %
const positionInPercentageString = computed(() => ({
x: `${position.value.x}%`,
@@ -72,8 +39,6 @@ export const useLive2d = defineStore('live2d', () => {
const scale = useLocalStorage('settings/live2d/scale', 1)
return {
- modelFile,
- modelUrl,
position,
positionInPercentageString,
currentMotion,
diff --git a/packages/stage-ui/src/stores/settings.ts b/packages/stage-ui/src/stores/settings.ts
index 123110a73..277062643 100644
--- a/packages/stage-ui/src/stores/settings.ts
+++ b/packages/stage-ui/src/stores/settings.ts
@@ -1,11 +1,14 @@
+import type { DisplayModel } from './display-models'
+
import messages from '@proj-airi/i18n/locales'
-import { useLocalStorage } from '@vueuse/core'
+import { useEventListener, useLocalStorage } from '@vueuse/core'
import { converter } from 'culori'
import { defineStore } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useAudioDevice } from './audio'
+import { DisplayModelFormat, useDisplayModelsStore } from './display-models'
const languageRemap: Record = {
'zh-CN': 'zh-Hans',
@@ -28,12 +31,69 @@ const convert = converter('oklch')
const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME_COLORS_HUE
export const useSettings = defineStore('settings', () => {
- const selectedAudioDevice = ref()
+ const displayModelsStore = useDisplayModelsStore()
const language = useLocalStorage('settings/language', '')
- const stageView = useLocalStorage('settings/stage/view/model-renderer', '2d')
+ const stageModelSelected = useLocalStorage('settings/stage/model', undefined)
+ const stageModelSelectedDisplayModel = ref()
+ const stageModelSelectedUrl = ref()
+ const stageModelRenderer = ref<'live2d' | 'vrm' | 'disabled'>()
+
+ async function updateStageModel() {
+ if (!stageModelSelected.value) {
+ stageModelSelectedUrl.value = undefined
+ stageModelSelectedDisplayModel.value = undefined
+ stageModelRenderer.value = 'disabled'
+ return
+ }
+
+ const model = await displayModelsStore.getDisplayModel(stageModelSelected.value)
+ if (!model) {
+ stageModelSelectedUrl.value = undefined
+ stageModelSelectedDisplayModel.value = undefined
+ stageModelRenderer.value = 'disabled'
+ return
+ }
+
+ if (model.type === 'file') {
+ if (stageModelSelectedUrl.value) {
+ URL.revokeObjectURL(stageModelSelectedUrl.value)
+ }
+
+ stageModelSelectedUrl.value = URL.createObjectURL(model.file)
+ }
+ else {
+ stageModelSelectedUrl.value = model.url
+ }
+
+ switch (model.format) {
+ case DisplayModelFormat.Live2dZip:
+ stageModelRenderer.value = 'live2d'
+ break
+ case DisplayModelFormat.VRM:
+ stageModelRenderer.value = 'vrm'
+ break
+ default:
+ stageModelRenderer.value = 'disabled'
+ break
+ }
+
+ stageModelSelectedDisplayModel.value = model
+ }
+
+ async function initializeStageModel() {
+ await updateStageModel()
+ }
+
+ useEventListener('unload', () => {
+ if (stageModelSelectedUrl.value) {
+ URL.revokeObjectURL(stageModelSelectedUrl.value)
+ }
+ })
+
const stageViewControlsEnabled = ref(false)
+
const live2dDisableFocus = useLocalStorage('settings/live2d/disable-focus', false)
const disableTransitions = useLocalStorage('settings/disable-transitions', true)
@@ -91,26 +151,30 @@ export const useSettings = defineStore('settings', () => {
return hueDifference < 0.01 || hueDifference > 359.99
}
- onMounted(() => {
- language.value = getLanguage()
- })
+ onMounted(() => language.value = getLanguage())
return {
disableTransitions,
usePageSpecificTransitions,
language,
- stageView,
- live2dDisableFocus,
+
+ stageModelRenderer,
+ stageModelSelected,
+ stageModelSelectedUrl,
+ stageModelSelectedDisplayModel,
stageViewControlsEnabled,
+
+ live2dDisableFocus,
themeColorsHue,
themeColorsHueDynamic,
- selectedAudioDevice,
allowVisibleOnAllWorkspaces,
setThemeColorsHue,
applyPrimaryColorFrom,
isColorSelectedForPrimary,
+ initializeStageModel,
+ updateStageModel,
}
})
diff --git a/packages/stage-ui/src/stores/vrm.ts b/packages/stage-ui/src/stores/vrm.ts
index de68ffb25..d66f4e369 100644
--- a/packages/stage-ui/src/stores/vrm.ts
+++ b/packages/stage-ui/src/stores/vrm.ts
@@ -1,8 +1,6 @@
-import localforage from 'localforage'
-
import { useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
-import { computed, onMounted, ref, watch } from 'vue'
+import { ref, watch } from 'vue'
import defaultSkyBoxSrc from '../components/Scenes/Tres/assets/sky_linekotsi_23_HDRI.hdr?url'
@@ -32,37 +30,6 @@ export const useVRM = defineStore('vrm', () => {
}
})
- const indexedDbModelFile = ref(null)
-
- async function loadModelFileFromIndexedDb() {
- const file = await localforage.getItem('assets-models-vrm')
- if (file) {
- indexedDbModelFile.value = file
- }
- }
-
- onMounted(async () => loadModelFileFromIndexedDb())
- // onShouldUpdateView(() => loadModelFileFromIndexedDb())
-
- const modelFile = computed({
- get: () => {
- return indexedDbModelFile.value
- },
- set: (file: File | null) => {
- if (file) {
- localforage.setItem('assets-models-vrm', file)
- }
- else {
- localforage.removeItem('assets-models-vrm')
- }
-
- indexedDbModelFile.value = file
- },
- })
-
- const defaultModelUrl = '/assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm'
- const modelUrl = useLocalStorage('settings/vrm/modelURL', defaultModelUrl)
-
const scale = useLocalStorage('settings/vrm/cameraScale', 1)
const modelSize = useLocalStorage('settings/vrm/modelSize', { x: 0, y: 0, z: 0 })
const modelOrigin = useLocalStorage('settings/vrm/modelOrigin', { x: 0, y: 0, z: 0 })
@@ -117,9 +84,6 @@ export const useVRM = defineStore('vrm', () => {
const skyBoxSrc = useLocalStorage('settings/vrm/skyBoxUrl', defaultSkyBoxSrc)
return {
- defaultModelUrl,
- modelFile,
- modelUrl,
modelSize,
scale,
diff --git a/packages/ui/src/components/Form/Input/BasicInputFile.vue b/packages/ui/src/components/Form/Input/BasicInputFile.vue
index 8688cf170..20a0c903b 100644
--- a/packages/ui/src/components/Form/Input/BasicInputFile.vue
+++ b/packages/ui/src/components/Form/Input/BasicInputFile.vue
@@ -17,13 +17,20 @@ const isDragging = ref(false)
const isDraggingDebounced = useDebounce(isDragging, 150)
function handleFileChange(e: Event) {
- const input = e.target as HTMLInputElement
+ files.value = []
- if (input.files && input.files.length > 0) {
- firstFile.value = input.files[0]
+ const input = e.target as HTMLInputElement
+ if (!input.files)
+ return
+
+ for (let i = 0; i < input.files?.length; i++) {
+ files.value.push(input.files[i])
+ }
+
+ if (files.value && files.value.length > 0) {
+ firstFile.value = files.value[0]
}
- files.value = Array.from(input.files || [])
isDragging.value = false
}
@@ -44,7 +51,7 @@ function handleFileChange(e: Event) {
type="file"
:accept="accept"
:multiple="multiple"
- class="absolute inset-0 cursor-pointer appearance-none opacity-0"
+ class="absolute inset-0 h-0 w-0 cursor-pointer appearance-none opacity-0"
@change="handleFileChange"
>
diff --git a/uno.config.ts b/uno.config.ts
index e2361786a..27456ca3d 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -203,22 +203,40 @@ export function sharedUnoConfig() {
*/
animation: {
keyframes: {
+ overlayShow: '{from{opacity:0;}to{opacity:1;}}',
+ overlayHide: '{from{opacity:1;}to{opacity:0;}}',
+ contentShow: '{from:{opacity:0;transform:translate(-50%,-48%) scale(0.96);}to:{opacity:1;transform:translate(-50%,-50%) scale(1);}}',
+ contentHide: '{from:{opacity:1;transform:translate(-50%,-50%) scale(1);}to:{opacity:0;transform:translate(-50%,-48%) scale(0.96);}}',
slideUpAndFade: '{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}',
slideRightAndFade: '{from{opacity:0;transform:translateX(-2px)}to{opacity:1;transform:translateX(0)}}',
slideDownAndFade: '{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}',
slideLeftAndFade: '{from{opacity:0;transform:translateX(2px)}to{opacity:1;transform:translateX(0)}}',
+ fadeIn: '{from{opacity:0;}to{opacity:1;}}',
+ fadeOut: '{from{opacity:1;}to{opacity:0;}}',
},
durations: {
+ overlayShow: '300ms',
+ overlayHide: '300ms',
+ contentShow: '150ms',
+ contentHide: '150ms',
slideUpAndFade: '400ms',
slideRightAndFade: '400ms',
slideDownAndFade: '400ms',
slideLeftAndFade: '400ms',
+ fadeIn: '200ms',
+ fadeOut: '200ms',
},
timingFns: {
+ overlayShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
+ overlayHide: 'cubic-bezier(0.16, 1, 0.3, 1)',
+ contentShow: 'cubic-bezier(0.16, 1, 0.3, 1)',
+ contentHide: 'cubic-bezier(0.16, 1, 0.3, 1)',
slideUpAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
slideRightAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
slideDownAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
+ fadeIn: 'ease-in-out',
+ fadeOut: 'ease-in-out',
},
},
},