diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index aafaad7b0..557154a71 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -362,6 +362,11 @@ class RpcApiType { return client.wshRpcCall("listallappfiles", data, opts); } + // command "listallapps" [call] + ListAllAppsCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("listallapps", null, opts); + } + // command "listalleditableapps" [call] ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise { return client.wshRpcCall("listalleditableapps", null, opts); diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index f22e784cf..0390614a7 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -7,6 +7,15 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, createBlock, getApi, isDev } from "@/store/global"; import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; +import { + FloatingPortal, + autoUpdate, + offset, + shift, + useDismiss, + useFloating, + useInteractions, +} from "@floating-ui/react"; import clsx from "clsx"; import { useAtomValue } from "jotai"; import { memo, useCallback, useEffect, useRef, useState } from "react"; @@ -67,6 +76,132 @@ const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal ); }); +function calculateGridSize(appCount: number): number { + if (appCount <= 4) return 2; + if (appCount <= 9) return 3; + if (appCount <= 16) return 4; + if (appCount <= 25) return 5; + return 6; +} + +const AppsFloatingWindow = memo( + ({ + isOpen, + onClose, + referenceElement, + }: { + isOpen: boolean; + onClose: () => void; + referenceElement: HTMLElement; + }) => { + const [apps, setApps] = useState([]); + const [loading, setLoading] = useState(true); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: onClose, + placement: "left-start", + middleware: [offset(-2), shift({ padding: 12 })], + whileElementsMounted: autoUpdate, + elements: { + reference: referenceElement, + }, + }); + + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); + + useEffect(() => { + if (!isOpen) return; + + const fetchApps = async () => { + setLoading(true); + try { + const allApps = await RpcApi.ListAllAppsCommand(TabRpcClient); + const localApps = allApps + .filter((app) => !app.appid.startsWith("draft/")) + .sort((a, b) => { + const aName = a.appid.replace(/^local\//, ""); + const bName = b.appid.replace(/^local\//, ""); + return aName.localeCompare(bName); + }); + setApps(localApps); + } catch (error) { + console.error("Failed to fetch apps:", error); + setApps([]); + } finally { + setLoading(false); + } + }; + + fetchApps(); + }, [isOpen]); + + if (!isOpen) return null; + + const gridSize = calculateGridSize(apps.length); + + return ( + +
+ {loading ? ( +
+ +
+ ) : apps.length === 0 ? ( +
No local apps found
+ ) : ( +
+ {apps.map((app) => { + const appMeta = app.manifest?.appmeta; + const displayName = app.appid.replace(/^local\//, ""); + const icon = appMeta?.icon || "cube"; + const iconColor = appMeta?.iconcolor || "white"; + + return ( +
{ + const blockDef: BlockDef = { + meta: { + view: "tsunami", + controller: "tsunami", + "tsunami:appid": app.appid, + }, + }; + createBlock(blockDef); + onClose(); + }} + > +
+ +
+
+ {displayName} +
+
+ ); + })} +
+ )} +
+
+ ); + } +); + const Widgets = memo(() => { const fullConfig = useAtomValue(atoms.fullConfigAtom); const hasCustomAIPresets = useAtomValue(atoms.hasCustomAIPresetsAtom); @@ -100,6 +235,9 @@ const Widgets = memo(() => { : Object.fromEntries(Object.entries(widgetsMap).filter(([key]) => key !== "defwidget@ai")); const widgets = sortByDisplayOrder(filteredWidgets); + const [isAppsOpen, setIsAppsOpen] = useState(false); + const appsButtonRef = useRef(null); + const checkModeNeeded = useCallback(() => { if (!containerRef.current || !measurementRef.current) return; @@ -204,10 +342,27 @@ const Widgets = memo(() => { ))}
- {showHelp ? ( + {isDev() || showHelp ? (
- - + {isDev() ? ( +
setIsAppsOpen(!isAppsOpen)} + > + +
+ +
+
+
+ ) : null} + {showHelp ? ( + <> + + + + ) : null}
) : null} @@ -217,6 +372,24 @@ const Widgets = memo(() => { ))}
+ {isDev() ? ( +
setIsAppsOpen(!isAppsOpen)} + > + +
+ +
+ {mode === "normal" && ( +
+ apps +
+ )} +
+
+ ) : null} {showHelp ? ( <> @@ -234,6 +407,13 @@ const Widgets = memo(() => {
) : null}
+ {isDev() && appsButtonRef.current && ( + setIsAppsOpen(false)} + referenceElement={appsButtonRef.current} + /> + )}
{ ) : null} + {isDev() ? ( +
+
+ +
+
apps
+
+ ) : null} {isDev() ? (