diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 2e9b0655fba9..9947fdbbaeab 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -39,6 +39,7 @@ import 'react-toastify/dist/ReactToastify.css'; import { useConfig } from './components/ConfigContext'; import { ModelAndProviderProvider } from './components/ModelAndProviderContext'; import { ThemeProvider } from './contexts/ThemeContext'; +import { GlyphProvider } from './contexts/GlyphContext'; import PermissionSettingsView from './components/settings/permission/PermissionSetting'; import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView'; @@ -702,13 +703,15 @@ export function AppInner() { export default function App() { return ( - - - - - - - + + + + + + + + + ); } diff --git a/ui/desktop/src/components/FlyingBird.tsx b/ui/desktop/src/components/FlyingBird.tsx index d4c8c55ecf5e..18a3d71437fa 100644 --- a/ui/desktop/src/components/FlyingBird.tsx +++ b/ui/desktop/src/components/FlyingBird.tsx @@ -1,29 +1,37 @@ import { useState, useEffect } from 'react'; -import { Bird1, Bird2, Bird3, Bird4, Bird5, Bird6 } from './icons'; +import { useGlyphPack } from '../contexts/GlyphContext'; interface FlyingBirdProps { className?: string; cycleInterval?: number; // milliseconds between bird frame changes } -const birdFrames = [Bird1, Bird2, Bird3, Bird4, Bird5, Bird6]; - export default function FlyingBird({ className = '', cycleInterval = 150 }: FlyingBirdProps) { + const { pack } = useGlyphPack(); + const frames = pack.AnimationFrames; const [currentFrameIndex, setCurrentFrameIndex] = useState(0); useEffect(() => { + if (!frames) return; const interval = setInterval(() => { - setCurrentFrameIndex((prevIndex) => (prevIndex + 1) % birdFrames.length); + setCurrentFrameIndex((prevIndex) => (prevIndex + 1) % frames.length); }, cycleInterval); - return () => clearInterval(interval); - }, [cycleInterval]); + }, [cycleInterval, frames]); - const CurrentFrame = birdFrames[currentFrameIndex]; + if (frames) { + const CurrentFrame = frames[currentFrameIndex]; + return ( +
+ +
+ ); + } + // Fallback: static glyph for packs without frame animation return (
- +
); } diff --git a/ui/desktop/src/components/GooseLogo.tsx b/ui/desktop/src/components/GooseLogo.tsx index b82a0384986e..4a3515ef50bb 100644 --- a/ui/desktop/src/components/GooseLogo.tsx +++ b/ui/desktop/src/components/GooseLogo.tsx @@ -1,5 +1,6 @@ -import { Goose, Rain } from './icons/Goose'; +import { Rain } from './icons/Goose'; import { cn } from '../utils'; +import { useGlyphPack } from '../contexts/GlyphContext'; interface GooseLogoProps { className?: string; @@ -12,6 +13,8 @@ export default function GooseLogo({ size = 'default', hover = true, }: GooseLogoProps) { + const { pack } = useGlyphPack(); + const sizes = { default: { frame: 'w-16 h-16', @@ -43,7 +46,7 @@ export default function GooseLogo({ hover && 'opacity-0 group-hover/with-hover:opacity-100' )} /> - + ); } diff --git a/ui/desktop/src/components/WelcomeGooseLogo.tsx b/ui/desktop/src/components/WelcomeGooseLogo.tsx index b47ab9fa376f..3063bfe34eb0 100644 --- a/ui/desktop/src/components/WelcomeGooseLogo.tsx +++ b/ui/desktop/src/components/WelcomeGooseLogo.tsx @@ -1,13 +1,16 @@ -import { Goose, Rain } from './icons/Goose'; +import { Rain } from './icons/Goose'; +import { useGlyphPack } from '../contexts/GlyphContext'; export default function WelcomeGooseLogo({ className = '' }) { + const { pack } = useGlyphPack(); + return (
- +
); diff --git a/ui/desktop/src/contexts/GlyphContext.tsx b/ui/desktop/src/contexts/GlyphContext.tsx new file mode 100644 index 000000000000..cf62cc4b1f90 --- /dev/null +++ b/ui/desktop/src/contexts/GlyphContext.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; +import type { GlyphPack } from '../packs/types'; +import { goosePack } from '../packs/goose'; +import { getPackById } from '../packs/registry'; + +interface GlyphContextValue { + pack: GlyphPack; + setPackId: (id: string) => void; +} + +const GlyphContext = createContext({ + pack: goosePack, + setPackId: () => {}, +}); + +export function useGlyphPack(): GlyphContextValue { + return useContext(GlyphContext); +} + +interface GlyphProviderProps { + children: React.ReactNode; +} + +export function GlyphProvider({ children }: GlyphProviderProps) { + const [packId, setPackIdState] = useState( + () => localStorage.getItem('glyphPack') || 'goose' + ); + + const pack = getPackById(packId) ?? goosePack; + + const setPackId = useCallback((id: string) => { + localStorage.setItem('glyphPack', id); + setPackIdState(id); + }, []); + + return ( + + {children} + + ); +} diff --git a/ui/desktop/src/packs/goose.ts b/ui/desktop/src/packs/goose.ts new file mode 100644 index 000000000000..5b329d5070a4 --- /dev/null +++ b/ui/desktop/src/packs/goose.ts @@ -0,0 +1,17 @@ +import { Goose } from '../components/icons/Goose'; +import { Bird1 } from '../components/icons/Bird1'; +import { Bird2 } from '../components/icons/Bird2'; +import { Bird3 } from '../components/icons/Bird3'; +import { Bird4 } from '../components/icons/Bird4'; +import { Bird5 } from '../components/icons/Bird5'; +import { Bird6 } from '../components/icons/Bird6'; +import type { GlyphPack } from './types'; + +export const goosePack: GlyphPack = { + id: 'goose', + name: 'Goose', + emoji: '🪿', + description: 'The original.', + StaticGlyph: Goose, + AnimationFrames: [Bird1, Bird2, Bird3, Bird4, Bird5, Bird6], +}; diff --git a/ui/desktop/src/packs/registry.ts b/ui/desktop/src/packs/registry.ts new file mode 100644 index 000000000000..5af01baad417 --- /dev/null +++ b/ui/desktop/src/packs/registry.ts @@ -0,0 +1,10 @@ +import type { GlyphPack } from './types'; +import { goosePack } from './goose'; + +export const allPacks: GlyphPack[] = [goosePack]; + +const packMap = new Map(allPacks.map((p) => [p.id, p])); + +export function getPackById(id: string): GlyphPack | undefined { + return packMap.get(id); +} diff --git a/ui/desktop/src/packs/types.ts b/ui/desktop/src/packs/types.ts new file mode 100644 index 000000000000..9965be50f952 --- /dev/null +++ b/ui/desktop/src/packs/types.ts @@ -0,0 +1,17 @@ +import type { ComponentType } from 'react'; + +/** Props accepted by all glyph components — matches existing icon conventions */ +export interface GlyphProps { + className?: string; +} + +export interface GlyphPack { + id: string; + name: string; + emoji: string; + description: string; + /** Static icon used in sidebar logo, welcome screen, etc. */ + StaticGlyph: ComponentType; + /** Frame-based animation components (goose uses Bird1–6). If absent, StaticGlyph is used. */ + AnimationFrames?: ComponentType[]; +}