diff --git a/.github/workflows/publish-npm-core.yml b/.github/workflows/publish-npm-core.yml index 462dbdc8e..403deb100 100644 --- a/.github/workflows/publish-npm-core.yml +++ b/.github/workflows/publish-npm-core.yml @@ -1,10 +1,9 @@ name: Publish core Package to npmjs on: push: - tags: ["v[0-9]+.[0-9]+.[0-9]+-core"] - paths: ["core/**", ".github/workflows/publish-npm-core.yml"] - pull_request: - paths: ["core/**", ".github/workflows/publish-npm-core.yml"] + tags: ['v[0-9]+.[0-9]+.[0-9]+-core'] + paths: ['core/**', '.github/workflows/publish-npm-core.yml'] + workflow_dispatch: jobs: build-and-publish-plugins: environment: production @@ -12,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: "0" + fetch-depth: '0' token: ${{ secrets.PAT_SERVICE_ACCOUNT }} - name: Install jq @@ -24,7 +23,7 @@ jobs: env: GITHUB_REF: ${{ github.ref }} - - name: "Get Semantic Version from tag" + - name: 'Get Semantic Version from tag' if: github.event_name == 'push' run: | # Get the tag from the event @@ -42,8 +41,8 @@ jobs: # Setup .npmrc file to publish to npm - uses: actions/setup-node@v3 with: - node-version: "20.x" - registry-url: "https://registry.npmjs.org" + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' - run: cd core && corepack enable && corepack prepare yarn@4.5.3 --activate && yarn --version && yarn config set -H enableImmutableInstalls false && yarn install && yarn build diff --git a/.github/workflows/publish-npm-joi.yml b/.github/workflows/publish-npm-joi.yml deleted file mode 100644 index 867ad80fe..000000000 --- a/.github/workflows/publish-npm-joi.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish joi Package to npmjs -on: - push: - tags: ["v[0-9]+.[0-9]+.[0-9]+-joi"] - paths: ["joi/**", ".github/workflows/publish-npm-joi.yml"] - pull_request: - paths: ["joi/**", ".github/workflows/publish-npm-joi.yml"] -jobs: - build-and-publish-plugins: - environment: production - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "0" - token: ${{ secrets.PAT_SERVICE_ACCOUNT }} - - - name: Install jq - uses: dcarbone/install-jq-action@v2.0.1 - - - name: Extract tag name without v prefix - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" - env: - GITHUB_REF: ${{ github.ref }} - - - name: "Get Semantic Version from tag" - if: github.event_name == 'push' - run: | - # Get the tag from the event - tag=${GITHUB_REF#refs/tags/v} - # remove the -joi suffix - new_version=$(echo $tag | sed -n 's/-joi//p') - echo $new_version - # Replace the old version with the new version in package.json - jq --arg version "$new_version" '.version = $version' joi/package.json > /tmp/package.json && mv /tmp/package.json joi/package.json - - # Print the new version - echo "Updated package.json version to: $new_version" - cat joi/package.json - - # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v3 - with: - node-version: "20.x" - registry-url: "https://registry.npmjs.org" - - - run: cd joi && corepack enable && corepack prepare yarn@4.5.3 --activate && yarn --version && yarn config set -H enableImmutableInstalls false && yarn install && yarn build - - - run: cd joi && yarn publish --access public - if: github.event_name == 'push' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/template-tauri-build-linux-x64.yml b/.github/workflows/template-tauri-build-linux-x64.yml index 7eb946706..6a15a2ee0 100644 --- a/.github/workflows/template-tauri-build-linux-x64.yml +++ b/.github/workflows/template-tauri-build-linux-x64.yml @@ -119,8 +119,8 @@ jobs: "usr/lib/Jan/binaries/libvulkan.so": "binaries/libvulkan.so"}' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json fi mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json - mv /tmp/package.json web/package.json + jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json + mv /tmp/package.json web-app/package.json ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" cat ./src-tauri/Cargo.toml diff --git a/.github/workflows/template-tauri-build-macos.yml b/.github/workflows/template-tauri-build-macos.yml index 9ad2cf1ba..e3c457433 100644 --- a/.github/workflows/template-tauri-build-macos.yml +++ b/.github/workflows/template-tauri-build-macos.yml @@ -120,8 +120,8 @@ jobs: # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json - mv /tmp/package.json web/package.json + jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json + mv /tmp/package.json web-app/package.json ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" cat ./src-tauri/Cargo.toml diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index e6f948c9a..54c236485 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -97,8 +97,8 @@ jobs: # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true | .bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json - mv /tmp/package.json web/package.json + jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json + mv /tmp/package.json web-app/package.json ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------Cargo.toml---------" diff --git a/Makefile b/Makefile index db83e6b21..56b50a9d2 100644 --- a/Makefile +++ b/Makefile @@ -24,88 +24,26 @@ ifeq ($(OS),Windows_NT) echo "skip" endif yarn install - yarn build:joi yarn build:core yarn build:extensions -check-file-counts: install-and-build -ifeq ($(OS),Windows_NT) - powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Where-Object Name -like *-extension* | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extensions with package.json'; exit 1 } else { Write-Host 'Extension build successful' }" -else - @tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d -exec test -e '{}/package.json' \; -print | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi -endif - -dev: check-file-counts - yarn dev - -dev-tauri: check-file-counts +dev: install-and-build yarn install:cortex yarn download:bin yarn copy:lib - CLEAN=true yarn dev:tauri + yarn dev + +# Deprecated soon +dev-tauri: install-and-build + yarn install:cortex + yarn download:bin + yarn copy:lib + yarn dev:tauri # Linting -lint: check-file-counts +lint: install-and-build yarn lint -update-playwright-config: -ifeq ($(OS),Windows_NT) - echo -e "const RPconfig = {\n\ - apiKey: '$(REPORT_PORTAL_API_KEY)',\n\ - endpoint: '$(REPORT_PORTAL_URL)',\n\ - project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\ - launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\ - attributes: [\n\ - {\n\ - key: 'key',\n\ - value: 'value',\n\ - },\n\ - {\n\ - value: 'value',\n\ - },\n\ - ],\n\ - description: '$(REPORT_PORTAL_DESCRIPTION)',\n\ - }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts; - sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts - -else ifeq ($(shell uname -s),Linux) - echo "const RPconfig = {\n\ - apiKey: '$(REPORT_PORTAL_API_KEY)',\n\ - endpoint: '$(REPORT_PORTAL_URL)',\n\ - project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\ - launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\ - attributes: [\n\ - {\n\ - key: 'key',\n\ - value: 'value',\n\ - },\n\ - {\n\ - value: 'value',\n\ - },\n\ - ],\n\ - description: '$(REPORT_PORTAL_DESCRIPTION)',\n\ - }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts; - sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts -else - echo "const RPconfig = {\n\ - apiKey: '$(REPORT_PORTAL_API_KEY)',\n\ - endpoint: '$(REPORT_PORTAL_URL)',\n\ - project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\ - launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\ - attributes: [\n\ - {\n\ - key: 'key',\n\ - value: 'value',\n\ - },\n\ - {\n\ - value: 'value',\n\ - },\n\ - ],\n\ - description: '$(REPORT_PORTAL_DESCRIPTION)',\n\ - }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts; - sed -i '' "s|^ reporter: .*| reporter: [['@reportportal\/agent-js-playwright', RPconfig]],|" electron/playwright.config.ts -endif - # Testing test: lint # yarn build:test @@ -114,16 +52,17 @@ test: lint yarn test # Builds and publishes the app -build-and-publish: check-file-counts - yarn build:publish - -# Build -build: check-file-counts +build-and-publish: install-and-build yarn build -build-tauri: check-file-counts +# Build +build: install-and-build + yarn build + +# Deprecated soon +build-tauri: install-and-build yarn copy:lib - yarn build-tauri + yarn build clean: ifeq ($(OS),Windows_NT) diff --git a/electron/.eslintrc.js b/electron/.eslintrc.js deleted file mode 100644 index 20e79804f..000000000 --- a/electron/.eslintrc.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - env: { - node: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react/recommended', - ], - rules: { - 'react/prop-types': 'off', // In favor of strong typing - no need to dedupe - 'react/no-is-mounted': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-explicit-any': 'off', - }, - settings: { - react: { - createClass: 'createReactClass', // Regex for Component Factory to use, - // default to "createReactClass" - pragma: 'React', // Pragma to use, default to "React" - version: 'detect', // React version. "detect" automatically picks the version you have installed. - // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. - // default to latest and warns if missing - // It will default to "detect" in the future - }, - linkComponents: [ - // Components used as alternatives to for linking, eg. - 'Hyperlink', - { name: 'Link', linkAttribute: 'to' }, - ], - }, - ignorePatterns: [ - 'build', - 'renderer', - 'node_modules', - '@global', - 'playwright-report', - 'test-data', - ], -} diff --git a/electron/@global/index.ts b/electron/@global/index.ts deleted file mode 100644 index b2d55fc1c..000000000 --- a/electron/@global/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export {} - -declare global { - namespace NodeJS { - interface Global { - core: any - } - } - var core: any | undefined -} diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist deleted file mode 100644 index ad77a2a1e..000000000 --- a/electron/entitlements.mac.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.disable-library-validation - - - \ No newline at end of file diff --git a/electron/handlers/common.ts b/electron/handlers/common.ts deleted file mode 100644 index a2a1bd2f7..000000000 --- a/electron/handlers/common.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Handler, RequestHandler } from '@janhq/core/node' -import { ipcMain } from 'electron' -import { windowManager } from '../managers/window' - -export function injectHandler() { - const ipcWrapper: Handler = ( - route: string, - listener: (...args: any[]) => any - ) => - ipcMain.handle(route, async (_event, ...args: any[]) => { - return listener(...args) - }) - - const handler = new RequestHandler( - ipcWrapper, - (channel: string, args: any) => - windowManager.mainWindow?.webContents.send(channel, args) - ) - handler.handle() -} diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts deleted file mode 100644 index f8f70c302..000000000 --- a/electron/handlers/native.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { app, ipcMain, dialog, shell, nativeTheme } from 'electron' -import { autoUpdater } from 'electron-updater' -import { join } from 'path' -import { windowManager } from '../managers/window' -import { - ModuleManager, - getJanDataFolderPath, - getJanExtensionsPath, - init, - AppEvent, - NativeRoute, - SelectFileProp, -} from '@janhq/core/node' -import { SelectFileOption } from '@janhq/core' -import { menu } from '../utils/menu' -import { migrate } from '../utils/migration' -import { createUserSpace } from '../utils/path' -import { setupExtensions } from '../utils/extension' - -const isMac = process.platform === 'darwin' - -export function handleAppIPCs() { - /** - * Handles the "openAppDirectory" IPC message by opening the app's user data directory. - * The `shell.openPath` method is used to open the directory in the user's default file explorer. - * @param _event - The IPC event object. - */ - ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => { - shell.openPath(getJanDataFolderPath()) - }) - - ipcMain.handle(NativeRoute.appUpdateDownload, async (_event) => { - autoUpdater.downloadUpdate() - }) - - /** - * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". - * This will change the appearance of the app to the light theme. - */ - ipcMain.handle(NativeRoute.setNativeThemeLight, () => { - nativeTheme.themeSource = 'light' - }) - - /** - * Handles the "setCloseApp" IPC message by closing the main application window. - * This effectively closes the application if no other windows are open. - */ - ipcMain.handle(NativeRoute.setCloseApp, () => { - windowManager.mainWindow?.close() - }) - - /** - * Handles the "setMinimizeApp" IPC message by minimizing the main application window. - * The window will be minimized to the system's taskbar or dock. - */ - ipcMain.handle(NativeRoute.setMinimizeApp, () => { - windowManager.mainWindow?.minimize() - }) - - /** - * Handles the "setMaximizeApp" IPC message. It toggles the maximization state of the main window. - * If the window is currently maximized, it will be un-maximized (restored to its previous size). - * If the window is not maximized, it will be maximized to fill the screen. - * @param _event - The IPC event object. - */ - ipcMain.handle(NativeRoute.setMaximizeApp, async (_event) => { - if (windowManager.mainWindow?.isMaximized()) { - windowManager.mainWindow.unmaximize() - } else { - windowManager.mainWindow?.maximize() - } - }) - - /** - * Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark". - * This will change the appearance of the app to the dark theme. - */ - ipcMain.handle(NativeRoute.setNativeThemeDark, () => { - nativeTheme.themeSource = 'dark' - }) - - /** - * Opens a URL in the user's default browser. - * @param _event - The IPC event object. - * @param url - The URL to open. - */ - ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => { - shell.openExternal(url) - }) - - /** - * Opens a URL in the user's default browser. - * @param _event - The IPC event object. - * @param url - The URL to open. - */ - ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => { - shell.openPath(url) - }) - - /** - * Relaunches the app in production - reload window in development. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle(NativeRoute.relaunch, async (_event) => { - ModuleManager.instance.clearImportedModules() - - if (app.isPackaged) { - app.relaunch() - app.exit() - } else { - for (const modulePath in ModuleManager.instance.requiredModules) { - delete require.cache[ - require.resolve(join(getJanExtensionsPath(), modulePath)) - ] - } - init({ - // Function to check from the main process that user wants to install a extension - confirmInstall: async (_extensions: string[]) => { - return true - }, - // Path to install extension to - extensionsPath: getJanExtensionsPath(), - }) - windowManager.mainWindow?.reload() - } - }) - - /** - * Handles the "selectDirectory" IPC message to open a dialog for selecting a directory. - * If no main window is found, logs an error and exits. - * @returns {string} The path of the selected directory, or nothing if canceled. - */ - ipcMain.handle(NativeRoute.selectDirectory, async () => { - const mainWindow = windowManager.mainWindow - if (!mainWindow) { - console.error('No main window found') - return - } - const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { - title: 'Select a folder', - buttonLabel: 'Select Folder', - properties: ['openDirectory', 'createDirectory'], - }) - if (canceled) { - return - } else { - return filePaths[0] - } - }) - - /** - * Handles the "selectFiles" IPC message to open a dialog for selecting files. - * Allows options for setting the dialog title, button label, and selection properties. - * Logs an error if no main window is found. - * @param _event - The IPC event object. - * @param option - Options for customizing file selection dialog. - * @returns {string[]} An array of selected file paths, or nothing if canceled. - */ - ipcMain.handle( - NativeRoute.selectFiles, - async (_event, option?: SelectFileOption) => { - const mainWindow = windowManager.mainWindow - if (!mainWindow) { - console.error('No main window found') - return - } - - const title = option?.title ?? 'Select files' - const buttonLabel = option?.buttonLabel ?? 'Select' - const props: SelectFileProp[] = ['openFile'] - - if (option?.allowMultiple) { - props.push('multiSelections') - } - - if (option?.selectDirectory) { - props.push('openDirectory') - } - console.debug(`Select files with props: ${props}`) - const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { - title, - buttonLabel, - properties: props, - filters: option?.filters, - }) - - if (canceled) return - - return filePaths - } - ) - - /** - * Handles the "hideQuickAskWindow" IPC message to hide the quick ask window. - * @returns A promise that resolves when the window is hidden. - */ - ipcMain.handle( - NativeRoute.hideQuickAskWindow, - async (): Promise => windowManager.hideQuickAskWindow() - ) - - /** - * Handles the "sendQuickAskInput" IPC message to send user input to the main window. - * @param _event - The IPC event object. - * @param input - User input string to be sent. - */ - ipcMain.handle( - NativeRoute.sendQuickAskInput, - async (_event, input: string): Promise => { - windowManager.mainWindow?.webContents.send( - AppEvent.onUserSubmitQuickAsk, - input - ) - } - ) - - /** - * Handles the "showOpenMenu" IPC message to show the context menu at given coordinates. - * Only applicable on non-Mac platforms. - * @param e - The event object. - * @param args - Contains coordinates where the menu should appear. - */ - ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) { - if (!isMac && windowManager.mainWindow) { - menu.popup({ - window: windowManager.mainWindow, - x: args.x, - y: args.y, - }) - } - }) - - /** - * Handles the "hideMainWindow" IPC message to hide the main application window. - * @returns A promise that resolves when the window is hidden. - */ - ipcMain.handle( - NativeRoute.hideMainWindow, - async (): Promise => windowManager.hideMainWindow() - ) - - /** - * Handles the "showMainWindow" IPC message to show the main application window. - * @returns A promise that resolves when the window is shown. - */ - ipcMain.handle( - NativeRoute.showMainWindow, - async (): Promise => windowManager.showMainWindow() - ) - - /** - * Handles the "quickAskSizeUpdated" IPC message to update the size of the quick ask window. - * Resizes window by the given height offset. - * @param _event - The IPC event object. - * @param heightOffset - The amount of height to increase. - * @returns A promise that resolves when the window is resized. - */ - ipcMain.handle( - NativeRoute.quickAskSizeUpdated, - async (_event, heightOffset: number): Promise => - windowManager.expandQuickAskWindow(heightOffset) - ) - - /** - * Handles the "ackDeepLink" IPC message to acknowledge a deep link. - * Triggers handling of deep link in the application. - * @param _event - The IPC event object. - * @returns A promise that resolves when the deep link is acknowledged. - */ - ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise => { - windowManager.ackDeepLink() - }) - - /** - * Handles the "factoryReset" IPC message to reset the application to its initial state. - * Clears loaded modules, recreates user space, runs migrations, and sets up extensions. - * @param _event - The IPC event object. - * @returns A promise that resolves after the reset operations are complete. - */ - ipcMain.handle(NativeRoute.factoryReset, async (_event): Promise => { - ModuleManager.instance.clearImportedModules() - return createUserSpace().then(migrate).then(setupExtensions) - }) - - /** - * Handles the "startServer" IPC message to start the Jan API server. - * Initializes and starts server with provided configuration options. - * @param _event - The IPC event object. - * @param args - Configuration object containing host, port, CORS settings etc. - * @returns Promise that resolves when server starts successfully - */ - ipcMain.handle( - NativeRoute.startServer, - async (_event, args): Promise => { - const { startServer } = require('@janhq/server') - return startServer({ - host: args?.host, - port: args?.port, - isCorsEnabled: args?.isCorsEnabled, - isVerboseEnabled: args?.isVerboseEnabled, - prefix: args?.prefix, - }) - } - ) - - /** - * Handles the "stopServer" IPC message to stop the Jan API server. - * Gracefully shuts down the server instance. - * @param _event - The IPC event object - * @returns Promise that resolves when server stops successfully - */ - ipcMain.handle(NativeRoute.stopServer, async (_event): Promise => { - /** - * Stop Jan API Server. - */ - const { stopServer } = require('@janhq/server') - return stopServer() - }) - - /** - * Handles the "appToken" IPC message to generate a random app ID. - */ - ipcMain.handle(NativeRoute.appToken, async (_event): Promise => { - return process.env.appToken ?? 'cortex.cpp' - }) -} diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts deleted file mode 100644 index 5dcbda582..000000000 --- a/electron/handlers/update.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { app, dialog } from 'electron' -import { windowManager } from './../managers/window' -import { - ProgressInfo, - UpdateDownloadedEvent, - UpdateInfo, - autoUpdater, -} from 'electron-updater' -import { AppEvent } from '@janhq/core/node' -import { trayManager } from '../managers/tray' - -export let waitingToInstallVersion: string | undefined = undefined - -export function handleAppUpdates() { - /* Should not check for update during development */ - if (!app.isPackaged) { - return - } - - /* New Update Available */ - autoUpdater.on('update-available', async (_info: UpdateInfo) => { - windowManager.mainWindow?.webContents.send( - AppEvent.onAppUpdateAvailable, - {} - ) - }) - - /* App Update Completion Message */ - autoUpdater.on('update-downloaded', async (_info: UpdateDownloadedEvent) => { - windowManager.mainWindow?.webContents.send( - AppEvent.onAppUpdateDownloadSuccess, - {} - ) - const action = await dialog.showMessageBox({ - message: `Update downloaded. Please restart the application to apply the updates.`, - buttons: ['Restart', 'Later'], - }) - if (action.response === 0) { - trayManager.destroyCurrentTray() - windowManager.closeQuickAskWindow() - waitingToInstallVersion = _info?.version - autoUpdater.quitAndInstall() - } - }) - - /* App Update Error */ - autoUpdater.on('error', (info: Error) => { - windowManager.mainWindow?.webContents.send( - AppEvent.onAppUpdateDownloadError, - { failedToInstallVersion: waitingToInstallVersion, info } - ) - }) - - /* App Update Progress */ - autoUpdater.on('download-progress', (progress: ProgressInfo) => { - console.debug('app update progress: ', progress.percent) - windowManager.mainWindow?.webContents.send( - AppEvent.onAppUpdateDownloadUpdate, - { - ...progress, - } - ) - }) - - autoUpdater.autoDownload = false - autoUpdater.autoInstallOnAppQuit = true - if (process.env.CI !== 'e2e') { - autoUpdater.checkForUpdates() - } -} diff --git a/electron/icons/512x512.png b/electron/icons/512x512.png deleted file mode 100644 index 289f99ded..000000000 Binary files a/electron/icons/512x512.png and /dev/null differ diff --git a/electron/icons/icon-tray.png b/electron/icons/icon-tray.png deleted file mode 100644 index ab356a9dc..000000000 Binary files a/electron/icons/icon-tray.png and /dev/null differ diff --git a/electron/icons/icon-tray@2x.png b/electron/icons/icon-tray@2x.png deleted file mode 100644 index a82c285f5..000000000 Binary files a/electron/icons/icon-tray@2x.png and /dev/null differ diff --git a/electron/icons/icon.ico b/electron/icons/icon.ico deleted file mode 100644 index 5d18719e8..000000000 Binary files a/electron/icons/icon.ico and /dev/null differ diff --git a/electron/icons/icon.png b/electron/icons/icon.png deleted file mode 100644 index 289f99ded..000000000 Binary files a/electron/icons/icon.png and /dev/null differ diff --git a/electron/icons_dev/jan-beta-512x512.png b/electron/icons_dev/jan-beta-512x512.png deleted file mode 100644 index 4b715494d..000000000 Binary files a/electron/icons_dev/jan-beta-512x512.png and /dev/null differ diff --git a/electron/icons_dev/jan-beta-tray.png b/electron/icons_dev/jan-beta-tray.png deleted file mode 100644 index eaca9ad9a..000000000 Binary files a/electron/icons_dev/jan-beta-tray.png and /dev/null differ diff --git a/electron/icons_dev/jan-beta-tray@2x.png b/electron/icons_dev/jan-beta-tray@2x.png deleted file mode 100644 index deb83aace..000000000 Binary files a/electron/icons_dev/jan-beta-tray@2x.png and /dev/null differ diff --git a/electron/icons_dev/jan-beta.ico b/electron/icons_dev/jan-beta.ico deleted file mode 100644 index 85cf0c1b4..000000000 Binary files a/electron/icons_dev/jan-beta.ico and /dev/null differ diff --git a/electron/icons_dev/jan-beta.png b/electron/icons_dev/jan-beta.png deleted file mode 100644 index 4b715494d..000000000 Binary files a/electron/icons_dev/jan-beta.png and /dev/null differ diff --git a/electron/icons_dev/jan-nightly-512x512.png b/electron/icons_dev/jan-nightly-512x512.png deleted file mode 100644 index 23f532947..000000000 Binary files a/electron/icons_dev/jan-nightly-512x512.png and /dev/null differ diff --git a/electron/icons_dev/jan-nightly-tray.png b/electron/icons_dev/jan-nightly-tray.png deleted file mode 100644 index bf164a0a6..000000000 Binary files a/electron/icons_dev/jan-nightly-tray.png and /dev/null differ diff --git a/electron/icons_dev/jan-nightly-tray@2x.png b/electron/icons_dev/jan-nightly-tray@2x.png deleted file mode 100644 index 3cab5709d..000000000 Binary files a/electron/icons_dev/jan-nightly-tray@2x.png and /dev/null differ diff --git a/electron/icons_dev/jan-nightly.ico b/electron/icons_dev/jan-nightly.ico deleted file mode 100644 index 8e64ba8b1..000000000 Binary files a/electron/icons_dev/jan-nightly.ico and /dev/null differ diff --git a/electron/icons_dev/jan-nightly.png b/electron/icons_dev/jan-nightly.png deleted file mode 100644 index 23f532947..000000000 Binary files a/electron/icons_dev/jan-nightly.png and /dev/null differ diff --git a/electron/jest.config.js b/electron/jest.config.js deleted file mode 100644 index ec5968ccd..000000000 --- a/electron/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverageFrom: ['src/**/*.{ts,tsx}'], - modulePathIgnorePatterns: ['/tests'], - moduleNameMapper: { - '@/(.*)': '/src/$1', - }, - runner: './testRunner.js', - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - diagnostics: false, - }, - ], - }, -} diff --git a/electron/main.ts b/electron/main.ts deleted file mode 100644 index 59e72ca24..000000000 --- a/electron/main.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { app, BrowserWindow } from 'electron' - -import { join, resolve } from 'path' -/** - * Managers - **/ -import { windowManager } from './managers/window' -import { getAppConfigurations, log } from '@janhq/core/node' - -/** - * IPC Handlers - **/ -import { injectHandler } from './handlers/common' -import { handleAppUpdates } from './handlers/update' -import { handleAppIPCs } from './handlers/native' - -/** - * Utils - **/ -import { setupMenu } from './utils/menu' -import { createUserSpace } from './utils/path' -import { migrate } from './utils/migration' -import { cleanUpAndQuit } from './utils/clean' -import { setupExtensions } from './utils/extension' -import { setupCore } from './utils/setup' -import { setupReactDevTool } from './utils/dev' - -import { trayManager } from './managers/tray' -import { logSystemInfo } from './utils/system' -import { registerGlobalShortcuts } from './utils/shortcut' -import { registerLogger } from './utils/logger' -import { randomBytes } from 'crypto' - -const preloadPath = join(__dirname, 'preload.js') -const preloadQuickAskPath = join(__dirname, 'preload.quickask.js') -const rendererPath = join(__dirname, '..', 'renderer') -const quickAskPath = join(rendererPath, 'search.html') -const mainPath = join(rendererPath, 'index.html') - -const mainUrl = 'http://localhost:3000' -const quickAskUrl = `${mainUrl}/search` - -const gotTheLock = app.requestSingleInstanceLock() - -if (process.defaultApp) { - if (process.argv.length >= 2) { - app.setAsDefaultProtocolClient('jan', process.execPath, [ - resolve(process.argv[1]), - ]) - } -} else { - app.setAsDefaultProtocolClient('jan') -} - -const createMainWindow = () => { - const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl - windowManager.createMainWindow(preloadPath, startUrl) -} - -// Generate a random token for the app -// This token is used for authentication when making request to cortex.cpp server -process.env.appToken = randomBytes(16).toString('hex') - -app - .whenReady() - .then(() => { - if (!gotTheLock) { - app.quit() - throw new Error('Another instance of the app is already running') - } else { - app.on( - 'second-instance', - (_event, commandLine, _workingDirectory): void => { - if (process.platform === 'win32' || process.platform === 'linux') { - // this is for handling deeplink on windows and linux - // since those OS will emit second-instance instead of open-url - const url = commandLine.pop() - if (url) { - windowManager.sendMainAppDeepLink(url) - } - } - windowManager.showMainWindow() - } - ) - } - }) - .then(setupCore) - .then(createUserSpace) - .then(registerLogger) - .then(migrate) - .then(setupExtensions) - .then(setupMenu) - .then(handleIPCs) - .then(() => process.env.CI !== 'e2e' && createQuickAskWindow()) - .then(createMainWindow) - .then(handleAppUpdates) - .then(registerGlobalShortcuts) - .then(() => { - if (!app.isPackaged) { - setupReactDevTool() - windowManager.mainWindow?.webContents.openDevTools() - } - }) - .then(() => process.env.CI !== 'e2e' && trayManager.createSystemTray()) - .then(logSystemInfo) - .then(() => { - app.on('activate', () => { - if (!BrowserWindow.getAllWindows().length) { - createMainWindow() - } else { - windowManager.showMainWindow() - } - }) - }) - -app.on('open-url', (_event, url) => { - windowManager.sendMainAppDeepLink(url) -}) - -app.on('before-quit', function (_event) { - trayManager.destroyCurrentTray() -}) - -app.once('quit', () => { - cleanUpAndQuit() -}) - -app.once('window-all-closed', () => { - // Feature Toggle for Quick Ask - if ( - getAppConfigurations().quick_ask && - !windowManager.isQuickAskWindowDestroyed() - ) - return - cleanUpAndQuit() -}) - -function createQuickAskWindow() { - // Feature Toggle for Quick Ask - if (!getAppConfigurations().quick_ask) return - const startUrl = app.isPackaged ? `file://${quickAskPath}` : quickAskUrl - windowManager.createQuickAskWindow(preloadQuickAskPath, startUrl) -} - -/** - * Handles various IPC messages from the renderer process. - */ -function handleIPCs() { - // Inject core handlers for IPCs - injectHandler() - - // Handle native IPCs - handleAppIPCs() -} - -/* - ** Suppress Node error messages - */ -process.on('uncaughtException', function (err) { - log(`Error: ${err}`) -}) diff --git a/electron/managers/mainWindowConfig.ts b/electron/managers/mainWindowConfig.ts deleted file mode 100644 index 997d081c3..000000000 --- a/electron/managers/mainWindowConfig.ts +++ /dev/null @@ -1,21 +0,0 @@ -const DEFAULT_MIN_WIDTH = 400 -const DEFAULT_MIN_HEIGHT = 600 - -export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = { - skipTaskbar: false, - minWidth: DEFAULT_MIN_WIDTH, - minHeight: DEFAULT_MIN_HEIGHT, - show: true, - // we want to go frameless on windows and linux - transparent: process.platform === 'darwin', - frame: process.platform === 'darwin', - titleBarStyle: 'hiddenInset', - vibrancy: 'fullscreen-ui', - visualEffectState: 'active', - backgroundMaterial: 'acrylic', - autoHideMenuBar: true, - trafficLightPosition: { - x: 16, - y: 10, - }, -} diff --git a/electron/managers/quickAskWindowConfig.ts b/electron/managers/quickAskWindowConfig.ts deleted file mode 100644 index 93180dd07..000000000 --- a/electron/managers/quickAskWindowConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -const DEFAULT_WIDTH = 556 - -const DEFAULT_HEIGHT = 60 - -export const quickAskWindowConfig: Electron.BrowserWindowConstructorOptions = { - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - skipTaskbar: true, - acceptFirstMouse: true, - hasShadow: true, - alwaysOnTop: true, - show: false, - fullscreenable: false, - resizable: false, - center: true, - movable: true, - maximizable: false, - focusable: true, - transparent: false, - frame: false, - type: 'panel', -} diff --git a/electron/managers/tray.ts b/electron/managers/tray.ts deleted file mode 100644 index b81b1e556..000000000 --- a/electron/managers/tray.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { join } from 'path' -import { Tray, app, Menu } from 'electron' -import { windowManager } from '../managers/window' -import { getAppConfigurations } from '@janhq/core/node' - -class TrayManager { - currentTray: Tray | undefined - - createSystemTray = () => { - // Feature Toggle for Quick Ask - if (!getAppConfigurations().quick_ask) return - - if (this.currentTray) { - return - } - const iconPath = join(app.getAppPath(), 'icons', 'icon-tray.png') - const tray = new Tray(iconPath) - tray.setToolTip(app.getName()) - - tray.on('click', () => { - windowManager.showQuickAskWindow() - }) - - // Add context menu for windows only - if (process.platform === 'win32') { - const contextMenu = Menu.buildFromTemplate([ - { - label: 'Open Jan', - type: 'normal', - click: () => windowManager.showMainWindow(), - }, - { - label: 'Open Quick Ask', - type: 'normal', - click: () => windowManager.showQuickAskWindow(), - }, - { label: 'Quit', type: 'normal', click: () => app.quit() }, - ]) - - tray.setContextMenu(contextMenu) - } - this.currentTray = tray - } - - destroyCurrentTray() { - this.currentTray?.destroy() - this.currentTray = undefined - } -} - -export const trayManager = new TrayManager() diff --git a/electron/managers/window.ts b/electron/managers/window.ts deleted file mode 100644 index dbb3a5101..000000000 --- a/electron/managers/window.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { BrowserWindow, app, shell } from 'electron' -import { quickAskWindowConfig } from './quickAskWindowConfig' -import { mainWindowConfig } from './mainWindowConfig' -import { getAppConfigurations, AppEvent } from '@janhq/core/node' -import { getBounds, saveBounds } from '../utils/setup' - -/** - * Manages the current window instance. - */ -// TODO: refactor this -let isAppQuitting = false - -class WindowManager { - public mainWindow?: BrowserWindow - private _quickAskWindow: BrowserWindow | undefined = undefined - private _quickAskWindowVisible = false - private _mainWindowVisible = false - - private deeplink: string | undefined - /** - * Creates a new window instance. - * @returns The created window instance. - */ - async createMainWindow(preloadPath: string, startUrl: string) { - const bounds = await getBounds() - - this.mainWindow = new BrowserWindow({ - ...mainWindowConfig, - width: bounds.width, - height: bounds.height, - show: false, - x: bounds.x, - y: bounds.y, - webPreferences: { - nodeIntegration: true, - preload: preloadPath, - webSecurity: false, - }, - }) - - if (process.platform === 'win32' || process.platform === 'linux') { - /// This is work around for windows deeplink. - /// second-instance event is not fired when app is not open, so the app - /// does not received the deeplink. - const commandLine = process.argv.slice(1) - if (commandLine.length > 0) { - const url = commandLine[0] - this.sendMainAppDeepLink(url) - } - } - - this.mainWindow.on('resized', () => { - saveBounds(this.mainWindow?.getBounds()) - }) - - this.mainWindow.on('moved', () => { - saveBounds(this.mainWindow?.getBounds()) - }) - - /* Load frontend app to the window */ - this.mainWindow.loadURL(startUrl) - - /* Open external links in the default browser */ - this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url) - return { action: 'deny' } - }) - - app.on('before-quit', function () { - isAppQuitting = true - }) - - windowManager.mainWindow?.on('close', function (evt) { - // Feature Toggle for Quick Ask - if (!getAppConfigurations().quick_ask) return - - if (!isAppQuitting) { - evt.preventDefault() - windowManager.hideMainWindow() - } - }) - - windowManager.mainWindow?.on('ready-to-show', function () { - windowManager.mainWindow?.show() - }) - } - - createQuickAskWindow(preloadPath: string, startUrl: string): void { - this._quickAskWindow = new BrowserWindow({ - ...quickAskWindowConfig, - webPreferences: { - nodeIntegration: true, - preload: preloadPath, - webSecurity: false, - }, - }) - - this._quickAskWindow.loadURL(startUrl) - this._quickAskWindow.on('blur', () => { - this.hideQuickAskWindow() - }) - } - - isMainWindowVisible(): boolean { - return this._mainWindowVisible - } - - hideMainWindow(): void { - this.mainWindow?.hide() - this._mainWindowVisible = false - } - - showMainWindow(): void { - this.mainWindow?.show() - this._mainWindowVisible = true - } - - hideQuickAskWindow(): void { - this._quickAskWindow?.hide() - this._quickAskWindowVisible = false - } - - showQuickAskWindow(): void { - this._quickAskWindow?.show() - this._quickAskWindowVisible = true - } - - closeQuickAskWindow(): void { - if (this._quickAskWindow?.isDestroyed()) return - this._quickAskWindow?.close() - this._quickAskWindow?.destroy() - this._quickAskWindow = undefined - this._quickAskWindowVisible = false - } - - isQuickAskWindowVisible(): boolean { - return this._quickAskWindowVisible - } - - isQuickAskWindowDestroyed(): boolean { - return this._quickAskWindow?.isDestroyed() ?? true - } - - /** - * Expand the quick ask window - */ - expandQuickAskWindow(heightOffset: number): void { - const width = quickAskWindowConfig.width! - const height = quickAskWindowConfig.height! + heightOffset - this._quickAskWindow?.setMinimumSize(width, height) - this._quickAskWindow?.setSize(width, height, true) - } - - /** - * Send the selected text to the quick ask window. - */ - sendQuickAskSelectedText(selectedText: string): void { - this._quickAskWindow?.webContents.send( - AppEvent.onSelectedText, - selectedText - ) - } - - /** - * Try to send the deep link to the main app. - */ - sendMainAppDeepLink(url: string): void { - this.deeplink = url - const interval = setInterval(() => { - if (!this.deeplink) clearInterval(interval) - const mainWindow = this.mainWindow - if (mainWindow) { - mainWindow.webContents.send(AppEvent.onDeepLink, this.deeplink) - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() - } - }, 500) - } - - /** - * Send main view state to the main app. - */ - sendMainViewState(route: string) { - if (this.mainWindow && !this.mainWindow.isDestroyed()) { - this.mainWindow.webContents.send(AppEvent.onMainViewStateChange, route) - } - } - - /** - * Clean up all windows. - */ - cleanUp(): void { - if (!this.mainWindow?.isDestroyed()) { - this.mainWindow?.close() - this.mainWindow?.destroy() - this.mainWindow = undefined - this._mainWindowVisible = false - } - if (!this._quickAskWindow?.isDestroyed()) { - this._quickAskWindow?.close() - this._quickAskWindow?.destroy() - this._quickAskWindow = undefined - this._quickAskWindowVisible = false - } - } - - /** - * Acknowledges that the window has received a deep link. We can remove it. - */ - ackDeepLink() { - this.deeplink = undefined - } -} - -export const windowManager = new WindowManager() diff --git a/electron/merge-latest-ymls.js b/electron/merge-latest-ymls.js deleted file mode 100644 index ee8caf825..000000000 --- a/electron/merge-latest-ymls.js +++ /dev/null @@ -1,29 +0,0 @@ -const yaml = require('js-yaml') -const fs = require('fs') - -// get two file paths from arguments: -const [, , ...args] = process.argv -const file1 = args[0] -const file2 = args[1] -const file3 = args[2] - -// check that all arguments are present and throw error instead -if (!file1 || !file2 || !file3) { - throw new Error( - 'Please provide 3 file paths as arguments: path to file1, to file2 and destination path' - ) -} - -const doc1 = yaml.load(fs.readFileSync(file1, 'utf8')) -console.log('doc1: ', doc1) - -const doc2 = yaml.load(fs.readFileSync(file2, 'utf8')) -console.log('doc2: ', doc2) - -const merged = { ...doc1, ...doc2 } -merged.files.push(...doc1.files) - -console.log('merged', merged) - -const mergedYml = yaml.dump(merged) -fs.writeFileSync(file3, mergedYml, 'utf8') diff --git a/electron/package.json b/electron/package.json deleted file mode 100644 index 8b673114b..000000000 --- a/electron/package.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "jan", - "version": "0.1.1740752217", - "main": "./build/main.js", - "author": "Jan ", - "license": "MIT", - "productName": "Jan", - "homepage": "https://github.com/menloresearch/jan/tree/main/electron", - "description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.", - "build": { - "appId": "jan.ai.app", - "productName": "Jan", - "files": [ - "renderer/**/*", - "build/**/*.{js,map}", - "pre-install", - "themes", - "scripts/**/*", - "icons/**/*", - "themes", - "shared" - ], - "asarUnpack": [ - "pre-install", - "themes", - "docs", - "scripts", - "icons", - "themes", - "shared" - ], - "publish": [ - { - "provider": "github", - "owner": "janhq", - "repo": "jan" - } - ], - "extends": null, - "mac": { - "type": "distribution", - "entitlements": "./entitlements.mac.plist", - "entitlementsInherit": "./entitlements.mac.plist", - "notarize": { - "teamId": "F8AH6NHVY5" - }, - "icon": "icons/icon.png" - }, - "linux": { - "target": [ - "deb" - ], - "category": "Utility", - "icon": "icons/" - }, - "win": { - "icon": "icons/icon.png", - "target": [ - "nsis" - ] - }, - "nsis": { - "oneClick": true, - "installerIcon": "icons/icon.ico", - "uninstallerIcon": "icons/icon.ico", - "include": "scripts/uninstaller.nsh", - "deleteAppDataOnUninstall": true - }, - "protocols": [ - { - "name": "Jan", - "schemes": [ - "jan" - ] - } - ], - "artifactName": "jan-${os}-${arch}-${version}.${ext}" - }, - "scripts": { - "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", - "test:e2e": "DEBUG=pw:browser xvfb-maybe -- playwright test --workers=1", - "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"", - "version-patch": "run-script-os", - "version-patch:darwin:linux": "jq '.version' package.json | tr -d '\"' > .version.bak && jq --arg ver \"0.1.$(date +%s)\" '.version = $ver' package.json > package.tmp && mv package.tmp package.json", - "version-patch:win32": "node -e \"const fs=require('fs');const pkg=require('./package.json');const bak=pkg.version;fs.writeFileSync('.version.bak',bak);pkg.version='0.1.'+Math.floor(Date.now()/1000);fs.writeFileSync('package.json',JSON.stringify(pkg,null,2));\"", - "version-restore": "run-script-os", - "version-restore:darwin:linux": "jq --arg ver $(cat .version.bak) '.version = $ver' package.json > package.tmp && mv package.tmp package.json && rm .version.bak", - "version-restore:win32": "node -e \"const fs=require('fs');const pkg=require('./package.json');const bak=fs.readFileSync('.version.bak','utf8');pkg.version=bak;fs.writeFileSync('package.json',JSON.stringify(pkg,null,2));\"", - "dev:darwin:linux": "yarn copy:assets && tsc -p . && yarn version-patch && electron . && yarn version-restore", - "dev:windows": "yarn copy:assets && tsc -p . && electron .", - "dev": "run-script-os", - "compile": "tsc -p .", - "start": "electron .", - "build": "yarn copy:assets && run-script-os", - "build:test": "yarn copy:assets && run-script-os", - "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", - "build:test:win32": "tsc -p . && electron-builder -p never -w --dir", - "build:test:linux": "tsc -p . && electron-builder -p never -l --dir", - "build:darwin": "tsc -p . && electron-builder -p never -m --universal", - "build:win32": "tsc -p . && electron-builder -p never -w", - "build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage", - "build:publish": "yarn copy:assets && run-script-os", - "build:publish:darwin": "tsc -p . && electron-builder -p always -m --universal", - "build:publish:win32": "tsc -p . && electron-builder -p always -w", - "build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage" - }, - "dependencies": { - "@alumna/reflect": "^1.1.3", - "@janhq/core": "link:../core", - "@janhq/server": "link:../server", - "@kirillvakalov/nut-tree__nut-js": "4.2.1-2", - "@npmcli/arborist": "^7.1.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.7", - "fs-extra": "^11.2.0", - "pacote": "^21.0.0", - "request": "^2.88.2", - "request-progress": "^3.0.0", - "ulidx": "^2.3.0" - }, - "devDependencies": { - "@electron/notarize": "^2.5.0", - "@playwright/test": "^1.38.1", - "@reportportal/agent-js-playwright": "^5.1.7", - "@types/npmcli__arborist": "^5.6.4", - "@types/pacote": "^11.1.7", - "@types/request": "^2.48.12", - "@typescript-eslint/eslint-plugin": "^6.7.3", - "@typescript-eslint/parser": "^6.7.3", - "electron": "30.0.6", - "electron-builder": "^24.13.3", - "electron-builder-squirrel-windows": "^24.13.3", - "electron-devtools-installer": "^3.2.0", - "electron-playwright-helpers": "^1.6.0", - "eslint": "8.57.0", - "eslint-plugin-react": "^7.34.0", - "rimraf": "^5.0.5", - "run-script-os": "^1.1.6", - "typescript": "^5.3.3", - "xvfb-maybe": "^0.2.1" - }, - "installConfig": { - "hoistingLimits": "workspaces" - }, - "packageManager": "yarn@4.5.3" -} diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts deleted file mode 100644 index 71f435f57..000000000 --- a/electron/playwright.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PlaywrightTestConfig } from '@playwright/test' - -const config: PlaywrightTestConfig = { - testDir: './tests/e2e', - retries: 0, - globalTimeout: 350000, - use: { - screenshot: 'only-on-failure', - video: 'retain-on-failure', - trace: 'retain-on-failure', - }, - // reporter: [['html', { outputFolder: './playwright-report' }]], -} -export default config diff --git a/electron/pre-install/.gitkeep b/electron/pre-install/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/electron/preload.quickask.ts b/electron/preload.quickask.ts deleted file mode 100644 index 7c2cadeb6..000000000 --- a/electron/preload.quickask.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @module preload - */ - -import { APIEvents, APIRoutes } from '@janhq/core/node' -import { contextBridge, ipcRenderer } from 'electron' - -const interfaces: { [key: string]: (...args: any[]) => any } = {} - -// Loop over each route in APIRoutes -APIRoutes.forEach((method) => { - // For each method, create a function on the interfaces object - // This function invokes the method on the ipcRenderer with any provided arguments - - interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) -}) - -// Loop over each method in APIEvents -APIEvents.forEach((method) => { - // For each method, create a function on the interfaces object - // This function sets up an event listener on the ipcRenderer for the method - // The handler for the event is provided as an argument to the function - interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) -}) - -// Expose the 'interfaces' object in the main world under the name 'electronAPI' -// This allows the renderer process to access these methods directly -contextBridge.exposeInMainWorld('electronAPI', { - ...interfaces, - isQuickAsk: () => true, -}) diff --git a/electron/preload.ts b/electron/preload.ts deleted file mode 100644 index dbfcd1f1e..000000000 --- a/electron/preload.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @module preload - */ - -import { APIEvents, APIRoutes, AppConfiguration } from '@janhq/core/node' -import { contextBridge, ipcRenderer } from 'electron' -import { readdirSync } from 'fs' - -const interfaces: { [key: string]: (...args: any[]) => any } = {} - -// Loop over each route in APIRoutes -APIRoutes.forEach((method) => { - // For each method, create a function on the interfaces object - // This function invokes the method on the ipcRenderer with any provided arguments - - interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) -}) - -// Loop over each method in APIEvents -APIEvents.forEach((method) => { - // For each method, create a function on the interfaces object - // This function sets up an event listener on the ipcRenderer for the method - // The handler for the event is provided as an argument to the function - interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) -}) - -interfaces['changeDataFolder'] = async (path) => { - const appConfiguration: AppConfiguration = await ipcRenderer.invoke( - 'getAppConfigurations' - ) - const currentJanDataFolder = appConfiguration.data_folder - appConfiguration.data_folder = path - const reflect = require('@alumna/reflect') - const { err } = await reflect({ - src: currentJanDataFolder, - dest: path, - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) - if (err) { - console.error(err) - throw err - } - await ipcRenderer.invoke('updateAppConfiguration', appConfiguration) -} - -interfaces['isDirectoryEmpty'] = async (path) => { - const dirChildren = await readdirSync(path) - return dirChildren.filter((x) => x !== '.DS_Store').length === 0 -} - -// Expose the 'interfaces' object in the main world under the name 'electronAPI' -// This allows the renderer process to access these methods directly -contextBridge.exposeInMainWorld('electronAPI', { - ...interfaces, - isQuickAsk: () => false, -}) diff --git a/electron/scripts/uninstaller.nsh b/electron/scripts/uninstaller.nsh deleted file mode 100644 index 684783258..000000000 --- a/electron/scripts/uninstaller.nsh +++ /dev/null @@ -1,46 +0,0 @@ -!include nsDialogs.nsh - -XPStyle on - -!macro customUnInstall - ${ifNot} ${isUpdated} - ; Define the process name of your Electron app - StrCpy $0 "Jan.exe" - - ; Check if the application is running - nsExec::ExecToStack 'tasklist /FI "IMAGENAME eq $0" /NH' - Pop $1 - - StrCmp $1 "" notRunning - - ; If the app is running, notify the user and attempt to close it - MessageBox MB_OK "Jan is being uninstalled, force close app." IDOK forceClose - - forceClose: - ; Attempt to kill the running application - nsExec::ExecToStack 'taskkill /F /IM $0' - Pop $1 - - ; Proceed with uninstallation - Goto continueUninstall - - notRunning: - ; If the app is not running, proceed with uninstallation - Goto continueUninstall - - continueUninstall: - ; Proceed with uninstallation - DeleteRegKey HKLM "Software\Jan" - RMDir /r "$INSTDIR" - Delete "$INSTDIR\*.*" - - ; Clean up shortcuts and app data - Delete "$DESKTOP\Jan.lnk" - Delete "$STARTMENU\Programs\Jan.lnk" - RMDir /r "$APPDATA\Jan" - RMDir /r "$LOCALAPPDATA\jan-updater" - - ; Close the uninstaller - Quit - ${endIf} -!macroend \ No newline at end of file diff --git a/electron/shared/.gitkeep b/electron/shared/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/electron/sign.js b/electron/sign.js deleted file mode 100644 index 9955e53e8..000000000 --- a/electron/sign.js +++ /dev/null @@ -1,69 +0,0 @@ -const { exec } = require('child_process') - -function execCommandWithRetry(command, retries = 3) { - return new Promise((resolve, reject) => { - const execute = (attempt) => { - exec(command, (error, stdout, stderr) => { - if (error) { - console.error(`Error: ${error}`) - if (attempt < retries) { - console.log(`Retrying... Attempt ${attempt + 1}`) - execute(attempt + 1) - } else { - return reject(error) - } - } else { - console.log(`stdout: ${stdout}`) - console.error(`stderr: ${stderr}`) - resolve() - } - }) - } - execute(0) - }) -} - -function sign({ - path, - name, - certUrl, - clientId, - tenantId, - clientSecret, - certName, - timestampServer, - version, -}) { - return new Promise((resolve, reject) => { - const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"` - execCommandWithRetry(command) - .then(resolve) - .catch(reject) - }) -} - -exports.default = async function (options) { - const certUrl = process.env.AZURE_KEY_VAULT_URI - const clientId = process.env.AZURE_CLIENT_ID - const tenantId = process.env.AZURE_TENANT_ID - const clientSecret = process.env.AZURE_CLIENT_SECRET - const certName = process.env.AZURE_CERT_NAME - const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1' - - try { - await sign({ - path: options.path, - name: 'jan-win-x64', - certUrl, - clientId, - tenantId, - clientSecret, - certName, - timestampServer, - version: options.version, - }) - } catch (error) { - console.error('Failed to sign after 3 attempts:', error) - process.exit(1) - } -} diff --git a/electron/testRunner.js b/electron/testRunner.js deleted file mode 100644 index b0d108160..000000000 --- a/electron/testRunner.js +++ /dev/null @@ -1,10 +0,0 @@ -const jestRunner = require('jest-runner'); - -class EmptyTestFileRunner extends jestRunner.default { - async runTests(tests, watcher, onStart, onResult, onFailure, options) { - const nonEmptyTests = tests.filter(test => test.context.hasteFS.getSize(test.path) > 0); - return super.runTests(nonEmptyTests, watcher, onStart, onResult, onFailure, options); - } -} - -module.exports = EmptyTestFileRunner; \ No newline at end of file diff --git a/electron/tests/config/constants.ts b/electron/tests/config/constants.ts deleted file mode 100644 index 7039ad58c..000000000 --- a/electron/tests/config/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const Constants = { - VIDEO_DIR: './playwright-video', - TIMEOUT: '300000', -} diff --git a/electron/tests/config/fixtures.ts b/electron/tests/config/fixtures.ts deleted file mode 100644 index c36910161..000000000 --- a/electron/tests/config/fixtures.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - _electron as electron, - BrowserContext, - ElectronApplication, - expect, - Page, - test as base, -} from '@playwright/test' -import { - ElectronAppInfo, - findLatestBuild, - parseElectronApp, - stubDialog, -} from 'electron-playwright-helpers' -import { Constants } from './constants' -import { HubPage } from '../pages/hubPage' -import { CommonActions } from '../pages/commonActions' -import { rmSync } from 'fs' -import * as path from 'path' - -export let electronApp: ElectronApplication -export let page: Page -export let appInfo: ElectronAppInfo -export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT) - -export async function setupElectron() { - console.log(`TEST TIMEOUT: ${TIMEOUT}`) - - process.env.CI = 'e2e' - - const latestBuild = findLatestBuild('dist') - expect(latestBuild).toBeTruthy() - - // parse the packaged Electron app and find paths and other info - appInfo = parseElectronApp(latestBuild) - expect(appInfo).toBeTruthy() - - electronApp = await electron.launch({ - args: [appInfo.main, '--no-sandbox'], // main file from package.json - executablePath: appInfo.executable, // path to the Electron executable - // recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings - }) - await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - - page = await electronApp.firstWindow({ - timeout: TIMEOUT, - }) -} - -export async function teardownElectron() { - await page.close() - await electronApp.close() -} - -/** - * this fixture is needed to record and attach videos / screenshot on failed tests when - * tests are run in serial mode (i.e. browser is not closed between tests) - */ -export const test = base.extend< - { - commonActions: CommonActions - hubPage: HubPage - attachVideoPage: Page - attachScreenshotsToReport: void - }, - { createVideoContext: BrowserContext } ->({ - commonActions: async ({ request }, use, testInfo) => { - await use(new CommonActions(page, testInfo)) - }, - hubPage: async ({ commonActions }, use) => { - await use(new HubPage(page, commonActions)) - }, - createVideoContext: [ - async ({ playwright }, use) => { - const context = electronApp.context() - await use(context) - }, - { scope: 'worker' }, - ], - - attachVideoPage: [ - async ({ createVideoContext }, use, testInfo) => { - await use(page) - - if (testInfo.status !== testInfo.expectedStatus) { - const path = await createVideoContext.pages()[0].video()?.path() - await createVideoContext.close() - await testInfo.attach('video', { - path: path, - }) - } - }, - { scope: 'test', auto: true }, - ], - - attachScreenshotsToReport: [ - async ({ commonActions }, use, testInfo) => { - await use() - - // After the test, we can check whether the test passed or failed. - if (testInfo.status !== testInfo.expectedStatus) { - await commonActions.takeScreenshot('') - } - }, - { auto: true }, - ], -}) - -test.beforeAll(async () => { - rmSync(path.join(__dirname, '../../test-data'), { - recursive: true, - force: true, - }) - - test.setTimeout(TIMEOUT) - await setupElectron() - await page.waitForSelector('img[alt="Jan - Logo"]', { - state: 'visible', - timeout: TIMEOUT, - }) -}) - -test.afterAll(async () => { - // teardownElectron() -}) diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts deleted file mode 100644 index 58d6a0854..000000000 --- a/electron/tests/e2e/hub.e2e.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { test, appInfo, page, TIMEOUT } from '../config/fixtures' -import { expect } from '@playwright/test' - -test.beforeAll(async () => { - expect(appInfo).toMatchObject({ - asar: true, - executable: expect.anything(), - main: expect.anything(), - name: 'jan', - packageJson: expect.objectContaining({ name: 'jan' }), - platform: process.platform, - resourcesDir: expect.anything(), - }) -}) - -test('explores hub', async ({ hubPage }) => { - await hubPage.navigateByMenu() - await hubPage.verifyContainerVisible() - await hubPage.scrollToBottom() - const useModelBtn = page.getByTestId(/^setup-btn/).first() - - await expect(useModelBtn).toBeVisible({ - timeout: TIMEOUT, - }) -}) diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts deleted file mode 100644 index 1b463d381..000000000 --- a/electron/tests/e2e/navigation.e2e.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from '@playwright/test' -import { page, test, TIMEOUT } from '../config/fixtures' - -test('renders left navigation panel', async () => { - const threadBtn = page.getByTestId('Thread').first() - await expect(threadBtn).toBeVisible({ timeout: TIMEOUT }) - // Chat section should be there - await page.getByTestId('Local API Server').first().click({ - timeout: TIMEOUT, - }) - const localServer = page.getByTestId('local-server-testid').first() - await expect(localServer).toBeVisible({ - timeout: TIMEOUT, - }) -}) diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts deleted file mode 100644 index 06b4d1acc..000000000 --- a/electron/tests/e2e/settings.e2e.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect } from '@playwright/test' - -import { test, page, TIMEOUT } from '../config/fixtures' - -test('shows settings', async () => { - await page.getByTestId('Settings').first().click({ - timeout: TIMEOUT, - }) - const settingDescription = page.getByTestId('testid-setting-description') - await expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) -}) diff --git a/electron/tests/e2e/thread.e2e.spec.ts b/electron/tests/e2e/thread.e2e.spec.ts deleted file mode 100644 index 41efc8437..000000000 --- a/electron/tests/e2e/thread.e2e.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from '@playwright/test' -import { page, test, TIMEOUT } from '../config/fixtures' - -test('show onboarding screen without any threads created or models downloaded', async () => { - await page.getByTestId('Thread').first().click({ - timeout: TIMEOUT, - }) - const denyButton = page.locator('[data-testid="btn-deny-product-analytics"]') - - if ((await denyButton.count()) > 0) { - await denyButton.click({ force: true }) - } - - const onboardScreen = page.getByTestId('onboard-screen') - await expect(onboardScreen).toBeVisible({ - timeout: TIMEOUT, - }) -}) diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts deleted file mode 100644 index 11e3ba81a..000000000 --- a/electron/tests/pages/basePage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Page, expect } from '@playwright/test' -import { CommonActions } from './commonActions' -import { TIMEOUT } from '../config/fixtures' - -export class BasePage { - menuId: string - - constructor( - protected readonly page: Page, - readonly action: CommonActions, - protected containerId: string - ) {} - - public getValue(key: string) { - return this.action.getValue(key) - } - - public setValue(key: string, value: string) { - this.action.setValue(key, value) - } - - async takeScreenshot(name: string = '') { - await this.action.takeScreenshot(name) - } - - async navigateByMenu() { - await this.clickFirstElement(this.menuId) - } - - async clickFirstElement(testId: string) { - await this.page.getByTestId(testId).first().click() - } - - async verifyContainerVisible() { - const container = this.page.getByTestId(this.containerId) - expect(container.isVisible()).toBeTruthy() - } - - async scrollToBottom() { - await this.page.evaluate(() => { - window.scrollTo(0, document.body.scrollHeight) - }) - } - - async waitUpdateLoader() { - await this.isElementVisible('img[alt="Jan - Logo"]') - } - - //wait and find a specific element with its selector and return Visible - async isElementVisible(selector: any) { - let isVisible = true - await this.page - .waitForSelector(selector, { state: 'visible', timeout: TIMEOUT }) - .catch(() => { - isVisible = false - }) - return isVisible - } -} diff --git a/electron/tests/pages/commonActions.ts b/electron/tests/pages/commonActions.ts deleted file mode 100644 index 08ea15f92..000000000 --- a/electron/tests/pages/commonActions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Page, TestInfo } from '@playwright/test' -import { page } from '../config/fixtures' - -export class CommonActions { - private testData = new Map() - - constructor( - public page: Page, - public testInfo: TestInfo - ) {} - - async takeScreenshot(name: string) { - const screenshot = await page.screenshot({ - fullPage: true, - }) - const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}` - await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), { - body: screenshot, - contentType: 'image/png', - }) - } - - async hooks() { - console.log('hook from the scenario page') - } - - setValue(key: string, value: string) { - this.testData.set(key, value) - } - - getValue(key: string) { - return this.testData.get(key) - } -} diff --git a/electron/tests/pages/hubPage.ts b/electron/tests/pages/hubPage.ts deleted file mode 100644 index 0299ab15d..000000000 --- a/electron/tests/pages/hubPage.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Page } from '@playwright/test' -import { BasePage } from './basePage' -import { CommonActions } from './commonActions' - -export class HubPage extends BasePage { - readonly menuId: string = 'Hub' - static readonly containerId: string = 'hub-container-test-id' - - constructor( - public page: Page, - readonly action: CommonActions - ) { - super(page, action, HubPage.containerId) - } -} diff --git a/electron/tsconfig.json b/electron/tsconfig.json deleted file mode 100644 index 5116f0e88..000000000 --- a/electron/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "noImplicitAny": true, - "sourceMap": true, - "strict": true, - "outDir": "./build", - "rootDir": "./", - "noEmitOnError": true, - "esModuleInterop": true, - "baseUrl": ".", - "allowJs": true, - "skipLibCheck": true, - "paths": { "*": ["node_modules/*"] }, - "typeRoots": ["node_modules/@types"] - }, - "ts-node": { - "esm": true - }, - "include": ["./**/*.ts"], - "exclude": ["core", "build", "dist", "tests", "node_modules", "test-data"] -} diff --git a/electron/utils/clean.ts b/electron/utils/clean.ts deleted file mode 100644 index 12a68d39e..000000000 --- a/electron/utils/clean.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ModuleManager } from '@janhq/core/node' -import { windowManager } from './../managers/window' -import { dispose } from './disposable' -import { app } from 'electron' - -export function cleanUpAndQuit() { - if (!ModuleManager.instance.cleaningResource) { - ModuleManager.instance.cleaningResource = true - windowManager.cleanUp() - dispose(ModuleManager.instance.requiredModules) - ModuleManager.instance.clearImportedModules() - app.quit() - } -} diff --git a/electron/utils/dev.ts b/electron/utils/dev.ts deleted file mode 100644 index bd510096b..000000000 --- a/electron/utils/dev.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const setupReactDevTool = async () => { - // Which means you're running from source code - const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import( - 'electron-devtools-installer' - ) // Don't use import on top level, since the installer package is dev-only - try { - const name = await installExtension(REACT_DEVELOPER_TOOLS) - console.debug(`Added Extension: ${name}`) - } catch (err) { - console.error('An error occurred while installing devtools:', err) - // Only log the error and don't throw it because it's not critical - } -} diff --git a/electron/utils/disposable.ts b/electron/utils/disposable.ts deleted file mode 100644 index 59018a775..000000000 --- a/electron/utils/disposable.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function dispose(requiredModules: Record) { - for (const key in requiredModules) { - const module = requiredModules[key] - if (typeof module['dispose'] === 'function') { - module['dispose']() - } - } -} diff --git a/electron/utils/extension.ts b/electron/utils/extension.ts deleted file mode 100644 index e055411a6..000000000 --- a/electron/utils/extension.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getJanExtensionsPath, init } from '@janhq/core/node' - -export const setupExtensions = async () => { - init({ - // Function to check from the main process that user wants to install a extension - confirmInstall: async (_extensions: string[]) => { - return true - }, - // Path to install extension to - extensionsPath: getJanExtensionsPath(), - }) -} diff --git a/electron/utils/logger.ts b/electron/utils/logger.ts deleted file mode 100644 index 48af0b93a..000000000 --- a/electron/utils/logger.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - createWriteStream, - existsSync, - mkdirSync, - readdir, - stat, - unlink, - writeFileSync, -} from 'fs' -import util from 'util' -import { - getAppConfigurations, - getJanDataFolderPath, - Logger, - LoggerManager, -} from '@janhq/core/node' -import path, { join } from 'path' - -/** - * File Logger - */ -export class FileLogger implements Logger { - name = 'file' - logCleaningInterval: number = 120000 - timeout: NodeJS.Timeout | undefined - appLogPath: string = './' - logEnabled: boolean = true - - constructor( - logEnabled: boolean = true, - logCleaningInterval: number = 120000 - ) { - this.logEnabled = logEnabled - if (logCleaningInterval) this.logCleaningInterval = logCleaningInterval - - const appConfigurations = getAppConfigurations() - const logFolderPath = join(appConfigurations.data_folder, 'logs') - if (!existsSync(logFolderPath)) { - mkdirSync(logFolderPath, { recursive: true }) - } - - this.appLogPath = join(logFolderPath, 'app.log') - } - - log(args: any) { - if (!this.logEnabled) return - let message = args[0] - const scope = args[1] - if (!message) return - const path = this.appLogPath - if (!scope && !message.startsWith('[')) { - message = `[APP]::${message}` - } else if (scope) { - message = `${scope}::${message}` - } - - message = `${new Date().toISOString()} ${message}` - - writeLog(message, path) - } - - cleanLogs( - maxFileSizeBytes?: number | undefined, - daysToKeep?: number | undefined - ): void { - // clear existing timeout - // in case we rerun it with different values - if (this.timeout) clearTimeout(this.timeout) - this.timeout = undefined - - if (!this.logEnabled) return - - console.log( - 'Validating app logs. Next attempt in ', - this.logCleaningInterval - ) - - const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB - const days = daysToKeep ?? 7 // 7 days - const logDirectory = path.join(getJanDataFolderPath(), 'logs') - // Perform log cleaning - const currentDate = new Date() - if (existsSync(logDirectory)) - readdir(logDirectory, (err, files) => { - if (err) { - console.error('Error reading log directory:', err) - return - } - - files.forEach((file) => { - const filePath = path.join(logDirectory, file) - stat(filePath, (err, stats) => { - if (err) { - console.error('Error getting file stats:', err) - return - } - - // Check size - if (stats.size > size) { - unlink(filePath, (err) => { - if (err) { - console.error('Error deleting log file:', err) - return - } - console.debug( - `Deleted log file due to exceeding size limit: ${filePath}` - ) - }) - } else { - // Check age - const creationDate = new Date(stats.ctime) - const daysDifference = Math.floor( - (currentDate.getTime() - creationDate.getTime()) / - (1000 * 3600 * 24) - ) - if (daysDifference > days) { - unlink(filePath, (err) => { - if (err) { - console.error('Error deleting log file:', err) - return - } - console.debug(`Deleted old log file: ${filePath}`) - }) - } - } - }) - }) - }) - - // Schedule the next execution with doubled delays - this.timeout = setTimeout( - () => this.cleanLogs(maxFileSizeBytes, daysToKeep), - this.logCleaningInterval - ) - } -} - -/** - * Write log function implementation - * @param message - * @param logPath - */ -const writeLog = (message: string, logPath: string) => { - if (!existsSync(logPath)) { - const logDirectory = path.join(getJanDataFolderPath(), 'logs') - if (!existsSync(logDirectory)) { - mkdirSync(logDirectory) - } - writeFileSync(logPath, message) - } else { - const logFile = createWriteStream(logPath, { - flags: 'a', - }) - logFile.write(util.format(message) + '\n') - logFile.close() - console.debug(message) - } -} - -/** - * Register logger for global application logging - */ -export const registerLogger = () => { - const logger = new FileLogger() - LoggerManager.instance().register(logger) - logger.cleanLogs() -} diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts deleted file mode 100644 index bab70da79..000000000 --- a/electron/utils/menu.ts +++ /dev/null @@ -1,129 +0,0 @@ -// @ts-nocheck -import { app, Menu, shell, dialog } from 'electron' -import { autoUpdater } from 'electron-updater' -import { log } from '@janhq/core/node' -const isMac = process.platform === 'darwin' -import { windowManager } from '../managers/window' - -const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [ - { - label: app.name, - submenu: [ - { - label: `About ${app.name}`, - click: () => - dialog.showMessageBox({ - title: `Jan`, - message: `Jan Version v${app.getVersion()}\n\nCopyright © 2024 Jan`, - }), - }, - { - label: 'Check for Updates...', - click: () => - // Check for updates and notify user if there are any - autoUpdater - .checkForUpdatesAndNotify() - .then((updateCheckResult) => { - if ( - !updateCheckResult?.updateInfo || - updateCheckResult?.updateInfo.version === app.getVersion() - ) { - windowManager.mainWindow?.webContents.send( - AppEvent.onAppUpdateNotAvailable, - {} - ) - return - } - }) - .catch((error) => { - log('Error checking for updates:' + JSON.stringify(error)) - }), - }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - { - label: `Settings`, - accelerator: 'CmdOrCtrl+,', - click: () => { - windowManager.showMainWindow() - windowManager.sendMainViewState('Settings') - }, - }, - { type: 'separator' }, - { role: 'quit' }, - ], - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - ...(isMac - ? [ - { role: 'pasteAndMatchStyle' }, - { role: 'delete' }, - { role: 'selectAll' }, - { type: 'separator' }, - { - label: 'Speech', - submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }], - }, - ] - : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]), - ], - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { role: 'forceReload' }, - { role: 'toggleDevTools' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ], - }, - { - label: 'Window', - submenu: [ - { role: 'minimize' }, - { role: 'zoom' }, - ...(isMac - ? [ - { type: 'separator' }, - { role: 'front' }, - { type: 'separator' }, - { role: 'window' }, - ] - : [{ role: 'close' }]), - ], - }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click: async () => { - await shell.openExternal('https://jan.ai/guides/') - }, - }, - ], - }, -] - -export const menu = Menu.buildFromTemplate(template) - -export const setupMenu = () => { - Menu.setApplicationMenu(menu) -} diff --git a/electron/utils/migration.ts b/electron/utils/migration.ts deleted file mode 100644 index 505de0f7b..000000000 --- a/electron/utils/migration.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { app } from 'electron' - -import { join } from 'path' -import { - rmdirSync, - existsSync, - mkdirSync, - readdirSync, - cpSync, - lstatSync, -} from 'fs' -import Store from 'electron-store' -import { - getJanDataFolderPath, - appResourcePath, - getJanExtensionsPath, -} from '@janhq/core/node' - -/** - * Migrates the extensions & themes. - * If the `migrated_version` key in the `Store` object does not match the current app version, - * the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version. - * @returns A Promise that resolves when the migration is complete. - */ -export async function migrate() { - const store = new Store() - if (store.get('migrated_version') !== app.getVersion()) { - console.debug('start migration:', store.get('migrated_version')) - - if (existsSync(getJanExtensionsPath())) - rmdirSync(getJanExtensionsPath(), { recursive: true }) - - await migrateThemes() - - store.set('migrated_version', app.getVersion()) - console.debug('migrate extensions done') - } else if (!existsSync(join(getJanDataFolderPath(), 'themes'))) { - await migrateThemes() - } -} - -async function migrateThemes() { - if (!existsSync(join(getJanDataFolderPath(), 'themes'))) - mkdirSync(join(getJanDataFolderPath(), 'themes'), { recursive: true }) - - const themes = readdirSync(join(appResourcePath(), 'themes')) - for (const theme of themes) { - const themePath = join(appResourcePath(), 'themes', theme) - await checkAndMigrateTheme(theme, themePath) - } -} - -async function checkAndMigrateTheme( - sourceThemeName: string, - sourceThemePath: string -) { - const janDataThemesFolder = join(getJanDataFolderPath(), 'themes') - const existingTheme = readdirSync(janDataThemesFolder).find( - (theme) => theme === sourceThemeName - ) - if (existingTheme) { - const desTheme = join(janDataThemesFolder, existingTheme) - if (!lstatSync(desTheme).isDirectory()) { - return - } - console.debug('Updating theme', existingTheme) - rmdirSync(desTheme, { recursive: true }) - cpSync(sourceThemePath, join(janDataThemesFolder, sourceThemeName), { - recursive: true, - }) - } else { - console.debug('Adding new theme', sourceThemeName) - cpSync(sourceThemePath, join(janDataThemesFolder, sourceThemeName), { - recursive: true, - }) - } -} diff --git a/electron/utils/path.ts b/electron/utils/path.ts deleted file mode 100644 index 4438156bc..000000000 --- a/electron/utils/path.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { mkdir } from 'fs-extra' -import { existsSync } from 'fs' -import { getJanDataFolderPath } from '@janhq/core/node' - -export async function createUserSpace(): Promise { - const janDataFolderPath = getJanDataFolderPath() - if (!existsSync(janDataFolderPath)) { - try { - await mkdir(janDataFolderPath) - } catch (err) { - console.error( - `Unable to create Jan data folder at ${janDataFolderPath}: ${err}` - ) - } - } -} diff --git a/electron/utils/selectedText.ts b/electron/utils/selectedText.ts deleted file mode 100644 index 51b2eb762..000000000 --- a/electron/utils/selectedText.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { clipboard, globalShortcut } from 'electron' -import { keyboard, Key } from "@kirillvakalov/nut-tree__nut-js" - -/** - * Gets selected text by synthesizing the keyboard shortcut - * "CommandOrControl+c" then reading text from the clipboard - */ -export const getSelectedText = async () => { - const currentClipboardContent = clipboard.readText() // preserve clipboard content - clipboard.clear() - const hotkeys: Key[] = [ - process.platform === 'darwin' ? Key.LeftCmd : Key.LeftControl, - Key.C, - ] - await keyboard.pressKey(...hotkeys) - await keyboard.releaseKey(...hotkeys) - await new Promise((resolve) => setTimeout(resolve, 200)) // add a delay before checking clipboard - const selectedText = clipboard.readText() - clipboard.writeText(currentClipboardContent) - return selectedText -} - -/** - * Registers a global shortcut of `accelerator`. The `callback` is called - * with the selected text when the registered shortcut is pressed by the user - * - * Returns `true` if the shortcut was registered successfully - */ -export const registerShortcut = ( - accelerator: Electron.Accelerator, - callback: (selectedText: string) => void -) => { - return globalShortcut.register(accelerator, async () => { - callback(await getSelectedText()) - }) -} - -/** - * Unregisters a global shortcut of `accelerator` and - * is equivalent to electron.globalShortcut.unregister - */ -export const unregisterShortcut = (accelerator: Electron.Accelerator) => { - globalShortcut.unregister(accelerator) -} diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts deleted file mode 100644 index 39b8a4133..000000000 --- a/electron/utils/setup.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { app, screen } from 'electron' -import Store from 'electron-store' - -const DEFAULT_WIDTH = 1000 -const DEFAULT_HEIGHT = 800 - -const storage = new Store() - -export const setupCore = async () => { - // Setup core api for main process - global.core = { - // Define appPath function for app to retrieve app path globally - appPath: () => app.getPath('userData'), - } -} - -export const getBounds = async () => { - const defaultBounds = { - x: undefined, - y: undefined, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - } - - const bounds = (await storage.get('windowBounds')) as - | Electron.Rectangle - | undefined - - // If no bounds are saved, use the defaults - if (!bounds) { - storage.set('windowBounds', defaultBounds) - return defaultBounds - } - - // Validate that the bounds are on a valid display - const displays = screen.getAllDisplays() - const isValid = displays.some((display) => { - const { x, y, width, height } = display.bounds - return ( - bounds.x >= x && - bounds.x < x + width && - bounds.y >= y && - bounds.y < y + height - ) - }) - - // If the position is valid, return the saved bounds, otherwise return default bounds - if (isValid) { - return bounds - } else { - const primaryDisplay = screen.getPrimaryDisplay() - const resetBounds = { - x: primaryDisplay.bounds.x, - y: primaryDisplay.bounds.y, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - } - storage.set('windowBounds', resetBounds) - return resetBounds - } -} - -export const saveBounds = (bounds: Electron.Rectangle | undefined) => { - storage.set('windowBounds', bounds) -} diff --git a/electron/utils/shortcut.ts b/electron/utils/shortcut.ts deleted file mode 100644 index aa4607d9a..000000000 --- a/electron/utils/shortcut.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getAppConfigurations } from '@janhq/core/node' -import { registerShortcut } from './selectedText' -import { windowManager } from '../managers/window' -// TODO: Retrieve from config later -const quickAskHotKey = 'CommandOrControl+J' - -export function registerGlobalShortcuts() { - if (!getAppConfigurations().quick_ask) return - const ret = registerShortcut(quickAskHotKey, (selectedText: string) => { - // Feature Toggle for Quick Ask - if (!windowManager.isQuickAskWindowVisible()) { - windowManager.showQuickAskWindow() - windowManager.sendQuickAskSelectedText(selectedText) - } else { - windowManager.hideQuickAskWindow() - } - }) - - if (!ret) { - console.error('Global shortcut registration failed') - } else { - console.log('Global shortcut registered successfully') - } -} diff --git a/electron/utils/system.ts b/electron/utils/system.ts deleted file mode 100644 index 5799de861..000000000 --- a/electron/utils/system.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { log } from '@janhq/core/node' -import { app } from 'electron' -import os from 'os' - -export const logSystemInfo = (): void => { - log(`[SPECS]::Version: ${app.getVersion()}`) - log(`[SPECS]::CPUs: ${JSON.stringify(os.cpus())}`) - log(`[SPECS]::Machine: ${os.machine()}`) - log(`[SPECS]::Endianness: ${os.endianness()}`) - log(`[SPECS]::Parallelism: ${os.availableParallelism()}`) - log(`[SPECS]::Free Mem: ${os.freemem()}`) - log(`[SPECS]::Total Mem: ${os.totalmem()}`) - log(`[SPECS]::OS Version: ${os.version()}`) - log(`[SPECS]::OS Platform: ${os.platform()}`) - log(`[SPECS]::OS Release: ${os.release()}`) -} diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 9ce48da88..153c22fdf 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -2,7 +2,7 @@ "name": "@janhq/model-extension", "productName": "Model Management", "version": "1.0.36", - "description": "Handles model list, and settings.", + "description": "Manages model operations including listing, importing, updating, and deleting.", "main": "dist/index.js", "author": "Jan ", "license": "AGPL-3.0", diff --git a/joi/.prettierignore b/joi/.prettierignore deleted file mode 100644 index e9e840d7e..000000000 --- a/joi/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -.next/ -node_modules/ -dist/ -*.hbs -*.mdx -*.mjs \ No newline at end of file diff --git a/joi/.prettierrc b/joi/.prettierrc deleted file mode 100644 index 933d88d62..000000000 --- a/joi/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "quoteProps": "consistent", - "trailingComma": "es5", - "endOfLine": "lf", - "plugins": ["prettier-plugin-tailwindcss"] -} diff --git a/joi/README.md b/joi/README.md deleted file mode 100644 index 161db4156..000000000 --- a/joi/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# @janhq/joi - -To install dependencies: - -```bash -yarn install -``` - -To run: - -```bash -yarn run dev -``` diff --git a/joi/jest.config.js b/joi/jest.config.js deleted file mode 100644 index 676042491..000000000 --- a/joi/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/src'], - testMatch: ['**/*.test.*'], - collectCoverageFrom: ['src/**/*.{ts,tsx}'], - setupFilesAfterEnv: ['/jest.setup.js'], - testEnvironment: 'jsdom', -} diff --git a/joi/jest.setup.js b/joi/jest.setup.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/joi/package.json b/joi/package.json deleted file mode 100644 index 2f7d771d5..000000000 --- a/joi/package.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "name": "@janhq/joi", - "version": "0.0.0", - "main": "dist/esm/index.js", - "types": "dist/index.d.ts", - "description": "A collection of UI component", - "files": [ - "dist" - ], - "keywords": [ - "design-system" - ], - "license": "MIT", - "homepage": "https://github.com/codecentrum/piksel#readme", - "repository": { - "type": "git", - "url": "https://github.com/codecentrum/piksel.git" - }, - "bugs": "https://github.com/codecentrum/piksel/issues", - "scripts": { - "dev": "rollup -c -w", - "build": "rimraf ./dist || true && rollup -c", - "test": "jest" - }, - "peerDependencies": { - "class-variance-authority": "^0.7.0", - "react": "^18", - "typescript": "^5.0.0" - }, - "dependencies": { - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.1.4", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-tooltip": "^1.0.7", - "@types/jest": "^29.5.12", - "autoprefixer": "10.4.16", - "jest": "^29.7.0", - "tailwind-merge": "^2.2.0", - "tailwindcss": "^3.4.1", - "ts-jest": "^29.2.5" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-terser": "0.4.4", - "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "^6.5.0", - "@testing-library/react": "^16.0.1", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.12", - "@types/react": "^18.3.12", - "@types/react-dom": "^19", - "class-variance-authority": "^0.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-transform-css": "^6.0.1", - "prettier": "^3.0.3", - "prettier-plugin-tailwindcss": "^0.5.6", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "rimraf": "^6.0.1", - "rollup": "4.12.0", - "rollup-plugin-bundle-size": "1.0.3", - "rollup-plugin-commonjs": "10.1.0", - "rollup-plugin-copy": "3.5.0", - "rollup-plugin-dts": "6.1.0", - "rollup-plugin-peer-deps-external": "2.2.4", - "rollup-plugin-postcss": "4.0.2", - "rollup-plugin-typescript2": "0.36.0", - "sass": "^1.83.1", - "typescript": "^5.7.2" - }, - "packageManager": "yarn@4.5.3" -} diff --git a/joi/rollup.config.mjs b/joi/rollup.config.mjs deleted file mode 100644 index 6577135dc..000000000 --- a/joi/rollup.config.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { readFileSync } from 'fs' -import dts from 'rollup-plugin-dts' -import terser from '@rollup/plugin-terser' -import autoprefixer from 'autoprefixer' -import commonjs from 'rollup-plugin-commonjs' -import bundleSize from 'rollup-plugin-bundle-size' -import peerDepsExternal from 'rollup-plugin-peer-deps-external' -import postcss from 'rollup-plugin-postcss' -import typescript from 'rollup-plugin-typescript2' -import tailwindcss from 'tailwindcss' -import typescriptEngine from 'typescript' -import resolve from '@rollup/plugin-node-resolve' -import copy from 'rollup-plugin-copy' - -const packageJson = JSON.parse(readFileSync('./package.json')) - -import tailwindConfig from './tailwind.config.js' - -export default [ - { - input: `./src/index.ts`, - output: [ - { - file: packageJson.main, - format: 'es', - exports: 'named', - sourcemap: false, - }, - ], - external: ['react', 'typescript', 'class-variance-authority'], - plugins: [ - postcss({ - plugins: [autoprefixer(), tailwindcss(tailwindConfig)], - sourceMap: true, - use: { - sass: { - silenceDeprecations: ['legacy-js-api'], - api: 'modern', - }, - }, - minimize: true, - extract: 'main.css', - }), - - peerDepsExternal({ includeDependencies: true }), - commonjs(), - resolve(), - typescript({ - tsconfig: './tsconfig.json', - typescript: typescriptEngine, - sourceMap: false, - exclude: [ - 'docs', - 'dist', - 'node_modules/**', - '**/*.test.ts', - '**/*.test.tsx', - ], - }), - terser(), - ], - watch: { - clearScreen: false, - }, - }, - { - input: 'dist/esm/index.d.ts', - output: [{ file: 'dist/index.d.ts', format: 'esm' }], - external: [/\.(sc|sa|c)ss$/], - plugins: [ - dts(), - peerDepsExternal({ includeDependencies: true }), - copy({ - targets: [{ src: 'dist/esm/main.css', dest: 'dist' }], - }), - bundleSize(), - ], - }, -] diff --git a/joi/src/core/Accordion/Accordion.test.tsx b/joi/src/core/Accordion/Accordion.test.tsx deleted file mode 100644 index 62b575ea3..000000000 --- a/joi/src/core/Accordion/Accordion.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react' -import '@testing-library/jest-dom' -import { render, screen, fireEvent } from '@testing-library/react' -import { Accordion, AccordionItem } from './index' - -// Mock the SCSS import -jest.mock('./styles.scss', () => ({})) - -describe('Accordion', () => { - it('renders accordion with items', () => { - render( - - - Content 1 - - - Content 2 - - - ) - - expect(screen.getByText('Item 1')).toBeInTheDocument() - expect(screen.getByText('Item 2')).toBeInTheDocument() - }) - - it('expands and collapses accordion items', () => { - render( - - - Content 1 - - - ) - - const trigger = screen.getByText('Item 1') - - // Initially, content should not be visible - expect(screen.queryByText('Content 1')).not.toBeInTheDocument() - - // Click to expand - fireEvent.click(trigger) - expect(screen.getByText('Content 1')).toBeInTheDocument() - - // Click to collapse - fireEvent.click(trigger) - expect(screen.queryByText('Content 1')).not.toBeInTheDocument() - }) - - it('respects defaultValue prop', () => { - render( - - - Content 1 - - - Content 2 - - - ) - - expect(screen.queryByText('Content 1')).not.toBeInTheDocument() - expect(screen.getByText('Content 2')).toBeInTheDocument() - }) -}) diff --git a/joi/src/core/Accordion/index.tsx b/joi/src/core/Accordion/index.tsx deleted file mode 100644 index 75a671ca4..000000000 --- a/joi/src/core/Accordion/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { ReactNode } from 'react' -import * as AccordionPrimitive from '@radix-ui/react-accordion' - -import { ChevronDownIcon } from '@radix-ui/react-icons' - -import './styles.scss' - -type AccordionProps = { - defaultValue: string[] - children: ReactNode -} - -type AccordionItemProps = { - children: ReactNode - value: string - title: string -} - -const AccordionItem = ({ children, value, title }: AccordionItemProps) => { - return ( - - - -
{title}
- -
-
- -
{children}
-
-
- ) -} - -const Accordion = ({ defaultValue, children }: AccordionProps) => ( - - {children} - -) - -export { Accordion, AccordionItem } diff --git a/joi/src/core/Accordion/styles.scss b/joi/src/core/Accordion/styles.scss deleted file mode 100644 index 028cc021c..000000000 --- a/joi/src/core/Accordion/styles.scss +++ /dev/null @@ -1,73 +0,0 @@ -.accordion { - border-top: 1px solid hsla(var(--app-border)); - - &__item { - overflow: hidden; - margin-top: 1px; - border-bottom: 1px solid hsla(var(--app-border)); - - :focus-within { - position: relative; - z-index: 1; - } - } - - &__header { - display: flex; - } - - &__trigger { - font-family: inherit; - background-color: transparent; - padding: 0 16px; - height: 40px; - flex: 1; - display: flex; - align-items: center; - justify-content: space-between; - font-weight: 500; - } - - &__content { - overflow: hidden; - - &--wrapper { - padding: 4px 16px 16px 16px; - } - } - - &__chevron { - color: hsla(var(--text-secondary)); - transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1); - } -} - -.accordion__content[data-state='open'] { - animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1); -} - -.accordion__content[data-state='closed'] { - animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); -} - -.accordion__trigger[data-state='open'] > .accordion__chevron { - transform: rotate(180deg); -} - -@keyframes slideDown { - from { - height: 0; - } - to { - height: var(--radix-accordion-content-height); - } -} - -@keyframes slideUp { - from { - height: var(--radix-accordion-content-height); - } - to { - height: 0; - } -} diff --git a/joi/src/core/Badge/Badge.test.tsx b/joi/src/core/Badge/Badge.test.tsx deleted file mode 100644 index 1d3192be7..000000000 --- a/joi/src/core/Badge/Badge.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Badge, badgeConfig } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Badge', () => { - it('renders with default props', () => { - render(Test Badge) - const badge = screen.getByText('Test Badge') - expect(badge).toBeInTheDocument() - expect(badge).toHaveClass('badge') - expect(badge).toHaveClass('badge--primary') - expect(badge).toHaveClass('badge--medium') - expect(badge).toHaveClass('badge--solid') - }) - - it('applies custom className', () => { - render(Test Badge) - const badge = screen.getByText('Test Badge') - expect(badge).toHaveClass('custom-class') - }) - - it('renders with different themes', () => { - const themes = Object.keys(badgeConfig.variants.theme) - themes.forEach((theme) => { - render(Test Badge {theme}) - const badge = screen.getByText(`Test Badge ${theme}`) - expect(badge).toHaveClass(`badge--${theme}`) - }) - }) - - it('renders with different variants', () => { - const variants = Object.keys(badgeConfig.variants.variant) - variants.forEach((variant) => { - render(Test Badge {variant}) - const badge = screen.getByText(`Test Badge ${variant}`) - expect(badge).toHaveClass(`badge--${variant}`) - }) - }) - - it('renders with different sizes', () => { - const sizes = Object.keys(badgeConfig.variants.size) - sizes.forEach((size) => { - render(Test Badge {size}) - const badge = screen.getByText(`Test Badge ${size}`) - expect(badge).toHaveClass(`badge--${size}`) - }) - }) - - it('fails when a new theme is added without updating the test', () => { - const expectedThemes = [ - 'primary', - 'secondary', - 'warning', - 'success', - 'info', - 'destructive', - ] - const actualThemes = Object.keys(badgeConfig.variants.theme) - expect(actualThemes).toEqual(expectedThemes) - }) - - it('fails when a new variant is added without updating the test', () => { - const expectedVariant = ['solid', 'soft', 'outline'] - const actualVariants = Object.keys(badgeConfig.variants.variant) - expect(actualVariants).toEqual(expectedVariant) - }) - - it('fails when a new size is added without updating the test', () => { - const expectedSizes = ['small', 'medium', 'large'] - const actualSizes = Object.keys(badgeConfig.variants.size) - expect(actualSizes).toEqual(expectedSizes) - }) - - it('fails when a new variant CVA is added without updating the test', () => { - const expectedVariantsCVA = ['theme', 'variant', 'size'] - const actualVariant = Object.keys(badgeConfig.variants) - expect(actualVariant).toEqual(expectedVariantsCVA) - }) -}) diff --git a/joi/src/core/Badge/index.tsx b/joi/src/core/Badge/index.tsx deleted file mode 100644 index 5aeb19631..000000000 --- a/joi/src/core/Badge/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { HTMLAttributes } from 'react' - -import { cva, type VariantProps } from 'class-variance-authority' - -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -export const badgeConfig = { - variants: { - theme: { - primary: 'badge--primary', - secondary: 'badge--secondary', - warning: 'badge--warning', - success: 'badge--success', - info: 'badge--info', - destructive: 'badge--destructive', - }, - variant: { - solid: 'badge--solid', - soft: 'badge--soft', - outline: 'badge--outline', - }, - size: { - small: 'badge--small', - medium: 'badge--medium', - large: 'badge--large', - }, - }, - defaultVariants: { - theme: 'primary' as const, - size: 'medium' as const, - variant: 'solid' as const, - }, -} - -const badgeVariants = cva('badge', badgeConfig) - -export interface BadgeProps - extends HTMLAttributes, - VariantProps {} - -const Badge = ({ className, theme, size, variant, ...props }: BadgeProps) => { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/joi/src/core/Badge/styles.scss b/joi/src/core/Badge/styles.scss deleted file mode 100644 index a912e9216..000000000 --- a/joi/src/core/Badge/styles.scss +++ /dev/null @@ -1,131 +0,0 @@ -.badge { - @apply inline-flex items-center justify-center px-2 font-medium transition-all; - - // Primary - &--primary { - color: hsla(var(--primary-fg)); - background-color: hsla(var(--primary-bg)); - - // Variant soft primary - &.badge--soft { - background-color: hsla(var(--primary-bg-soft)); - color: hsla(var(--primary-bg)); - } - - // Variant outline primary - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--primary-bg)); - color: hsla(var(--primary-bg)); - } - } - - // Secondary - &--secondary { - background-color: hsla(var(--secondary-bg)); - color: hsla(var(--secondary-fg)); - - &.badge--soft { - background-color: hsla(var(--secondary-bg-soft)); - color: hsla(var(--secondary-bg)); - } - - // Variant outline secondary - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--secondary-bg)); - } - } - - // Destructive - &--destructive { - color: hsla(var(--destructive-fg)); - background-color: hsla(var(--destructive-bg)); - - // Variant soft destructive - &.badge--soft { - background-color: hsla(var(--destructive-bg-soft)); - color: hsla(var(--destructive-bg)); - } - - // Variant outline destructive - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--destructive-bg)); - color: hsla(var(--destructive-bg)); - } - } - - // Success - &--success { - @apply text-white; - background-color: hsla(var(--success-bg)); - - // Variant soft success - &.badge--soft { - background-color: hsla(var(--success-bg-soft)); - color: hsla(var(--success-bg)); - } - - // Variant outline success - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--success-bg)); - color: hsla(var(--success-bg)); - } - } - - // Warning - &--warning { - @apply text-white; - background-color: hsla(var(--warning-bg)); - - // Variant soft warning - &.badge--soft { - background-color: hsla(var(--warning-bg-soft)); - color: hsla(var(--warning-bg)); - } - - // Variant outline warning - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--warning-bg)); - color: hsla(var(--warning-bg)); - } - } - - // Info - &--info { - @apply text-white; - background-color: hsla(var(--info-bg)); - - // Variant soft info - &.badge--soft { - background-color: hsla(var(--info-bg-soft)); - color: hsla(var(--info-bg)); - } - - // Variant outline info - &.badge--outline { - background-color: transparent; - border: 1px solid hsla(var(--info-bg)); - color: hsla(var(--info-bg)); - } - } - - // Size - &--small { - @apply h-5; - border-radius: 4px; - } - - &--medium { - @apply h-6; - border-radius: 6px; - } - - &--large { - @apply h-7; - border-radius: 8px; - } -} diff --git a/joi/src/core/Button/Button.test.tsx b/joi/src/core/Button/Button.test.tsx deleted file mode 100644 index a4c679773..000000000 --- a/joi/src/core/Button/Button.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Button, buttonConfig } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Button', () => { - it('renders with default props', () => { - render() - const button = screen.getByRole('button', { name: /click me/i }) - expect(button).toBeInTheDocument() - expect(button).toHaveClass('btn btn--primary btn--medium btn--solid') - }) - - it('applies custom className', () => { - render() - const badge = screen.getByText('Test Button') - expect(badge).toHaveClass('custom-class') - }) - - it('renders as a child component when asChild is true', () => { - render( - - ) - const link = screen.getByRole('link', { name: /link button/i }) - expect(link).toBeInTheDocument() - expect(link).toHaveClass('btn btn--primary btn--medium btn--solid') - }) - - it.each(Object.keys(buttonConfig.variants.theme))( - 'renders with theme %s', - (theme) => { - render() - const button = screen.getByRole('button', { name: /theme button/i }) - expect(button).toHaveClass(`btn btn--${theme}`) - } - ) - - it.each(Object.keys(buttonConfig.variants.variant))( - 'renders with variant %s', - (variant) => { - render() - const button = screen.getByRole('button', { name: /variant button/i }) - expect(button).toHaveClass(`btn btn--${variant}`) - } - ) - - it.each(Object.keys(buttonConfig.variants.size))( - 'renders with size %s', - (size) => { - render() - const button = screen.getByRole('button', { name: /size button/i }) - expect(button).toHaveClass(`btn btn--${size}`) - } - ) - - it('renders with block prop', () => { - render() - const button = screen.getByRole('button', { name: /block button/i }) - expect(button).toHaveClass('btn btn--block') - }) - - it('fails when a new theme is added without updating the test', () => { - const expectedThemes = ['primary', 'ghost', 'icon', 'destructive'] - const actualThemes = Object.keys(buttonConfig.variants.theme) - expect(actualThemes).toEqual(expectedThemes) - }) - - it('fails when a new variant is added without updating the test', () => { - const expectedVariant = ['solid', 'soft', 'outline'] - const actualVariants = Object.keys(buttonConfig.variants.variant) - expect(actualVariants).toEqual(expectedVariant) - }) - - it('fails when a new size is added without updating the test', () => { - const expectedSizes = ['small', 'medium', 'large'] - const actualSizes = Object.keys(buttonConfig.variants.size) - expect(actualSizes).toEqual(expectedSizes) - }) - - it('fails when a new variant CVA is added without updating the test', () => { - const expectedVariantsCVA = ['theme', 'variant', 'size', 'block'] - const actualVariant = Object.keys(buttonConfig.variants) - expect(actualVariant).toEqual(expectedVariantsCVA) - }) -}) diff --git a/joi/src/core/Button/index.tsx b/joi/src/core/Button/index.tsx deleted file mode 100644 index 9945eb4e9..000000000 --- a/joi/src/core/Button/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { forwardRef, ButtonHTMLAttributes } from 'react' - -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' - -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -export const buttonConfig = { - variants: { - theme: { - primary: 'btn--primary', - ghost: 'btn--ghost', - icon: 'btn--icon', - destructive: 'btn--destructive', - }, - variant: { - solid: 'btn--solid', - soft: 'btn--soft', - outline: 'btn--outline', - }, - size: { - small: 'btn--small', - medium: 'btn--medium', - large: 'btn--large', - }, - block: { - true: 'btn--block', - }, - }, - defaultVariants: { - theme: 'primary' as const, - size: 'medium' as const, - variant: 'solid' as const, - block: false as const, - }, -} -const buttonVariants = cva('btn', buttonConfig) - -export interface ButtonProps - extends ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = forwardRef( - ( - { className, theme, size, variant, block, asChild = false, ...props }, - ref - ) => { - const Comp = asChild ? Slot : 'button' - return ( - - ) - } -) - -export { Button } diff --git a/joi/src/core/Button/styles.scss b/joi/src/core/Button/styles.scss deleted file mode 100644 index f7cdce6a4..000000000 --- a/joi/src/core/Button/styles.scss +++ /dev/null @@ -1,134 +0,0 @@ -.btn { - @apply inline-flex items-center justify-center px-4 font-semibold transition-all; - - &:focus, - &:focus-within { - @apply outline-2 outline-offset-4; - } - &:hover { - filter: brightness(95%); - } - - // Primary - &--primary { - color: hsla(var(--primary-fg)); - background-color: hsla(var(--primary-bg)) !important; - &:hover { - filter: brightness(65%); - } - - // Variant soft primary - &.btn--soft { - background-color: hsla(var(--primary-bg-soft)) !important; - color: hsla(var(--primary-bg)); - } - - // Variant outline primary - &.btn--outline { - background-color: transparent !important; - border: 1px solid hsla(var(--primary-bg)); - color: hsla(var(--primary-bg)); - } - } - - // Ghost - &--ghost { - background-color: transparent !important; - &.btn--soft { - background-color: transparent !important; - } - - // Variant outline ghost - &.btn--outline { - background-color: transparent !important; - border: 1px solid hsla(var(--ghost-border)); - } - } - - // Destructive - &--destructive { - color: hsla(var(--destructive-fg)); - background-color: hsla(var(--destructive-bg)) !important; - &:hover { - filter: brightness(65%); - } - - // Variant soft destructive - &.btn--soft { - background-color: hsla(var(--destructive-bg-soft)) !important; - color: hsla(var(--destructive-bg)); - } - - // Variant outline destructive - &.btn--outline { - background-color: transparent !important; - border: 1px solid hsla(var(--destructive-bg)); - color: hsla(var(--destructive-bg)); - } - } - - // Disabled - &:disabled { - color: hsla(var(--disabled-fg)); - background-color: hsla(var(--disabled-bg)) !important; - cursor: not-allowed; - - &:hover { - filter: brightness(100%); - } - } - - // Icon - &--icon { - width: 24px; - height: 24px; - padding: 2px; - &:hover { - background-color: hsla(var(--icon-bg)) !important; - } - - &.btn--outline { - background-color: transparent !important; - border: 1px solid hsla(var(--icon-border)); - &:hover { - background-color: hsla(var(--icon-bg)) !important; - } - } - } - - // Size - &--small { - @apply h-6 px-2; - font-size: 12px; - border-radius: 4px; - &.btn--icon { - width: 24px; - height: 24px; - padding: 2px; - } - } - - &--medium { - @apply h-8; - border-radius: 6px; - &.btn--icon { - width: 24px; - height: 24px; - padding: 2px; - } - } - - &--large { - @apply h-9; - border-radius: 8px; - &.btn--icon { - width: 24px; - height: 24px; - padding: 2px; - } - } - - &--block { - @apply w-full; - } -} diff --git a/joi/src/core/Checkbox/Checkbox.test.tsx b/joi/src/core/Checkbox/Checkbox.test.tsx deleted file mode 100644 index ce81132d9..000000000 --- a/joi/src/core/Checkbox/Checkbox.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Checkbox } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Checkbox', () => { - it('renders correctly with label', () => { - render() - expect(screen.getByLabelText('Test Checkbox')).toBeInTheDocument() - }) - - it('renders with helper description', () => { - render() - expect(screen.getByText('Helper text')).toBeInTheDocument() - }) - - it('renders error message when provided', () => { - render() - expect(screen.getByText('Error occurred')).toBeInTheDocument() - }) - - it('calls onChange when clicked', () => { - const mockOnChange = jest.fn() - render( - - ) - - fireEvent.click(screen.getByLabelText('Test Checkbox')) - expect(mockOnChange).toHaveBeenCalledTimes(1) - }) - - it('applies custom className', () => { - render() - expect(screen.getByRole('checkbox').parentElement).toHaveClass( - 'custom-class' - ) - }) - - it('disables the checkbox when disabled prop is true', () => { - render() - expect(screen.getByLabelText('Disabled Checkbox')).toBeDisabled() - }) -}) diff --git a/joi/src/core/Checkbox/index.tsx b/joi/src/core/Checkbox/index.tsx deleted file mode 100644 index 71f9523ac..000000000 --- a/joi/src/core/Checkbox/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react' - -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -export interface CheckboxProps extends InputHTMLAttributes { - disabled?: boolean - className?: string - label?: ReactNode - helperDescription?: ReactNode - errorMessage?: string - onChange?: (e: ChangeEvent) => void -} - -const Checkbox = ({ - id, - name, - checked, - disabled, - label, - defaultChecked, - helperDescription, - errorMessage, - className, - onChange, - ...props -}: CheckboxProps) => { - return ( -
- -
- -

