@@ -7,6 +7,15 @@ import { RpcApi } from "@/app/store/wshclientapi";
77import { TabRpcClient } from "@/app/store/wshrpcutil" ;
88import { atoms , createBlock , getApi , isDev } from "@/store/global" ;
99import { fireAndForget , isBlank , makeIconClass } from "@/util/util" ;
10+ import {
11+ FloatingPortal ,
12+ autoUpdate ,
13+ offset ,
14+ shift ,
15+ useDismiss ,
16+ useFloating ,
17+ useInteractions ,
18+ } from "@floating-ui/react" ;
1019import clsx from "clsx" ;
1120import { useAtomValue } from "jotai" ;
1221import { memo , useCallback , useEffect , useRef , useState } from "react" ;
@@ -67,6 +76,132 @@ const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal
6776 ) ;
6877} ) ;
6978
79+ function calculateGridSize ( appCount : number ) : number {
80+ if ( appCount <= 4 ) return 2 ;
81+ if ( appCount <= 9 ) return 3 ;
82+ if ( appCount <= 16 ) return 4 ;
83+ if ( appCount <= 25 ) return 5 ;
84+ return 6 ;
85+ }
86+
87+ const AppsFloatingWindow = memo (
88+ ( {
89+ isOpen,
90+ onClose,
91+ referenceElement,
92+ } : {
93+ isOpen : boolean ;
94+ onClose : ( ) => void ;
95+ referenceElement : HTMLElement ;
96+ } ) => {
97+ const [ apps , setApps ] = useState < AppInfo [ ] > ( [ ] ) ;
98+ const [ loading , setLoading ] = useState ( true ) ;
99+
100+ const { refs, floatingStyles, context } = useFloating ( {
101+ open : isOpen ,
102+ onOpenChange : onClose ,
103+ placement : "left-start" ,
104+ middleware : [ offset ( - 2 ) , shift ( { padding : 12 } ) ] ,
105+ whileElementsMounted : autoUpdate ,
106+ elements : {
107+ reference : referenceElement ,
108+ } ,
109+ } ) ;
110+
111+ const dismiss = useDismiss ( context ) ;
112+ const { getFloatingProps } = useInteractions ( [ dismiss ] ) ;
113+
114+ useEffect ( ( ) => {
115+ if ( ! isOpen ) return ;
116+
117+ const fetchApps = async ( ) => {
118+ setLoading ( true ) ;
119+ try {
120+ const allApps = await RpcApi . ListAllAppsCommand ( TabRpcClient ) ;
121+ const localApps = allApps
122+ . filter ( ( app ) => ! app . appid . startsWith ( "draft/" ) )
123+ . sort ( ( a , b ) => {
124+ const aName = a . appid . replace ( / ^ l o c a l \/ / , "" ) ;
125+ const bName = b . appid . replace ( / ^ l o c a l \/ / , "" ) ;
126+ return aName . localeCompare ( bName ) ;
127+ } ) ;
128+ setApps ( localApps ) ;
129+ } catch ( error ) {
130+ console . error ( "Failed to fetch apps:" , error ) ;
131+ setApps ( [ ] ) ;
132+ } finally {
133+ setLoading ( false ) ;
134+ }
135+ } ;
136+
137+ fetchApps ( ) ;
138+ } , [ isOpen ] ) ;
139+
140+ if ( ! isOpen ) return null ;
141+
142+ const gridSize = calculateGridSize ( apps . length ) ;
143+
144+ return (
145+ < FloatingPortal >
146+ < div
147+ ref = { refs . setFloating }
148+ style = { floatingStyles }
149+ { ...getFloatingProps ( ) }
150+ className = "bg-modalbg border border-border rounded-lg shadow-xl p-4 z-50"
151+ >
152+ { loading ? (
153+ < div className = "flex items-center justify-center p-8" >
154+ < i className = "fa fa-solid fa-spinner fa-spin text-2xl text-muted" > </ i >
155+ </ div >
156+ ) : apps . length === 0 ? (
157+ < div className = "text-muted text-sm p-4 text-center" > No local apps found</ div >
158+ ) : (
159+ < div
160+ className = "grid gap-3"
161+ style = { {
162+ gridTemplateColumns : `repeat(${ gridSize } , minmax(0, 1fr))` ,
163+ maxWidth : `${ gridSize * 80 } px` ,
164+ } }
165+ >
166+ { apps . map ( ( app ) => {
167+ const appMeta = app . manifest ?. appmeta ;
168+ const displayName = app . appid . replace ( / ^ l o c a l \/ / , "" ) ;
169+ const icon = appMeta ?. icon || "cube" ;
170+ const iconColor = appMeta ?. iconcolor || "white" ;
171+
172+ return (
173+ < div
174+ key = { app . appid }
175+ className = "flex flex-col items-center justify-center p-2 rounded hover:bg-hoverbg cursor-pointer transition-colors"
176+ onClick = { ( ) => {
177+ const blockDef : BlockDef = {
178+ meta : {
179+ view : "tsunami" ,
180+ controller : "tsunami" ,
181+ "tsunami:appid" : app . appid ,
182+ } ,
183+ } ;
184+ createBlock ( blockDef ) ;
185+ onClose ( ) ;
186+ } }
187+ >
188+ < div style = { { color : iconColor } } className = "text-3xl mb-1" >
189+ < i className = { makeIconClass ( icon , false ) } > </ i >
190+ </ div >
191+ < div className = "text-xxs text-center text-secondary break-words w-full px-1" >
192+ { displayName }
193+ </ div >
194+ </ div >
195+ ) ;
196+ } ) }
197+ </ div >
198+ ) }
199+ </ div >
200+ </ FloatingPortal >
201+ ) ;
202+ }
203+ ) ;
204+
70205const Widgets = memo ( ( ) => {
71206 const fullConfig = useAtomValue ( atoms . fullConfigAtom ) ;
72207 const hasCustomAIPresets = useAtomValue ( atoms . hasCustomAIPresetsAtom ) ;
@@ -100,6 +235,9 @@ const Widgets = memo(() => {
100235 : Object . fromEntries ( Object . entries ( widgetsMap ) . filter ( ( [ key ] ) => key !== "defwidget@ai" ) ) ;
101236 const widgets = sortByDisplayOrder ( filteredWidgets ) ;
102237
238+ const [ isAppsOpen , setIsAppsOpen ] = useState ( false ) ;
239+ const appsButtonRef = useRef < HTMLDivElement > ( null ) ;
240+
103241 const checkModeNeeded = useCallback ( ( ) => {
104242 if ( ! containerRef . current || ! measurementRef . current ) return ;
105243
@@ -204,10 +342,27 @@ const Widgets = memo(() => {
204342 ) ) }
205343 </ div >
206344 < div className = "flex-grow" />
207- { showHelp ? (
345+ { isDev ( ) || showHelp ? (
208346 < div className = "grid grid-cols-2 gap-0 w-full" >
209- < Widget key = "tips" widget = { tipsWidget } mode = { mode } />
210- < Widget key = "help" widget = { helpWidget } mode = { mode } />
347+ { isDev ( ) ? (
348+ < div
349+ ref = { appsButtonRef }
350+ className = "flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-sm overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
351+ onClick = { ( ) => setIsAppsOpen ( ! isAppsOpen ) }
352+ >
353+ < Tooltip content = "Local WaveApps" placement = "left" disable = { isAppsOpen } >
354+ < div >
355+ < i className = { makeIconClass ( "cube" , true ) } > </ i >
356+ </ div >
357+ </ Tooltip >
358+ </ div >
359+ ) : null }
360+ { showHelp ? (
361+ < >
362+ < Widget key = "tips" widget = { tipsWidget } mode = { mode } />
363+ < Widget key = "help" widget = { helpWidget } mode = { mode } />
364+ </ >
365+ ) : null }
211366 </ div >
212367 ) : null }
213368 </ >
@@ -217,6 +372,24 @@ const Widgets = memo(() => {
217372 < Widget key = { `widget-${ idx } ` } widget = { data } mode = { mode } />
218373 ) ) }
219374 < div className = "flex-grow" />
375+ { isDev ( ) ? (
376+ < div
377+ ref = { appsButtonRef }
378+ className = "flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-lg overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
379+ onClick = { ( ) => setIsAppsOpen ( ! isAppsOpen ) }
380+ >
381+ < Tooltip content = "Local WaveApps" placement = "left" disable = { isAppsOpen } >
382+ < div >
383+ < i className = { makeIconClass ( "cube" , true ) } > </ i >
384+ </ div >
385+ { mode === "normal" && (
386+ < div className = "text-xxs mt-0.5 w-full px-0.5 text-center whitespace-nowrap overflow-hidden text-ellipsis" >
387+ apps
388+ </ div >
389+ ) }
390+ </ Tooltip >
391+ </ div >
392+ ) : null }
220393 { showHelp ? (
221394 < >
222395 < Widget key = "tips" widget = { tipsWidget } mode = { mode } />
@@ -234,6 +407,13 @@ const Widgets = memo(() => {
234407 </ div >
235408 ) : null }
236409 </ div >
410+ { isDev ( ) && appsButtonRef . current && (
411+ < AppsFloatingWindow
412+ isOpen = { isAppsOpen }
413+ onClose = { ( ) => setIsAppsOpen ( false ) }
414+ referenceElement = { appsButtonRef . current }
415+ />
416+ ) }
237417
238418 < div
239419 ref = { measurementRef }
@@ -249,6 +429,14 @@ const Widgets = memo(() => {
249429 < Widget key = "measurement-help" widget = { helpWidget } mode = "normal" />
250430 </ >
251431 ) : null }
432+ { isDev ( ) ? (
433+ < div className = "flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-lg" >
434+ < div >
435+ < i className = { makeIconClass ( "cube" , true ) } > </ i >
436+ </ div >
437+ < div className = "text-xxs mt-0.5 w-full px-0.5 text-center" > apps</ div >
438+ </ div >
439+ ) : null }
252440 { isDev ( ) ? (
253441 < div
254442 className = "dev-label flex justify-center items-center w-full py-1 text-accent text-[30px]"
0 commit comments