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:
64
dashboard/src/components/asset-provider.tsx
Normal file
64
dashboard/src/components/asset-provider.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user