diff --git a/io-connect-desktop-launchpad/.gitignore b/io-connect-desktop-launchpad/.gitignore
new file mode 100644
index 0000000..4d29575
--- /dev/null
+++ b/io-connect-desktop-launchpad/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/io-connect-desktop-launchpad/.prettierignore b/io-connect-desktop-launchpad/.prettierignore
new file mode 100644
index 0000000..63520da
--- /dev/null
+++ b/io-connect-desktop-launchpad/.prettierignore
@@ -0,0 +1,3 @@
+node_modules
+dist
+build
diff --git a/io-connect-desktop-launchpad/.prettierrc b/io-connect-desktop-launchpad/.prettierrc
new file mode 100644
index 0000000..8164313
--- /dev/null
+++ b/io-connect-desktop-launchpad/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "semi": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 100
+}
diff --git a/io-connect-desktop-launchpad/README.md b/io-connect-desktop-launchpad/README.md
new file mode 100644
index 0000000..6b023a5
--- /dev/null
+++ b/io-connect-desktop-launchpad/README.md
@@ -0,0 +1,220 @@
+# Launchpad App for io.Connect Desktop
+
+Template for creating a custom desktop Launchpad app for **io.Connect Desktop**, built on top of the
+`IOLaunchpadDesktop` feature exported by [`@interopio/components-react`](../../packages/components-react). Clone
+this app and customize only the pieces you need — everything else keeps working out of the box.
+
+## Prerequisites
+
+**io.Connect Desktop** is required, since this app only configures the `desktop` factory (see
+[src/constants/constants.ts](src/constants/constants.ts)).
+
+## Setup
+
+- Run `npm install` to install all dependencies.
+- Run `npm run start` to start the app (dev server runs on `http://localhost:4242` by default, see
+ [vite.config.ts](vite.config.ts)).
+- Point your io.Connect Desktop configuration's Launchpad/System application entry to this app's URL, then start
+ io.Connect Desktop to see your customized Launchpad. Refer to your io.Connect Desktop version's documentation for
+ the exact application-definition file and property to update.
+- Run `npm run build` to produce a production build in `dist/`.
+- Start **io.Connect Desktop** to start using and modifying your Launchpad App.
+
+## How it's wired
+
+`src/components/launchpad.tsx` wires up the required providers around a single `` component. You
+don't need to pass a `components` prop at all - `` already renders a fully working default header
+(`DragHandle` + `LogoButton` before search, `ExtendedArea` + `NotificationsButton` + `MainContextMenu` after
+search) out of the box:
+
+```tsx
+import {
+ ThemeProvider,
+ PlatformPrefsProvider,
+ IOActivePanelRenderer,
+ IODownloadManager,
+ IOLaunchpadDesktop,
+ IONotifications,
+} from "@interopio/components-react";
+
+const { LaunchpadProvider, LaunchpadBodyPopupProvider, PanelPopupProvider, Launchpad } = IOLaunchpadDesktop;
+const { NotificationsProvider } = IONotifications;
+const { DownloadManagerProvider } = IODownloadManager;
+const { PanelManagerProvider } = IOActivePanelRenderer;
+
+function LaunchpadWrapper() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+All customization happens through the `components` prop of `` - you shouldn't need to touch the
+provider chain above.
+
+## Customization
+
+### 1. Replacing the default io.Connect Desktop Launchpad with a custom implementation
+
+For details on how io.Connect Desktop discovers and loads a custom Launchpad app, see the
+[Launchpad documentation](https://docs.interop.io/desktop/capabilities/launcher/index.html#launchpad).
+
+
+### 2. The `components` prop
+
+`` forwards straight to the native `IOLaunchpad.ContentsContainer`, so it only
+accepts `header` (`BeforeSearch` / `AfterSearch`) and `sections` - there's no per-piece override system.
+`DragHandle`, `LogoButton`, `NotificationsButton`, `MainContextMenu`, `ExtendedArea` are exported standalone so you
+compose them yourself into `header.BeforeSearch` / `AfterSearch`, as shown above.
+
+> **Tip:** define `BeforeSearch` / `AfterSearch` (and the `components` object) **outside** your component's render
+> function (module scope), or memoize them. Passing a brand-new inline function on every render forces
+> `` to remount that zone on every render - wasteful in general, and it can also break in-flight
+> async work started by a piece inside it (e.g. opening the docked search popup).
+
+### 3. Default `BeforeSearch` / `AfterSearch`
+
+`` fills in `header.BeforeSearch` / `AfterSearch` with its own defaults whenever you don't specify
+them, so there are 3 possible outcomes per zone:
+
+1. **Omitted** (key not present, or `undefined`) - the library default renders (`DragHandle` + `LogoButton` for
+ `BeforeSearch`; `ExtendedArea` + `NotificationsButton` + `MainContextMenu` for `AfterSearch`).
+2. **Explicitly empty** (e.g. `BeforeSearch: () => null`) - the default is overridden and nothing renders in that
+ zone.
+3. **A real custom component** - that component renders in place of the default.
+
+```tsx
+// 1. Omitted - renders the library defaults
+;
+
+// 2. Explicitly empty - hides the zone entirely
+ null } }} />;
+
+// 3. Custom component - fully replaces the zone
+function CustomBeforeSearch() {
+ return ;
+}
+
+;
+```
+
+### 4. Removing a piece
+
+Since `header.BeforeSearch` / `AfterSearch` **fully replace** the zone, just omit a piece from your composition to
+hide it - e.g. hide the notifications bell:
+
+```tsx
+function AfterSearch() {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+### 5. Passing your own component
+
+Compose your own implementation alongside (or instead of) the default pieces:
+
+```tsx
+function MyMainContextMenu() {
+ return ;
+}
+
+function AfterSearch() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+```
+
+### 6. Overriding `BeforeSearch` / `AfterSearch`
+
+Pass `components.header` to fully replace either zone with something entirely custom:
+
+```tsx
+function CustomBeforeSearch() {
+ return ;
+}
+
+function CustomAfterSearch() {
+ return ;
+}
+
+const launchpadComponents = { header: { BeforeSearch: CustomBeforeSearch, AfterSearch: CustomAfterSearch } };
+
+;
+```
+
+Since `header.BeforeSearch` / `AfterSearch` **fully replace** the zone, if you just want to add something
+alongside the defaults, import the default pieces (they're also exported standalone) and compose them yourself:
+
+```tsx
+import { IOLaunchpadDesktop } from "@interopio/components-react";
+
+const { DragHandle, LogoButton } = IOLaunchpadDesktop;
+
+function CustomBeforeSearch() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+const launchpadComponents = { header: { BeforeSearch: CustomBeforeSearch } };
+```
+
+### 7. Customizing the `LogoButton`
+
+`LogoButton` itself accepts `icon`, `iconSrc`, and `onClick` props, so you don't need to replace it entirely just
+to change the logo or its click behavior:
+
+```tsx
+import { IOLaunchpadDesktop } from "@interopio/components-react";
+
+const { DragHandle, LogoButton } = IOLaunchpadDesktop;
+
+function BeforeSearch() {
+ return (
+ <>
+
+ console.log("logo clicked")} />
+ >
+ );
+}
+```
+
+- `icon` — an icon variant name (ignored if `iconSrc` is set); defaults to `"logo"`.
+- `iconSrc` — a custom image URL/data-URI to render instead of `icon`.
+- `onClick` — replaces the default collapse/popup-toggle behavior; drag-to-move still works either way.
+
+### 7. `ExtendedArea` visibility
+
+`ExtendedArea` self-guards: it renders `null` unless the Launchpad is both docked and the
+`LAUNCHPAD_SHOW_EXTENDED_AREA` platform pref is enabled, so it's safe to always include it in `AfterSearch` - it
+simply won't render anything otherwise.
diff --git a/io-connect-desktop-launchpad/index.html b/io-connect-desktop-launchpad/index.html
new file mode 100644
index 0000000..2695372
--- /dev/null
+++ b/io-connect-desktop-launchpad/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Launchpad
+
+
+
+
+
+
+
+
+
+
diff --git a/io-connect-desktop-launchpad/package.json b/io-connect-desktop-launchpad/package.json
new file mode 100644
index 0000000..5345b4d
--- /dev/null
+++ b/io-connect-desktop-launchpad/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "io-connect-desktop-launchpad",
+ "version": "4.6.0",
+ "homepage": "./",
+ "private": true,
+ "scripts": {
+ "start": "vite",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@interopio/components-react": "~4.6.0",
+ "@interopio/desktop": "~6.16.0",
+ "@interopio/react-hooks": "~4.6.0",
+ "@interopio/workspaces-ui-react": "~4.6.0",
+ "@interopio/modals-api": "~4.6.0",
+ "@interopio/search-api": "^3.3.0",
+ "@interopio/theme": "~4.6.0",
+ "react": "~18.2.0",
+ "react-dom": "~18.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "~20.12.7",
+ "@types/react": "~18.2.79",
+ "@types/react-dom": "~18.2.25",
+ "@vitejs/plugin-react": "~3.0.0",
+ "typescript": "~5.8.2",
+ "vite": "~4.0.1"
+ }
+}
diff --git a/io-connect-desktop-launchpad/public/favicon.ico b/io-connect-desktop-launchpad/public/favicon.ico
new file mode 100644
index 0000000..1a05395
Binary files /dev/null and b/io-connect-desktop-launchpad/public/favicon.ico differ
diff --git a/io-connect-desktop-launchpad/src/app/app.tsx b/io-connect-desktop-launchpad/src/app/app.tsx
new file mode 100644
index 0000000..40a53d7
--- /dev/null
+++ b/io-connect-desktop-launchpad/src/app/app.tsx
@@ -0,0 +1,41 @@
+import { lazy, Suspense, useContext, useEffect } from "react";
+import { IOConnectContext, IOConnectProvider } from "@interopio/react-hooks";
+import { ioConfig } from "../constants/constants";
+import "@interopio/workspaces-ui-react/dist/styles/workspaces.css";
+import "@interopio/components-react/dist/styles/components/ui/dropdown-menu.css";
+import "@interopio/components-react/dist/styles/components/ui/overlay-scrollbars-container.css";
+import "@interopio/components-react/dist/styles/features/active-panel-renderer/styles.css";
+import "@interopio/components-react/dist/styles/features/notifications/styles.css";
+import "@interopio/components-react/dist/styles/features/preferences/styles.css";
+import "@interopio/components-react/dist/styles/features/profile/styles.css";
+import "@interopio/components-react/dist/styles/features/launchpad/styles.css";
+import "@interopio/components-react/dist/styles/features/launchpad-desktop/styles.css";
+
+const Launchpad = lazy(() => import("../components/launchpad"));
+
+// IOConnectProvider owns the only io.Connect initialization - read the resulting instance from context instead of initializing again.
+function GlobalIOBinder() {
+ const io = useContext(IOConnectContext);
+
+ useEffect(() => {
+ if (io) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (globalThis as any).io = io;
+ }
+ }, [io]);
+
+ return null;
+}
+
+export function App() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/io-connect-desktop-launchpad/src/components/launchpad.tsx b/io-connect-desktop-launchpad/src/components/launchpad.tsx
new file mode 100644
index 0000000..115f1c4
--- /dev/null
+++ b/io-connect-desktop-launchpad/src/components/launchpad.tsx
@@ -0,0 +1,37 @@
+import {
+ ThemeProvider,
+ PlatformPrefsProvider,
+ IOActivePanelRenderer,
+ IODownloadManager,
+ IOLaunchpadDesktop,
+ IONotifications,
+} from "@interopio/components-react";
+
+const { PanelManagerProvider } = IOActivePanelRenderer;
+const { DownloadManagerProvider } = IODownloadManager;
+const { LaunchpadProvider, LaunchpadBodyPopupProvider, PanelPopupProvider, Launchpad } = IOLaunchpadDesktop;
+const { NotificationsProvider } = IONotifications;
+
+function LaunchpadWrapper() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default LaunchpadWrapper;
diff --git a/io-connect-desktop-launchpad/src/constants/constants.ts b/io-connect-desktop-launchpad/src/constants/constants.ts
new file mode 100644
index 0000000..8e60736
--- /dev/null
+++ b/io-connect-desktop-launchpad/src/constants/constants.ts
@@ -0,0 +1,28 @@
+import IODesktop from "@interopio/desktop";
+import IOSearch from "@interopio/search-api";
+import IOWorkspaces from "@interopio/workspaces-api";
+import IOModalsAPI from "@interopio/modals-api";
+import { IOConnectInitSettings } from "@interopio/react-hooks";
+
+export const ioConfig = {
+ desktop: {
+ factory: IODesktop,
+ config: {
+ gateway: {
+ logging: {
+ level: "warn",
+ },
+ },
+ libraries: [IOWorkspaces, IOSearch, IOModalsAPI],
+ appManager: "full",
+ layouts: "full",
+ activities: "trackAll",
+ channels: false,
+ metrics: false,
+ contexts: true,
+ systemLogger: {
+ level: "warn",
+ },
+ },
+ },
+} as IOConnectInitSettings;
diff --git a/io-connect-desktop-launchpad/src/main.tsx b/io-connect-desktop-launchpad/src/main.tsx
new file mode 100644
index 0000000..784f2cf
--- /dev/null
+++ b/io-connect-desktop-launchpad/src/main.tsx
@@ -0,0 +1,6 @@
+import * as ReactDOM from "react-dom/client";
+import App from "./app/app";
+
+const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
+
+root.render();
diff --git a/io-connect-desktop-launchpad/src/styles.css b/io-connect-desktop-launchpad/src/styles.css
new file mode 100644
index 0000000..fb8e8d5
--- /dev/null
+++ b/io-connect-desktop-launchpad/src/styles.css
@@ -0,0 +1,2 @@
+/* App-specific overrides go here. Launchpad desktop styling now ships with
+ @interopio/components-react/dist/styles/features/launchpad-desktop/styles.css */
diff --git a/io-connect-desktop-launchpad/tsconfig.json b/io-connect-desktop-launchpad/tsconfig.json
new file mode 100644
index 0000000..d72ea2f
--- /dev/null
+++ b/io-connect-desktop-launchpad/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "allowJs": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ }
+}
diff --git a/io-connect-desktop-launchpad/vite-env.d.ts b/io-connect-desktop-launchpad/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/io-connect-desktop-launchpad/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/io-connect-desktop-launchpad/vite.config.ts b/io-connect-desktop-launchpad/vite.config.ts
new file mode 100644
index 0000000..4e31992
--- /dev/null
+++ b/io-connect-desktop-launchpad/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ base: "./",
+ server: {
+ port: 4242,
+ host: "localhost",
+ },
+ build: {
+ commonjsOptions: {
+ include: [/@interopio\/components-react/, /node_modules/],
+ },
+ },
+});