Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions io-connect-desktop-launchpad/.gitignore
Original file line number Diff line number Diff line change
@@ -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*
3 changes: 3 additions & 0 deletions io-connect-desktop-launchpad/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
build
6 changes: 6 additions & 0 deletions io-connect-desktop-launchpad/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
220 changes: 220 additions & 0 deletions io-connect-desktop-launchpad/README.md
Original file line number Diff line number Diff line change
@@ -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 `<Launchpad />` component. You
don't need to pass a `components` prop at all - `<Launchpad />` 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 (
<ThemeProvider>
<PlatformPrefsProvider>
<NotificationsProvider>
<DownloadManagerProvider>
<PanelManagerProvider>
<LaunchpadProvider>
<LaunchpadBodyPopupProvider>
<PanelPopupProvider>
<Launchpad />
</PanelPopupProvider>
</LaunchpadBodyPopupProvider>
</LaunchpadProvider>
</PanelManagerProvider>
</DownloadManagerProvider>
</NotificationsProvider>
</PlatformPrefsProvider>
</ThemeProvider>
);
}
```

All customization happens through the `components` prop of `<Launchpad />` - 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

`<Launchpad components={{ ... }} />` 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
> `<Launchpad />` 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`

`<Launchpad />` 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
<Launchpad />;

// 2. Explicitly empty - hides the zone entirely
<Launchpad components={{ header: { AfterSearch: () => null } }} />;

// 3. Custom component - fully replaces the zone
function CustomBeforeSearch() {
return <MyCustomLogo />;
}

<Launchpad components={{ header: { BeforeSearch: CustomBeforeSearch } }} />;
```

### 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 (
<>
<ExtendedArea />
<MainContextMenu />
</>
);
}
```

### 5. Passing your own component

Compose your own implementation alongside (or instead of) the default pieces:

```tsx
function MyMainContextMenu() {
return <button onClick={() => console.log("clicked!")}>⋮</button>;
}

function AfterSearch() {
return (
<>
<ExtendedArea />
<NotificationsButton />
<MyMainContextMenu />
</>
);
}
```

### 6. Overriding `BeforeSearch` / `AfterSearch`

Pass `components.header` to fully replace either zone with something entirely custom:

```tsx
function CustomBeforeSearch() {
return <MyCustomLogo />;
}

function CustomAfterSearch() {
return <MyCustomButton />;
}

const launchpadComponents = { header: { BeforeSearch: CustomBeforeSearch, AfterSearch: CustomAfterSearch } };

<Launchpad components={launchpadComponents} />;
```

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 (
<>
<DragHandle />
<LogoButton />
<MyExtraBadge />
</>
);
}

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 (
<>
<DragHandle />
<LogoButton iconSrc="/my-logo.png" onClick={() => 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.
15 changes: 15 additions & 0 deletions io-connect-desktop-launchpad/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<title>Launchpad</title>
<base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions io-connect-desktop-launchpad/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file added io-connect-desktop-launchpad/public/favicon.ico
Binary file not shown.
41 changes: 41 additions & 0 deletions io-connect-desktop-launchpad/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IOConnectProvider settings={ioConfig}>
<GlobalIOBinder />
<Suspense fallback={null}>
<Launchpad />
</Suspense>
</IOConnectProvider>
);
}

export default App;
37 changes: 37 additions & 0 deletions io-connect-desktop-launchpad/src/components/launchpad.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider>
<PlatformPrefsProvider>
<NotificationsProvider>
<DownloadManagerProvider>
<PanelManagerProvider>
<LaunchpadProvider>
<LaunchpadBodyPopupProvider>
<PanelPopupProvider>
<Launchpad />
</PanelPopupProvider>
</LaunchpadBodyPopupProvider>
</LaunchpadProvider>
</PanelManagerProvider>
</DownloadManagerProvider>
</NotificationsProvider>
</PlatformPrefsProvider>
</ThemeProvider>
);
}

export default LaunchpadWrapper;
Loading
Loading