|
| 1 | +# @studio/ui |
| 2 | + |
| 3 | +A portable UI layer for Studio that can run as both an Electron renderer and a standalone web app. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +### Dual-target design |
| 8 | + |
| 9 | +The same React application runs in two environments with different data backends: |
| 10 | + |
| 11 | +- **Electron**: Uses an IPC connector that delegates to `window.ipcApi` (exposed by the Electron preload script). Auth is optional (handled by the desktop app). |
| 12 | +- **Web**: Uses a REST connector backed by the Telex API (`telex.automattic.ai`) with WordPress.com OAuth for authentication. |
| 13 | + |
| 14 | +A single entry point (`main.tsx`) selects the connector at runtime based on the `__IS_ELECTRON__` Vite define (set via the `--mode` flag). The UI code never imports environment-specific modules directly. |
| 15 | + |
| 16 | +### Connector pattern |
| 17 | + |
| 18 | +The `Connector` interface (`data/core/types.ts`) defines the data operations the UI needs: |
| 19 | + |
| 20 | +``` |
| 21 | +data/core/ |
| 22 | + types.ts # Connector interface + domain types (SiteDetails, AuthUser) |
| 23 | + connector-context.tsx # React Context provider + useConnector() hook |
| 24 | + query-client.ts # TanStack Query client with localStorage persistence |
| 25 | + connectors/ |
| 26 | + ipc/index.ts # Electron IPC implementation (requiresAuth: false) |
| 27 | + rest/index.ts # Telex REST API + WP.com OAuth (requiresAuth: true) |
| 28 | +``` |
| 29 | + |
| 30 | +Components access data through the `useConnector()` hook, which pulls the active connector from React Context. This keeps all UI code environment-agnostic. |
| 31 | + |
| 32 | +The `Connector` interface includes an auth surface (`requiresAuth`, `isAuthenticated`, `authenticate`, `logout`, `getAuthUser`) alongside data methods (`getSites`, `createSite`, `deleteSite`, `startSite`, `stopSite`). The interface is intentionally minimal -- new methods are added as features are built. |
| 33 | + |
| 34 | +### Authentication |
| 35 | + |
| 36 | +The REST connector uses WordPress.com OAuth (implicit grant flow) for authentication: |
| 37 | + |
| 38 | +1. `connector.authenticate()` redirects to `public-api.wordpress.com/oauth2/authorize` |
| 39 | +2. After authorization, WordPress.com redirects back to `/auth/callback#access_token=...` |
| 40 | +3. `main.tsx` intercepts the callback **before React mounts** to avoid TanStack Router parsing issues with special characters in the hash fragment |
| 41 | +4. The token and user profile are stored in `localStorage` |
| 42 | + |
| 43 | +Route protection is handled in the root route's `beforeLoad` hook. When `connector.requiresAuth` is true, all routes except `/login` require authentication -- unauthenticated users are redirected to `/login`. |
| 44 | + |
| 45 | +The IPC connector sets `requiresAuth: false`, so auth checks are skipped entirely in Electron. |
| 46 | + |
| 47 | +### Data fetching with TanStack Query |
| 48 | + |
| 49 | +Query hooks in `data/queries/` wrap connector methods with TanStack Query for caching, deduplication, and cache invalidation. The query client uses localStorage persistence (24h max age) mirroring the wp-calypso setup. |
| 50 | + |
| 51 | +```typescript |
| 52 | +function useSites() { |
| 53 | + const connector = useConnector(); |
| 54 | + return useQuery({ |
| 55 | + queryKey: ['sites'], |
| 56 | + queryFn: () => connector.getSites(), |
| 57 | + }); |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +Mutations invalidate related queries on success, keeping the UI in sync without manual refetching. |
| 62 | + |
| 63 | +### Routing with TanStack Router |
| 64 | + |
| 65 | +Routes are **code-based** (not file-based), following the wp-calypso hosting dashboard pattern. Routes are defined with `createRoute()` calls under `router/` and assembled into a route tree in `router/router.tsx`. |
| 66 | + |
| 67 | +The router context carries both the `QueryClient` and `Connector`, enabling route-level data prefetching and auth checks in `beforeLoad` hooks. |
| 68 | + |
| 69 | +### Component structure |
| 70 | + |
| 71 | +Components use a folder-per-component pattern with CSS Modules: |
| 72 | + |
| 73 | +``` |
| 74 | +components/ |
| 75 | + sidebar-layout/ |
| 76 | + index.tsx |
| 77 | + style.module.css |
| 78 | + site-list/ |
| 79 | + index.tsx |
| 80 | + style.module.css |
| 81 | + onboarding-layout/ |
| 82 | + index.tsx |
| 83 | + style.module.css |
| 84 | +``` |
| 85 | + |
| 86 | +UI is built with `@wordpress/ui` and `@wordpress/theme` from the WordPress Design System, plus `@wordpress/icons` for iconography. |
| 87 | + |
| 88 | +## Development |
| 89 | + |
| 90 | +You can run the UI in two modes during development: |
| 91 | + |
| 92 | + |
| 93 | +```bash |
| 94 | +# Web mode (REST connector, Telex API) |
| 95 | +npm -w @studio/ui run dev:web |
| 96 | + |
| 97 | +# Electron mode (Electron app with IPC connector) |
| 98 | +npm run start:new-ui |
| 99 | +``` |
0 commit comments