feat(dashboard): add background customization system

Add image/video background support across 5 layout layers (page, sidebar,
header, card, dialog) with per-layer effect controls and custom CSS injection.
- IndexedDB asset store for blob persistence (idb)
- AssetStoreProvider for blob URL lifecycle management
- BackgroundLayer component with CSS effects and prefers-reduced-motion support
- useBackground hook with inherit logic
- BackgroundUploader with local file and remote URL support
- BackgroundEffectsControls and ComponentCSSEditor UI components
- Background settings integrated into AppearanceTab in settings.tsx
- Layout, Card, and Dialog integration via non-breaking wrapper components
This commit is contained in:
DrSmoothl
2026-02-23 18:08:01 +08:00
parent 698b8355a4
commit 1fec4c3b9a
16 changed files with 1322 additions and 17 deletions

View File

@@ -0,0 +1,64 @@
import { createContext, useContext, useEffect, useMemo, useRef } from 'react'
import type { ReactNode } from 'react'
import { getAsset } from '@/lib/asset-store'
type AssetStoreContextType = {
getAssetUrl: (assetId: string) => Promise<string | undefined>
}
const AssetStoreContext = createContext<AssetStoreContextType | null>(null)
type AssetStoreProviderProps = {
children: ReactNode
}
export function AssetStoreProvider({ children }: AssetStoreProviderProps) {
const urlCache = useRef<Map<string, string>>(new Map())
const getAssetUrl = async (assetId: string): Promise<string | undefined> => {
// Check cache first
const cached = urlCache.current.get(assetId)
if (cached) {
return cached
}
// Fetch from IndexedDB
const record = await getAsset(assetId)
if (!record) {
return undefined
}
// Create blob URL and cache it
const url = URL.createObjectURL(record.blob)
urlCache.current.set(assetId, url)
return url
}
const value = useMemo(
() => ({
getAssetUrl,
}),
[],
)
// Cleanup: revoke all blob URLs on unmount
useEffect(() => {
return () => {
urlCache.current.forEach((url) => {
URL.revokeObjectURL(url)
})
urlCache.current.clear()
}
}, [])
return <AssetStoreContext.Provider value={value}>{children}</AssetStoreContext.Provider>
}
export function useAssetStore() {
const context = useContext(AssetStoreContext)
if (!context) {
throw new Error('useAssetStore must be used within AssetStoreProvider')
}
return context
}