diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore index 4108b33e..4cec9104 100644 --- a/apps/desktop/.gitignore +++ b/apps/desktop/.gitignore @@ -9,7 +9,9 @@ lerna-debug.log* node_modules dist +dist-electron dist-ssr +release *.local # Editor directories and files diff --git a/apps/desktop/README.md b/apps/desktop/README.md index f02aedf8..d5bcf73d 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,30 +1,22 @@ -# React + TypeScript + Vite +# @multica/desktop -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Electron desktop app. Vite + React + `createHashRouter`. -Currently, two official plugins are available: +## Development -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} +```bash +multica dev desktop ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +## Build + +```bash +pnpm --filter @multica/desktop build +``` + +## Conventions + +- **Routing**: `react-router-dom` v7 with `createHashRouter` (Electron loads via `file://`, BrowserRouter won't work). Pages go in `src/pages/`. +- **UI**: All components from `@multica/ui`. No local UI components. +- **State**: Store hooks from `@multica/store`. +- **Styles**: Tailwind CSS v4 via `@multica/ui/globals.css`, imported in `src/main.tsx`. diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 302852f4..94948866 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -4,18 +4,8 @@ import path from 'node:path' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -// The built directory structure -// -// ├─┬─┬ dist -// │ │ └── index.html -// │ │ -// │ ├─┬ dist-electron -// │ │ ├── main.js -// │ │ └── preload.mjs -// │ process.env.APP_ROOT = path.join(__dirname, '..') -// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') @@ -26,17 +16,11 @@ let win: BrowserWindow | null function createWindow() { win = new BrowserWindow({ - icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), }, }) - // Test active push message to Renderer-process. - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) - if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL) } else { @@ -45,9 +29,6 @@ function createWindow() { } } -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() @@ -56,8 +37,6 @@ app.on('window-all-closed', () => { }) app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow() } diff --git a/apps/desktop/index.html b/apps/desktop/index.html index 1136ddeb..d17771da 100644 --- a/apps/desktop/index.html +++ b/apps/desktop/index.html @@ -2,9 +2,8 @@ - - Vite + React + TS + Multica
diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8eb3132b..f98f7e98 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,7 +12,8 @@ "dependencies": { "@multica/ui": "workspace:*", "react": "catalog:", - "react-dom": "catalog:" + "react-dom": "catalog:", + "react-router-dom": "^7.13.0" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", diff --git a/apps/desktop/public/electron-vite.animate.svg b/apps/desktop/public/electron-vite.animate.svg deleted file mode 100644 index ea3e7770..00000000 --- a/apps/desktop/public/electron-vite.animate.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/desktop/public/electron-vite.svg b/apps/desktop/public/electron-vite.svg deleted file mode 100644 index 8a6aefe6..00000000 --- a/apps/desktop/public/electron-vite.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/desktop/public/vite.svg b/apps/desktop/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/apps/desktop/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index fc7cead5..bd26458b 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,7 +1,12 @@ -import { ComponentExample } from '@multica/ui/components/component-example' +import { createHashRouter, RouterProvider } from 'react-router-dom' +import HomePage from './pages/home' +import ChatPage from './pages/chat' -function App() { - return +const router = createHashRouter([ + { path: '/', element: }, + { path: '/chat', element: }, +]) + +export default function App() { + return } - -export default App diff --git a/apps/desktop/src/assets/react.svg b/apps/desktop/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/apps/desktop/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index ad3387ad..a4e610e1 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -8,8 +8,3 @@ ReactDOM.createRoot(document.getElementById('root')!).render( , ) - -// Use contextBridge -window.ipcRenderer.on('main-process-message', (_event, message) => { - console.log(message) -}) diff --git a/apps/desktop/src/pages/chat.tsx b/apps/desktop/src/pages/chat.tsx new file mode 100644 index 00000000..f3495285 --- /dev/null +++ b/apps/desktop/src/pages/chat.tsx @@ -0,0 +1,15 @@ +import { useNavigate } from 'react-router-dom' +import { Button } from '@multica/ui/components/ui/button' + +export default function ChatPage() { + const navigate = useNavigate() + + return ( +
+

Chat

+ +
+ ) +} diff --git a/apps/desktop/src/pages/home.tsx b/apps/desktop/src/pages/home.tsx new file mode 100644 index 00000000..c8042511 --- /dev/null +++ b/apps/desktop/src/pages/home.tsx @@ -0,0 +1,12 @@ +import { useNavigate } from 'react-router-dom' +import { Button } from '@multica/ui/components/ui/button' + +export default function HomePage() { + const navigate = useNavigate() + + return ( +
+ +
+ ) +} diff --git a/packages/store/README.md b/packages/store/README.md new file mode 100644 index 00000000..38e02ee4 --- /dev/null +++ b/packages/store/README.md @@ -0,0 +1,17 @@ +# @multica/store + +Zustand state management for Multica apps. + +## Usage + +```tsx +// From barrel +import { useHubStore, useMessagesStore, useGatewayStore } from '@multica/store' + +// Per-file subpath import +import { useGatewayStore } from '@multica/store/gateway' +import { useHubStore } from '@multica/store/hub' +import { useMessagesStore } from '@multica/store/messages' +import { useHubInit } from '@multica/store/hub-init' +import { useDeviceId } from '@multica/store/device-id' +``` diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 00000000..e61096c6 --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,32 @@ +# @multica/ui + +Shared UI component library. Shadcn + Tailwind CSS v4. + +## Usage + +```tsx +// UI components — subpath imports, no barrel +import { Button } from '@multica/ui/components/ui/button' +import { Card, CardContent } from '@multica/ui/components/ui/card' + +// Feature components +import { ThemeProvider } from '@multica/ui/components/theme-provider' +import { Chat } from '@multica/ui/components/chat' +import { Markdown } from '@multica/ui/components/markdown' + +// Hooks +import { useIsMobile } from '@multica/ui/hooks/use-mobile' +import { useAutoScroll } from '@multica/ui/hooks/use-auto-scroll' + +// Utilities +import { cn } from '@multica/ui/lib/utils' + +// Styles (app entry point) +import '@multica/ui/globals.css' +``` + +## Adding Components + +```bash +pnpm --filter @multica/ui dlx shadcn@latest add +``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fc53daf..a71a43c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.3(react@19.2.3) + react-router-dom: + specifier: ^7.13.0 + version: 7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) devDependencies: '@tailwindcss/vite': specifier: ^4.1.18 @@ -5282,6 +5285,23 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.13.0: + resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.13.0: + resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -5485,6 +5505,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -10008,7 +10031,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -10041,7 +10064,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10056,7 +10079,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -12368,6 +12391,20 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-router: 7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + + react-router@7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + cookie: 1.1.1 + react: 19.2.3 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react@19.2.3: {} read-config-file@6.3.2: @@ -12663,6 +12700,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4