{helperDescription}

- {errorMessage &&

{errorMessage}

} -
-
- ) -} -export { Checkbox } diff --git a/joi/src/core/Checkbox/styles.scss b/joi/src/core/Checkbox/styles.scss deleted file mode 100644 index 775a6289b..000000000 --- a/joi/src/core/Checkbox/styles.scss +++ /dev/null @@ -1,51 +0,0 @@ -.checkbox { - @apply inline-flex items-start space-x-2; - - > input[type='checkbox'] { - @apply flex h-4 w-4 flex-shrink-0 cursor-pointer appearance-none items-center justify-center; - background-color: transparent; - margin-top: 1px; - border: 1px solid hsla(var(--app-border)); - border-radius: 4px; - &:focus, - &:focus-within { - @apply outline-2 outline-offset-4; - } - - &:checked { - background-color: hsla(var(--primary-bg)); - background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E"); - } - - &:disabled { - background-color: hsla(var(----disabled-bg)); - color: hsla(var(--disabled-fg)); - - &:checked { - background-color: hsla(var(--primary-bg)); - @apply cursor-not-allowed opacity-50; - } - - & + div > .checkbox__label { - @apply cursor-not-allowed opacity-50; - } - } - } - - &__helper { - font-size: 12px; - } - - &__error { - color: hsla(var(--destructive-bg)); - } - - &__label { - @apply inline-block cursor-pointer; - } - - &:disabled { - background-color: hsla(var(----disabled-bg)); - color: hsla(var(--disabled-fg)); - } -} diff --git a/joi/src/core/Dropdown/index.tsx b/joi/src/core/Dropdown/index.tsx deleted file mode 100644 index 6d9abcbea..000000000 --- a/joi/src/core/Dropdown/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Fragment, PropsWithChildren, ReactNode } from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import './styles.scss' -import { twMerge } from 'tailwind-merge' - -type Props = { - options?: { name: ReactNode; value: string; suffix?: ReactNode }[] - className?: string - onValueChanged?: (value: string) => void -} - -const Dropdown = (props: PropsWithChildren & Props) => { - return ( - - {props.children} - - - - {props.options?.map((e, i) => ( - - {i !== 0 && ( - - )} - props.onValueChanged?.(e.value)} - > - {e.name} -
- {e.suffix} - - - ))} - - - - - ) -} - -export { Dropdown } diff --git a/joi/src/core/Dropdown/styles.scss b/joi/src/core/Dropdown/styles.scss deleted file mode 100644 index d472578df..000000000 --- a/joi/src/core/Dropdown/styles.scss +++ /dev/null @@ -1,155 +0,0 @@ -.DropdownMenuContent, -.DropdownMenuSubContent { - min-width: 220px; - background-color: white; - border-radius: 6px; - overflow: hidden; - padding: 0px; - box-shadow: - 0px 10px 38px -10px rgba(22, 23, 24, 0.35), - 0px 10px 20px -15px rgba(22, 23, 24, 0.2); - animation-duration: 400ms; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); - will-change: transform, opacity; -} -.DropdownMenuContent[data-side='top'], -.DropdownMenuSubContent[data-side='top'] { - animation-name: slideDownAndFade; -} -.DropdownMenuContent[data-side='right'], -.DropdownMenuSubContent[data-side='right'] { - animation-name: slideLeftAndFade; -} -.DropdownMenuContent[data-side='bottom'], -.DropdownMenuSubContent[data-side='bottom'] { - animation-name: slideUpAndFade; -} -.DropdownMenuContent[data-side='left'], -.DropdownMenuSubContent[data-side='left'] { - animation-name: slideRightAndFade; -} - -.DropdownMenuItem { - padding: 14px; - cursor: pointer; - outline: none; - flex: 1; - display: flex; - justify-content: space-between; /* Distribute space between children */ - align-items: center; /* Optional: Align items vertically */ - gap: 16px; - border-color: hsla(var(--app-border)); -} -.DropdownMenuCheckboxItem, -.DropdownMenuRadioItem, -.DropdownMenuSubTrigger { - font-size: 13px; - line-height: 1; - border-radius: 3px; - display: flex; - align-items: center; - height: 25px; - padding: 0 0; - position: relative; - padding-left: 25px; - user-select: none; - outline: none; -} -.DropdownMenuItem[data-disabled], -.DropdownMenuCheckboxItem[data-disabled], -.DropdownMenuRadioItem[data-disabled], -.DropdownMenuSubTrigger[data-disabled] { - pointer-events: none; -} -.DropdownMenuItem[data-highlighted], -.DropdownMenuCheckboxItem[data-highlighted], -.DropdownMenuRadioItem[data-highlighted], -.DropdownMenuSubTrigger[data-highlighted] { - background-color: hsla(var(--secondary-bg)); -} - -.DropdownMenuSeparator { - height: 1px; - width: '100%'; - background-color: hsla(var(--app-border)); -} - -.DropdownMenuItem::hover { - background-color: hsla(var(--secondary-bg)); -} - -.DropdownMenuLabel { - padding-left: 25px; - font-size: 12px; - line-height: 25px; - color: var(--mauve-11); -} - -.DropdownMenuItemIndicator { - position: absolute; - left: 0; - width: 25px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.DropdownMenuArrow { - fill: white; -} - -.RightSlot { - margin-left: auto; - padding-left: 20px; - color: var(--mauve-11); -} -[data-highlighted] > .RightSlot { - color: white; -} -[data-disabled] .RightSlot { - color: var(--mauve-8); -} - -@keyframes slideUpAndFade { - from { - opacity: 0; - transform: translateY(2px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideRightAndFade { - from { - opacity: 0; - transform: translateX(-2px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideDownAndFade { - from { - opacity: 0; - transform: translateY(-2px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideLeftAndFade { - from { - opacity: 0; - transform: translateX(2px); - } - to { - opacity: 1; - transform: translateX(0); - } -} diff --git a/joi/src/core/Input/Input.test.tsx b/joi/src/core/Input/Input.test.tsx deleted file mode 100644 index 55bed74bb..000000000 --- a/joi/src/core/Input/Input.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Input } from './index' - -// Mock the styles import -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Input', () => { - it('renders correctly', () => { - render() - expect(screen.getByPlaceholderText('Test input')).toBeInTheDocument() - }) - - it('applies custom className', () => { - render() - expect(screen.getByRole('textbox')).toHaveClass('custom-class') - }) - - it('aligns text to the right when textAlign prop is set', () => { - render() - expect(screen.getByRole('textbox')).toHaveClass('text-right') - }) - - it('renders prefix icon when provided', () => { - render(Prefix} />) - expect(screen.getByTestId('prefix-icon')).toBeInTheDocument() - }) - - it('renders suffix icon when provided', () => { - render(Suffix} />) - expect(screen.getByTestId('suffix-icon')).toBeInTheDocument() - }) - - it('renders clear icon when clearable is true', () => { - render() - expect(screen.getByTestId('cross-2-icon')).toBeInTheDocument() - }) - - it('calls onClick when input is clicked', () => { - const onClick = jest.fn() - render() - fireEvent.click(screen.getByRole('textbox')) - expect(onClick).toHaveBeenCalledTimes(1) - }) - - it('calls onClear when clear icon is clicked', () => { - const onClear = jest.fn() - render() - fireEvent.click(screen.getByTestId('cross-2-icon')) - expect(onClear).toHaveBeenCalledTimes(1) - }) -}) diff --git a/joi/src/core/Input/index.tsx b/joi/src/core/Input/index.tsx deleted file mode 100644 index 9f5e4c663..000000000 --- a/joi/src/core/Input/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { ReactNode, forwardRef } from 'react' -import { twMerge } from 'tailwind-merge' - -import './styles.scss' -import { Cross2Icon } from '@radix-ui/react-icons' - -export interface Props extends React.InputHTMLAttributes { - textAlign?: 'left' | 'right' - prefixIcon?: ReactNode - suffixIcon?: ReactNode - onCLick?: () => void - clearable?: boolean - onClear?: () => void -} - -const Input = forwardRef( - ( - { - className, - type, - textAlign, - prefixIcon, - suffixIcon, - onClick, - onClear, - clearable, - ...props - }, - ref - ) => { - return ( -
- {prefixIcon && ( -
- {prefixIcon} -
- )} - {suffixIcon && ( -
- {suffixIcon} -
- )} - {clearable && ( -
- -
- )} - -
- ) - } -) - -export { Input } diff --git a/joi/src/core/Input/styles.scss b/joi/src/core/Input/styles.scss deleted file mode 100644 index 540d880b1..000000000 --- a/joi/src/core/Input/styles.scss +++ /dev/null @@ -1,50 +0,0 @@ -.input { - background-color: hsla(var(--input-bg)); - border: 1px solid hsla(var(--app-border)); - @apply inline-flex h-8 w-full items-center rounded-md border px-3 transition-colors; - @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0; - @apply file:border-0 file:bg-transparent file:font-medium; - @apply hover:border-[hsla(var(--primary-bg))]; - - &__wrapper { - position: relative; - } - - &.text-right { - text-align: right; - } - - &::placeholder { - color: hsla(var(--input-placeholder)); - } - - &:disabled { - color: hsla(var(--disabled-fg)); - background-color: hsla(var(--disabled-bg)); - cursor: not-allowed; - border: none; - } - - &__prefix-icon { - @apply absolute left-3 top-1/2 -translate-y-1/2 cursor-pointer; - color: hsla(var(--input-icon)); - + .input { - padding-left: 32px; - } - } - - &__suffix-icon { - @apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer; - color: hsla(var(--input-icon)); - + .input { - padding-right: 32px; - } - } - &__clear-icon { - @apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer; - color: hsla(var(--input-icon)); - + .input { - padding: 0 32px; - } - } -} diff --git a/joi/src/core/Modal/Modal.test.tsx b/joi/src/core/Modal/Modal.test.tsx deleted file mode 100644 index fe7ca7eac..000000000 --- a/joi/src/core/Modal/Modal.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Modal } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('Modal', () => { - it('renders the modal with trigger and content', () => { - render( - Open Modal} - content={
Modal Content
} - /> - ) - - expect(screen.getByText('Open Modal')).toBeInTheDocument() - fireEvent.click(screen.getByText('Open Modal')) - expect(screen.getByText('Modal Content')).toBeInTheDocument() - }) - - it('renders the modal with title', () => { - render( - Open Modal} - content={
Modal Content
} - title="Modal Title" - /> - ) - - fireEvent.click(screen.getByText('Open Modal')) - expect(screen.getByText('Modal Title')).toBeInTheDocument() - }) - - it('renders full page modal', () => { - render( - Open Modal} - content={
Modal Content
} - fullPage - /> - ) - - fireEvent.click(screen.getByText('Open Modal')) - expect(screen.getByRole('dialog')).toHaveClass('modal__content--fullpage') - }) - - it('hides close button when hideClose is true', () => { - render( - Open Modal} - content={
Modal Content
} - hideClose - /> - ) - - fireEvent.click(screen.getByText('Open Modal')) - expect(screen.queryByLabelText('Close')).not.toBeInTheDocument() - }) - - it('calls onOpenChange when opening and closing the modal', () => { - const onOpenChangeMock = jest.fn() - render( - Open Modal} - content={
Modal Content
} - onOpenChange={onOpenChangeMock} - /> - ) - - fireEvent.click(screen.getByText('Open Modal')) - expect(onOpenChangeMock).toHaveBeenCalledWith(true) - - fireEvent.click(screen.getByLabelText('Close')) - expect(onOpenChangeMock).toHaveBeenCalledWith(false) - }) -}) diff --git a/joi/src/core/Modal/index.tsx b/joi/src/core/Modal/index.tsx deleted file mode 100644 index 7754fb3f0..000000000 --- a/joi/src/core/Modal/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { ReactNode } from 'react' -import * as DialogPrimitive from '@radix-ui/react-dialog' -import { Cross2Icon } from '@radix-ui/react-icons' - -import './styles.scss' -import { twMerge } from 'tailwind-merge' - -type Props = { - trigger?: ReactNode - content: ReactNode - open?: boolean - className?: string - fullPage?: boolean - hideClose?: boolean - title?: ReactNode - onOpenChange?: (open: boolean) => void -} - -const ModalClose = DialogPrimitive.Close - -const Modal = ({ - trigger, - content, - open, - title, - fullPage, - className, - onOpenChange, - hideClose, -}: Props) => ( - - {trigger} - - - - - {title} - - {content} - {!hideClose && ( - - - - )} - - - -) - -export { Modal, ModalClose } diff --git a/joi/src/core/Modal/styles.scss b/joi/src/core/Modal/styles.scss deleted file mode 100644 index 717ce2ac7..000000000 --- a/joi/src/core/Modal/styles.scss +++ /dev/null @@ -1,85 +0,0 @@ -/* reset */ -button, -fieldset, -.modal { - &__overlay { - background-color: hsla(var(--modal-overlay)); - z-index: 200; - position: fixed; - inset: 0; - animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); - } - - &__content { - color: hsla(var(--modal-fg)); - overflow: auto; - background-color: hsla(var(--modal-bg)); - border-radius: 8px; - font-size: 14px; - position: fixed; - z-index: 300; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 50vw; - max-width: 560px; - max-height: 85vh; - padding: 16px; - animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1); - border: 1px solid hsla(var(--app-border)); - @apply w-full; - - &--fullpage { - max-width: none; - width: 90vw; - max-height: 90vh; - } - - &:focus { - outline: none; - } - } - - &__title { - @apply leading-relaxed; - margin: 0 0 8px 0; - padding-right: 16px; - font-weight: 600; - color: hsla(var(--modal-fg)); - font-size: 18px; - } - - &__close-icon { - font-family: inherit; - border-radius: 100%; - height: 24px; - width: 24px; - display: inline-flex; - align-items: center; - justify-content: center; - color: hsla(var(--modal-fg)); - position: absolute; - top: 8px; - right: 16px; - } -} - -@keyframes overlayShow { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes contentShow { - from { - opacity: 0; - transform: translate(-50%, -48%) scale(0.96); - } - to { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } -} diff --git a/joi/src/core/Progress/Progress.test.tsx b/joi/src/core/Progress/Progress.test.tsx deleted file mode 100644 index 9d18bf019..000000000 --- a/joi/src/core/Progress/Progress.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Progress } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Progress', () => { - it('renders with default props', () => { - render() - const progressElement = screen.getByRole('progressbar') - expect(progressElement).toBeInTheDocument() - expect(progressElement).toHaveClass('progress') - expect(progressElement).toHaveClass('progress--medium') - expect(progressElement).toHaveAttribute('aria-valuenow', '50') - }) - - it('applies custom className', () => { - render() - const progressElement = screen.getByRole('progressbar') - expect(progressElement).toHaveClass('custom-class') - }) - - it('renders with different sizes', () => { - const { rerender } = render() - let progressElement = screen.getByRole('progressbar') - expect(progressElement).toHaveClass('progress--small') - - rerender() - progressElement = screen.getByRole('progressbar') - expect(progressElement).toHaveClass('progress--large') - }) - - it('sets the correct transform style based on value', () => { - render() - const progressElement = screen.getByRole('progressbar') - const indicatorElement = progressElement.firstChild as HTMLElement - expect(indicatorElement).toHaveStyle('transform: translateX(-25%)') - }) - - it('handles edge cases for value', () => { - const { rerender } = render() - let progressElement = screen.getByRole('progressbar') - let indicatorElement = progressElement.firstChild as HTMLElement - expect(indicatorElement).toHaveStyle('transform: translateX(-100%)') - expect(progressElement).toHaveAttribute('aria-valuenow', '0') - - rerender() - progressElement = screen.getByRole('progressbar') - indicatorElement = progressElement.firstChild as HTMLElement - expect(indicatorElement).toHaveStyle('transform: translateX(-0%)') - expect(progressElement).toHaveAttribute('aria-valuenow', '100') - }) -}) diff --git a/joi/src/core/Progress/index.tsx b/joi/src/core/Progress/index.tsx deleted file mode 100644 index 01aefbeb0..000000000 --- a/joi/src/core/Progress/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { HTMLAttributes } from 'react' - -import { cva, type VariantProps } from 'class-variance-authority' - -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -const progressVariants = cva('progress', { - variants: { - size: { - small: 'progress--small', - medium: 'progress--medium', - large: 'progress--large', - }, - }, - defaultVariants: { - size: 'medium', - }, -}) - -export interface ProgressProps - extends HTMLAttributes, - VariantProps { - value: number -} - -const Progress = ({ className, size, value, ...props }: ProgressProps) => { - return ( -
-
-
- ) -} - -export { Progress } diff --git a/joi/src/core/Progress/styles.scss b/joi/src/core/Progress/styles.scss deleted file mode 100644 index 02d22f5f4..000000000 --- a/joi/src/core/Progress/styles.scss +++ /dev/null @@ -1,25 +0,0 @@ -.progress { - background-color: hsla(var(--progress-track-bg)); - border-radius: 8px; - position: relative; - overflow: hidden; - @apply transition-all; - - &--indicator { - background-color: hsla(var(--primary-bg)); - position: absolute; - border-radius: 8px; - width: 100%; - height: 100%; - } - - &--small { - height: 6px; - } - &--medium { - @apply h-2; - } - &--large { - @apply h-3; - } -} diff --git a/joi/src/core/ScrollArea/ScrollArea.test.tsx b/joi/src/core/ScrollArea/ScrollArea.test.tsx deleted file mode 100644 index 961c5da59..000000000 --- a/joi/src/core/ScrollArea/ScrollArea.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { ScrollArea } from './index' - -declare const global: typeof globalThis - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -class ResizeObserverMock { - observe() {} - unobserve() {} - disconnect() {} -} - -global.ResizeObserver = ResizeObserverMock - -describe('@joi/core/ScrollArea', () => { - it('renders children correctly', () => { - render( - -
Test Content
-
- ) - - const child = screen.getByTestId('child') - expect(child).toBeInTheDocument() - expect(child).toHaveTextContent('Test Content') - }) - - it('applies custom className', () => { - const { container } = render() - - const root = container.firstChild as HTMLElement - expect(root).toHaveClass('scroll-area__root') - expect(root).toHaveClass('custom-class') - }) - - it('forwards ref to the Viewport component', () => { - const ref = React.createRef() - render() - - expect(ref.current).toBeInstanceOf(HTMLDivElement) - expect(ref.current).toHaveClass('scroll-area__viewport') - }) -}) diff --git a/joi/src/core/ScrollArea/index.tsx b/joi/src/core/ScrollArea/index.tsx deleted file mode 100644 index 2d44b4af8..000000000 --- a/joi/src/core/ScrollArea/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { PropsWithChildren, forwardRef } from 'react' -import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, onScroll, ...props }, ref) => ( - - - {children} - - - - - - - - - -)) - -export { ScrollArea } diff --git a/joi/src/core/ScrollArea/styles.scss b/joi/src/core/ScrollArea/styles.scss deleted file mode 100644 index 99ee7de87..000000000 --- a/joi/src/core/ScrollArea/styles.scss +++ /dev/null @@ -1,53 +0,0 @@ -.scroll-area { - position: relative; - z-index: 999; - - &__root { - width: 200px; - height: 225px; - overflow: hidden; - } - - &__viewport { - width: 100%; - height: 100%; - border-radius: inherit; - } - - &__bar { - display: flex; - user-select: none; - touch-action: none; - padding: 1px; - background: hsla(var(--scrollbar-tracker)); - transition: background 160ms ease-out; - } - - &__thumb { - flex: 1; - background: hsla(var(--scrollbar-thumb)); - border-radius: 20px; - position: relative; - - ::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - height: 100%; - min-width: 44px; - min-height: 44px; - } - } -} - -.scroll-area__bar[data-orientation='vertical'] { - width: 8px; -} - -.scroll-area__bar[data-orientation='horizontal'] { - flex-direction: column; - height: 8px; -} diff --git a/joi/src/core/Select/Select.test.tsx b/joi/src/core/Select/Select.test.tsx deleted file mode 100644 index 1b450706b..000000000 --- a/joi/src/core/Select/Select.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { Select } from './index' -import '@testing-library/jest-dom' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -jest.mock('tailwind-merge', () => ({ - twMerge: (...classes: string[]) => classes.filter(Boolean).join(' '), -})) - -const mockOnValueChange = jest.fn() -jest.mock('@radix-ui/react-select', () => ({ - Root: ({ - children, - onValueChange, - }: { - children: React.ReactNode - onValueChange?: (value: string) => void - }) => { - mockOnValueChange.mockImplementation(onValueChange) - return
{children}
- }, - Trigger: ({ - children, - className, - }: { - children: React.ReactNode - className?: string - }) => ( - - ), - Value: ({ placeholder }: { placeholder?: string }) => ( - {placeholder} - ), - Icon: ({ children }: { children: React.ReactNode }) => ( - {children} - ), - Portal: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - Content: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - Viewport: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - Item: ({ children, value }: { children: React.ReactNode; value: string }) => ( -
mockOnValueChange(value)} - > - {children} -
- ), - ItemText: ({ children }: { children: React.ReactNode }) => ( - {children} - ), - ItemIndicator: ({ children }: { children: React.ReactNode }) => ( - {children} - ), - Arrow: () =>
, -})) -describe('@joi/core/Select', () => { - const options = [ - { name: 'Option 1', value: 'option1' }, - { name: 'Option 2', value: 'option2' }, - ] - - it('renders with placeholder', () => { - render() - expect(screen.getByTestId('select-item-option1')).toBeInTheDocument() - expect(screen.getByTestId('select-item-option2')).toBeInTheDocument() - }) - - it('calls onValueChange when an option is selected', async () => { - const user = userEvent.setup() - const onValueChange = jest.fn() - render() - expect(screen.getByTestId('select-trigger')).toHaveClass('select__disabled') - }) - - it('applies block class when block prop is true', () => { - render( - {children} -
- ), - Track: ({ children }: any) => ( -
{children}
- ), - Range: () =>
, - Thumb: () =>
, -})) - -describe('@joi/core/Slider', () => { - it('renders correctly with default props', () => { - render() - expect(screen.getByTestId('slider-root')).toBeInTheDocument() - expect(screen.getByTestId('slider-track')).toBeInTheDocument() - expect(screen.getByTestId('slider-range')).toBeInTheDocument() - expect(screen.getByTestId('slider-thumb')).toBeInTheDocument() - }) - - it('passes props correctly to SliderPrimitive.Root', () => { - const props = { - name: 'test-slider', - min: 0, - max: 100, - value: [50], - step: 1, - disabled: true, - } - render() - const sliderRoot = screen.getByTestId('slider-root') - expect(sliderRoot).toHaveAttribute('name', 'test-slider') - expect(sliderRoot).toHaveAttribute('min', '0') - expect(sliderRoot).toHaveAttribute('max', '100') - expect(sliderRoot).toHaveAttribute('value', '50') - expect(sliderRoot).toHaveAttribute('step', '1') - expect(sliderRoot).toHaveAttribute('disabled', '') - }) - - it('calls onValueChange when value changes', () => { - const onValueChange = jest.fn() - render() - const input = screen.getByTestId('slider-root').querySelector('input') - fireEvent.change(input!, { target: { value: '75' } }) - expect(onValueChange).toHaveBeenCalledWith([75]) - }) -}) diff --git a/joi/src/core/Slider/index.tsx b/joi/src/core/Slider/index.tsx deleted file mode 100644 index ea3d8dfca..000000000 --- a/joi/src/core/Slider/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import * as SliderPrimitive from '@radix-ui/react-slider' -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -type Props = { - name?: string - min?: number - max?: number - onValueChange?(value: number[]): void - value?: number[] - defaultValue?: number[] - step?: number - disabled?: boolean -} - -const Slider = ({ - name, - min, - max, - onValueChange, - value, - defaultValue, - step, - disabled, -}: Props) => ( - - - - - {value?.map((_, i) => ( - - ))} - -) - -export { Slider } diff --git a/joi/src/core/Slider/styles.scss b/joi/src/core/Slider/styles.scss deleted file mode 100644 index 6b7cf8575..000000000 --- a/joi/src/core/Slider/styles.scss +++ /dev/null @@ -1,43 +0,0 @@ -.slider { - position: relative; - display: flex; - align-items: center; - user-select: none; - touch-action: none; - height: 16px; - - &--disabled { - cursor: not-allowed; - opacity: 0.2; - } - - &__track { - background-color: hsla(var(--slider-track-bg)); - position: relative; - flex-grow: 1; - border-radius: 9999px; - height: 4px; - } - - &__range { - position: absolute; - background-color: hsla(var(--primary-bg)); - border-radius: 9999px; - height: 100%; - } - - &__thumb { - display: block; - width: 16px; - height: 16px; - background-color: hsla(var(--slider-thumb-bg)); - border-radius: 10px; - padding: 2px; - border: 2px solid hsla(var(--primary-bg)); - - &:focus { - outline: none; - box-shadow: 0 0 0 5px hsla(var(--slider-track-bg), 50%); - } - } -} diff --git a/joi/src/core/Switch/Switch.test.tsx b/joi/src/core/Switch/Switch.test.tsx deleted file mode 100644 index 72f3d8007..000000000 --- a/joi/src/core/Switch/Switch.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import { render, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Switch } from './index' - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Switch', () => { - it('renders correctly', () => { - const { getByRole } = render() - const checkbox = getByRole('checkbox') - expect(checkbox).toBeInTheDocument() - }) - - it('applies custom className', () => { - const { container } = render() - expect(container.firstChild).toHaveClass('switch custom-class') - }) - - it('can be checked and unchecked', () => { - const { getByRole } = render() - const checkbox = getByRole('checkbox') as HTMLInputElement - - expect(checkbox.checked).toBe(false) - fireEvent.click(checkbox) - expect(checkbox.checked).toBe(true) - fireEvent.click(checkbox) - expect(checkbox.checked).toBe(false) - }) - - it('can be disabled', () => { - const { getByRole } = render() - const checkbox = getByRole('checkbox') as HTMLInputElement - expect(checkbox).toBeDisabled() - }) - - it('calls onChange when clicked', () => { - const handleChange = jest.fn() - const { getByRole } = render() - const checkbox = getByRole('checkbox') - - fireEvent.click(checkbox) - expect(handleChange).toHaveBeenCalledTimes(1) - }) - - it('can have a default checked state', () => { - const { getByRole } = render() - const checkbox = getByRole('checkbox') as HTMLInputElement - expect(checkbox.checked).toBe(true) - }) -}) diff --git a/joi/src/core/Switch/index.tsx b/joi/src/core/Switch/index.tsx deleted file mode 100644 index 28eabe6e6..000000000 --- a/joi/src/core/Switch/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { ChangeEvent, InputHTMLAttributes } from 'react' - -import { twMerge } from 'tailwind-merge' - -import './styles.scss' - -export interface SwitchProps extends InputHTMLAttributes { - disabled?: boolean - className?: string - onChange?: (e: ChangeEvent) => void -} - -const Switch = ({ - name, - checked, - disabled, - defaultChecked, - className, - onChange, - ...props -}: SwitchProps) => { - return ( - - ) -} -export { Switch } diff --git a/joi/src/core/Switch/styles.scss b/joi/src/core/Switch/styles.scss deleted file mode 100644 index 9f7adbd4f..000000000 --- a/joi/src/core/Switch/styles.scss +++ /dev/null @@ -1,67 +0,0 @@ -.switch { - position: relative; - display: inline-block; - width: 32px; - height: 18px; - - > input { - opacity: 0; - width: 0; - height: 0; - - // disabled - &:disabled { - + .switch--thumb { - cursor: not-allowed; - background-color: hsla(var(--disabled-bg)); - &:before { - background-color: hsla(var(--disabled-fg)); - } - } - // disabled and checked - &:checked + .switch--thumb { - cursor: not-allowed; - background-color: hsla(var(--primary-bg)); - &:before { - background-color: hsla(var(--disabled-fg)); - } - } - } - - &:checked + .switch--thumb { - background-color: hsla(var(--primary-bg)); - - &::before { - -webkit-transform: translateX(14px); - -ms-transform: translateX(14px); - transform: translateX(14px); - } - } - } - - &--thumb { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: hsla(var(--switch-bg)); - -webkit-transition: 0.4s; - transition: 0.4s; - border-radius: 20px; - - &:before { - position: absolute; - content: ''; - height: 14px; - width: 14px; - left: 2px; - bottom: 2px; - background-color: hsla(var(--switch-fg)); - -webkit-transition: 0.4s; - transition: 0.4s; - border-radius: 50%; - } - } -} diff --git a/joi/src/core/Tabs/Tabs.test.tsx b/joi/src/core/Tabs/Tabs.test.tsx deleted file mode 100644 index 46bd48435..000000000 --- a/joi/src/core/Tabs/Tabs.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Tabs, TabsContent } from './index' - -// Mock the Tooltip component -jest.mock('../Tooltip', () => ({ - Tooltip: ({ children, content, trigger }) => ( -
- {trigger || children} -
- ), -})) - -// Mock the styles -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/Tabs', () => { - const mockOptions = [ - { name: 'Tab 1', value: 'tab1' }, - { name: 'Tab 2', value: 'tab2' }, - { - name: 'Tab 3', - value: 'tab3', - disabled: true, - tooltipContent: 'Disabled tab', - }, - ] - - it('renders tabs correctly', () => { - render( - {}}> - Content 1 - Content 2 - Content 3 - - ) - - expect(screen.getByText('Tab 1')).toBeInTheDocument() - expect(screen.getByText('Tab 2')).toBeInTheDocument() - expect(screen.getByText('Tab 3')).toBeInTheDocument() - expect(screen.getByText('Content 1')).toBeInTheDocument() - }) - - it('changes tab content when clicked', () => { - const { rerender } = render( - {}}> - Content 1 - Content 2 - Content 3 - - ) - - expect(screen.getByText('Content 1')).toBeInTheDocument() - expect(screen.queryByText('Content 2')).not.toBeInTheDocument() - - fireEvent.click(screen.getByText('Tab 2')) - - // Rerender with the new value to simulate the state change - rerender( - {}}> - Content 1 - Content 2 - Content 3 - - ) - - expect(screen.queryByText('Content 1')).not.toBeInTheDocument() - expect(screen.getByText('Content 2')).toBeInTheDocument() - }) - - it('disables tab when specified', () => { - render( - {}}> - Content 1 - Content 2 - Content 3 - - ) - - expect(screen.getByText('Tab 3')).toHaveAttribute('disabled') - }) - - it('renders tooltip for disabled tab', () => { - render( - {}}> - Content 1 - Content 2 - Content 3 - - ) - - const tooltipWrapper = screen.getByTestId('mock-tooltip') - expect(tooltipWrapper).toHaveAttribute( - 'data-tooltip-content', - 'Disabled tab' - ) - }) - - it('applies the tabStyle if provided', () => { - render( - {}} - tabStyle="segmented" - /> - ) - - const tabsContainer = screen.getByTestId('segmented-style') - expect(tabsContainer).toHaveClass('tabs') - expect(tabsContainer).toHaveClass('tabs--segmented') - }) -}) diff --git a/joi/src/core/Tabs/index.tsx b/joi/src/core/Tabs/index.tsx deleted file mode 100644 index 2dca19831..000000000 --- a/joi/src/core/Tabs/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { ReactNode } from 'react' - -import * as TabsPrimitive from '@radix-ui/react-tabs' - -import { Tooltip } from '../Tooltip' - -import './styles.scss' -import { twMerge } from 'tailwind-merge' - -type TabStyles = 'segmented' - -type TabsProps = { - options: { - name: string - value: string - disabled?: boolean - tooltipContent?: string - }[] - children?: ReactNode - - defaultValue?: string - tabStyle?: TabStyles - value: string - onValueChange?: (value: string) => void -} - -type TabsContentProps = { - value: string - children: ReactNode - className?: string -} - -const TabsContent = ({ value, children, className }: TabsContentProps) => { - return ( - - {children} - - ) -} - -const Tabs = ({ - options, - children, - tabStyle, - defaultValue, - value, - onValueChange, - ...props -}: TabsProps) => ( - - - {options.map((option, i) => { - return option.disabled ? ( - - {option.name} - - } - /> - ) : ( - - {option.name} - - ) - })} - - - {children} - -) - -export { Tabs, TabsContent } diff --git a/joi/src/core/Tabs/styles.scss b/joi/src/core/Tabs/styles.scss deleted file mode 100644 index 932b8431a..000000000 --- a/joi/src/core/Tabs/styles.scss +++ /dev/null @@ -1,66 +0,0 @@ -.tabs { - display: flex; - flex-direction: column; - width: 100%; - - &--segmented { - background-color: hsla(var(--secondary-bg)); - border-radius: 6px; - height: 33px; - - .tabs__list { - border: none; - justify-content: center; - align-items: center; - height: 33px; - } - - .tabs__trigger[data-state='active'] { - background-color: hsla(var(--app-bg)); - border: none; - height: 25px; - margin: 0 4px; - border-radius: 5px; - } - } - - &__list { - flex-shrink: 0; - display: flex; - border-bottom: 1px solid hsla(var(--app-border)); - } - - &__trigger { - padding: 0 12px; - flex: 1; - height: 38px; - display: flex; - white-space: nowrap; - color: hsla(var(--text-secondary)); - align-items: center; - justify-content: center; - line-height: 1; - font-weight: medium; - user-select: none; - &:focus { - position: relative; - } - &:disabled { - cursor: not-allowed; - opacity: 0.5; - } - } - - &__content { - flex-grow: 1; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - outline: none; - } -} - -.tabs__trigger[data-state='active'] { - border-bottom: 1px solid hsla(var(--primary-bg)); - font-weight: 600; - color: hsla(var(--text-primary)); -} diff --git a/joi/src/core/TextArea/TextArea.test.tsx b/joi/src/core/TextArea/TextArea.test.tsx deleted file mode 100644 index e29eed5d0..000000000 --- a/joi/src/core/TextArea/TextArea.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import { render, screen, act } from '@testing-library/react' -import '@testing-library/jest-dom' -import { TextArea } from './index' - -jest.mock('./styles.scss', () => ({})) - -describe('@joi/core/TextArea', () => { - it('renders correctly', () => { - render(