diff --git a/.github/labeler.yml b/.github/labeler.yml index 454edc2ba..587ff935d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,7 +9,7 @@ labels: - label: 'build' sync: true matcher: - title: '^(ci|build)(\((?!release)\w+\))?: (?!.*\brelease\b).*' + title: '^(ci|build)(\((?!release)\w+\))?: (?!.*\brelease\b).*' - label: 'chore' sync: true @@ -46,7 +46,7 @@ labels: sync: true matcher: title: '^test(\(\w+\))?: .*' - + checks: - context: 'Semantic Pull Request' description: @@ -62,4 +62,4 @@ checks: - build - release - chore - - dependency \ No newline at end of file + - dependency diff --git a/.github/release.yml b/.github/release.yml index 182ae71e4..02d0b9250 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -29,4 +29,4 @@ changelog: - chore - title: Other Changes labels: - - "*" \ No newline at end of file + - '*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9640a8e2f..1da15896d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,12 @@ on: branches: - main paths-ignore: - - "docs/**" + - 'docs/**' pull_request: branches: - main paths-ignore: - - "docs/**" + - 'docs/**' permissions: {} @@ -32,7 +32,7 @@ jobs: permissions: contents: read with: - directory: "." + directory: '.' tests: name: Tests diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fa96e1bae..18d049be3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,10 +5,10 @@ on: branches: - main paths: - - "docs/**" + - 'docs/**' pull_request: paths: - - "docs/**" + - 'docs/**' permissions: {} @@ -19,4 +19,4 @@ jobs: permissions: contents: read with: - directory: "docs" + directory: 'docs' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16caf65ac..2f365003f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,13 +6,13 @@ on: directory: type: string required: true - description: "The directory to lint" + description: 'The directory to lint' permissions: {} jobs: lint-code: - name: Lint Code [biomejs] + name: Lint Code [vite-plus] runs-on: ubuntu-latest permissions: contents: read @@ -28,8 +28,8 @@ jobs: with: directory: ${{ inputs.directory }} - - name: Run linter - run: pnpm lint:check + - name: Run lint and format checks + run: pnpm check working-directory: ${{ inputs.directory }} lint-i18n: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08b15fd07..afed50705 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: permissions: contents: read with: - directory: "." + directory: '.' tests: name: Tests diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index 029580aed..51498470f 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -23,10 +23,13 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + sparse-checkout: | + renovate.json + .github - uses: ./.github/actions/setup-node with: - run-install: "false" + run-install: 'false' - name: Install Renovate run: pnpm install --global renovate diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 5e5f2c064..7b1db181c 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,28 +1,28 @@ name: GitHub Actions Security Analysis on: - push: - branches: - - main - pull_request: - branches: - - "**" + push: + branches: + - main + pull_request: + branches: + - '**' permissions: {} jobs: - zizmor: - name: Run zizmor 🌈 - runs-on: ubuntu-latest - permissions: - security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. - contents: read # Only needed for private repos. Needed to clone the repo. - actions: read # Only needed for private repos. Needed for upload-sarif to read workflow run info. - steps: - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + zizmor: + name: Run zizmor 🌈 + runs-on: ubuntu-latest + permissions: + security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. + contents: read # Only needed for private repos. Needed to clone the repo. + actions: read # Only needed for private repos. Needed for upload-sarif to read workflow run info. + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Run zizmor 🌈 - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + - name: Run zizmor 🌈 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 diff --git a/.husky/pre-commit b/.husky/pre-commit index 8b3e5b645..6606e9a49 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm dlx lint-staged +pnpm exec vp staged diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 62a02adca..474347c07 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "GraphQL.vscode-graphql", "GraphQL.vscode-graphql-syntax", "lokalise.i18n-ally", + "oxc.oxc-vscode", "SonarSource.sonarlint-vscode", "vitest.explorer", "zizmor.zizmor-vscode" diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b146dc31..587f55b73 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,12 @@ { "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit" + "source.fixAll.biome": "explicit", + "source.fixAll.oxc": "explicit" }, - "editor.defaultFormatter": "biomejs.biome", + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, + "oxc.lint.configPath": "./vite.config.ts", + "oxc.fmt.configPath": "./vite.config.ts", "files.associations": { "*.css": "tailwindcss" }, @@ -10,10 +14,16 @@ "editor.defaultFormatter": "astro-build.astro-vscode" }, "[json]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" }, "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" }, "cSpell.words": [ "atlassify", @@ -21,7 +31,9 @@ "partialize", "rovo", "xcss", - "aptabase" + "aptabase", + "oxfmt", + "oxlint" ], "i18n-ally.translate.engines": ["google"], "i18n-ally.localesPaths": ["src/renderer/i18n/locales"], diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fdf305451..32b0765ad 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,10 +1,11 @@ # Atlassify Project Architecture ## Overview -Atlassify is a cross-platform desktop app for monitoring notifications from Atlassian Cloud products. Built with Electron, React, and TypeScript, it provides a fast, native experience with customizable filters, themes, and tray/menu bar integration. +Atlassify is a cross-platform desktop app for monitoring notifications from Atlassian Cloud products. Built with Electron, React, and TypeScript, it provides a fast, native experience with customizable filters, themes, and tray/menu bar integration. ## Folder Structure + ``` / β”œβ”€β”€ src/ @@ -20,61 +21,64 @@ Atlassify is a cross-platform desktop app for monitoring notifications from Atla β”œβ”€β”€ ... # Config files, project metadata ``` - ## Core Components + - **Main Process (`src/main`)**: Handles app lifecycle, system events, tray/menu bar, auto-launch, updater, and IPC communication. - **Preload (`src/preload`)**: Secure bridge exposing whitelisted APIs to the renderer. - **Renderer (`src/renderer`)**: React-based UI, state management (stores via [Zustand](https://github.com/pmndrs/zustand)), hooks, components, and settings. Uses [TanStack Query](https://tanstack.com/query/latest) for data fetching and caching. - **Shared (`src/shared`)**: Common utilities, types, and constants used across main, preload, and renderer. - ## State Management The app uses a deliberate three-layer approach. Use the following decision rule when choosing where state belongs: -| Layer | Use when | Examples | -|---|---|---| -| **Zustand (persisted)** | Data must survive app restarts; serialisable config or credentials | `useAccountsStore`, `useFiltersStore`, `useSettingsStore` | -| **Zustand (non-persisted)** | Volatile state that must be readable *outside* React (e.g. tray subscriptions) | `useRuntimeStore` | -| **AppContext + React Query** | Ephemeral, async, React-lifecycle-bound state; cannot be serialised | notifications, loading/error status, mutations | +| Layer | Use when | Examples | +| ---------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------- | +| **Zustand (persisted)** | Data must survive app restarts; serialisable config or credentials | `useAccountsStore`, `useFiltersStore`, `useSettingsStore` | +| **Zustand (non-persisted)** | Volatile state that must be readable _outside_ React (e.g. tray subscriptions) | `useRuntimeStore` | +| **AppContext + React Query** | Ephemeral, async, React-lifecycle-bound state; cannot be serialised | notifications, loading/error status, mutations | **Persisted Zustand stores** (`src/renderer/stores`): + - `useAccountsStore` β€” Authenticated accounts; tokens are encrypted via `electron.safeStorage` before being written to local storage. - `useFiltersStore` β€” Active notification filter values (products, categories, read-states, etc.). - `useSettingsStore` β€” All UI and behaviour preferences (theme, zoom, shortcuts). **Non-persisted Zustand store**: + - `useRuntimeStore` β€” The Reactβ†’Electron bridge. The `useNotifications` hook writes filtered counts and error state into this store each render, so that `subscriptions.ts` (which runs outside of React) can read pre-filtered values without re-applying filter logic or touching the React Query cache. **AppContext** (`src/renderer/context/AppContext.tsx`): + - Acts as a faΓ§ade/coordinator, not a `useState` holder. It composes hooks and stores into a single stable, memoised context value that route-level components consume via `useAppContext()`. - Does **not** hold persistent config β€” components that need settings, filters, or account data access those stores directly, avoiding unnecessary re-renders. **Subscriptions** (`src/renderer/stores/subscriptions.ts`): Store subscriptions synchronize state with Electron APIs and system events (e.g. updating the tray icon when notification counts change). - ## Hooks All hooks live in `src/renderer/hooks`. Each hook has a single, named responsibility. **Data fetching**: + - `useNotifications` β€” Fetches, filters, and caches notifications via React Query. Applies `useFiltersStore` values as a `select` transformer (instant filtering, no re-fetch). Writes filtered counts into `useRuntimeStore` for tray updates. - `useAccounts` β€” Periodically refreshes authenticated account profiles (1hr interval). **Lifecycle & side-effects** (called inside `AppProvider`): + - `useOnlineSync` β€” Subscribes to TanStack Query's `onlineManager` and keeps `useRuntimeStore.isOnline` in sync. Also corrects `onlineManager`'s initial state from `navigator.onLine` on mount. - `useAppReset` β€” Listens for the `onResetApp` IPC event and calls `reset()` on every persisted store. Add new persisted stores here. - `useKeyboardNavigation` β€” Tracks the focused notification ID for keyboard traversal. - `useGlobalShortcuts` β€” Registers system-level keyboard shortcuts from settings. **Utilities**: + - `useIntervalTimer` β€” Wraps `setInterval` for reliable background polling regardless of window visibility (see polling strategy below). - `useNavigationAnalytics` β€” Tracks route transitions. - `useAppContext` β€” Type-safe accessor for `AppContext`; throws if used outside `AppProvider`. **IPC Communication**: Main ↔ Preload ↔ Renderer via secure, typed IPC channels for data, commands, and notifications. - ## Data Flow - **Background Polling Strategy**: @@ -90,33 +94,39 @@ All hooks live in `src/renderer/hooks`. Each hook has a single, named responsibi - **Animation Coordination**: Components manage their own exit animations independently of cache updates, decoupling UI timing from data layer. ## UI + - **Component Library**: Uses [Atlassian @atlaskit](https://atlassian.design/components/) for UI components, ensuring a native Atlassian look and feel. - **Styling**: [Tailwind CSS](https://tailwindcss.com/) is used for utility-first styling and layout. - **Design Tokens**: Atlassian design tokens are integrated for consistent theming and design language across the app. ## API & Integrations + - **Atlassian API**: Accessed via secure requests, using API tokens with scopes. - **State & Query**: [Zustand](https://github.com/pmndrs/zustand) for state management, [TanStack Query](https://tanstack.com/query/latest) for server state and caching. - **Third-party Integrations**: Homebrew, Netlify, SonarCloud, GitHub Actions for CI/CD and releases. ## Build & Deployment + - **Build Tools**: Vite for frontend, Electron Builder for packaging. - **Testing**: [Vitest](https://vitest.dev/) for unit tests and coverage. - **CI/CD**: Automated workflows for linting, testing, building, signing, and releasing. - **Release Process**: See CONTRIBUTING.md for details. ## Extensibility & Customization + - **Locales**: Add new language files in `src/renderer/i18n/locales`. - **Themes & Filters**: Customizable via settings store and UI. - **Components**: Modular React components for easy extension. ## Key Patterns & Conventions + - **TypeScript**: Strong typing throughout. - **Biome**: Linting and formatting. - **Testing**: Vitest for unit tests, coverage tracked. - **Commit Style**: Clear, descriptive commit messages. ## Useful Links + - [CONTRIBUTING.md](CONTRIBUTING.md) - [README.md](README.md) - [GitHub Issues](https://github.com/setchy/atlassify/issues) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6a8bb5ce..d427ca815 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Atlassify Contributing Guide -Hi, we're really excited that you're interested in contributing to Atlassify! +Hi, we're really excited that you're interested in contributing to Atlassify! Before submitting your contribution, please read through the following guide. @@ -13,24 +13,27 @@ This project is a tool for monitoring new notifications from Atlassian Cloud pro To get started: Clone the repository and install dependencies: - ```shell - pnpm install - ``` + +```shell +pnpm install +``` Start development mode (includes GraphQL codegen and hot module reload): - ```shell - pnpm dev - ``` + +```shell +pnpm dev +``` ### Tests -There are two main checks: -1. Linter & formatter with [Biome][biome-website] -2. Unit tests with [Vitest][vitest-website] +Tooling is unified through [Vite+][vite-plus-website], which bundles the linter (oxlint), formatter (oxfmt), test runner (Vitest), and dev/build pipeline (Vite). ```shell -# Run biome to check linting and formatting -pnpm lint:check +# Run lint, format, and type checks +pnpm check + +# Auto-fix formatting and lint issues +pnpm check:fix # Run unit tests with coverage pnpm test @@ -41,7 +44,7 @@ pnpm test -u ### Code Style & Conventions -- We use [Biome][biome-website] for linting and formatting. Please run `pnpm lint:check` before submitting a PR. +- Linting and formatting are configured in `vite.config.ts` (the `lint` and `fmt` blocks). Please run `pnpm check` before submitting a PR. - Follow existing file and folder naming conventions. - Keep commit messages clear and descriptive. @@ -56,37 +59,42 @@ The release process is automated. Follow the steps below. 1. **Verify features:** Ensure all features and fixes you want included in the release are merged into `main`. 2. **Check dependencies:** Review the [Renovate Dependency Dashboard][github-dependency-dashboard] for any dependency updates you want to include. 3. **Create a release branch:** - - Name your branch `release/vX.X.X` (e.g., `release/v1.2.3`). - - Run `pnpm version ` to **bump the version** in `package.json` and create a version commit/tag. - - Update `sonar.projectVersion` within `sonar-project.properties` - - Commit and push these changes. - - Open a Pull Request (PR) from your release branch. + +- Name your branch `release/vX.X.X` (e.g., `release/v1.2.3`). +- Run `pnpm version ` to **bump the version** in `package.json` and create a version commit/tag. +- Update `sonar.projectVersion` within `sonar-project.properties` +- Commit and push these changes. +- Open a Pull Request (PR) from your release branch. + 4. **GitHub release:** GitHub Actions will automatically build, sign, and upload release assets to a new draft release with automated release notes. 5. **Merge the release branch:** Once the PR is approved and checks pass, merge your release branch into `main`. 6. **Publish the release:** - - Finalize the release notes in the draft release on GitHub. - - Confirm all assets are present and correct. - - Publish the release. + +- Finalize the release notes in the draft release on GitHub. +- Confirm all assets are present and correct. +- Publish the release. + 7. **Update milestones:** - - Edit the current [Milestone][github-milestones]: - - Add a link to the release notes in the description. - - Set the due date to the release date. - - Close the milestone. - - Create a [New Milestone][github-new-milestone] for the next release cycle. +- Edit the current [Milestone][github-milestones]: + - Add a link to the release notes in the description. + - Set the due date to the release date. + - Close the milestone. +- Create a [New Milestone][github-new-milestone] for the next release cycle. ### Locales Atlassify supports multiple languages/locales. To add a new locale: + - Add a new locale file under `./src/renderer/i18n/locales`. - Import and update the resources in `./src/renderer/i18n/index.ts`. - Use the VSCode Extension `i18n Ally` to automatically translate the keys. -- Carefully verify the translated values. **Do not translate placeholder variables** like `{{ }}`. You may need to manually update these if needed. - +- Carefully verify the translated values. **Do not translate placeholder variables** like `{{ }}`. You may need to manually update these if needed. + [biome-website]: https://biomejs.dev/ [github-dependency-dashboard]: https://github.com/setchy/atlassify/issues/1 [github-issues]: https://github.com/setchy/atlassify/issues @@ -95,4 +103,3 @@ To add a new locale: [github-new-release]: https://github.com/setchy/atlassify/releases/new [homebrew-cask-autobump-workflow]: https://github.com/Homebrew/homebrew-cask/actions/workflows/autobump.yml [vitest-website]: https://vitest.dev/ - diff --git a/README.md b/README.md index efca07a97..aa51572b5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ --- - ## Features - πŸ”” Unified notifications from Atlassian Cloud products @@ -18,20 +17,20 @@ - ⚑ Fast, native experience - 🌐 Multi-language/localization support - ## Quick Start 1. **Download** Atlassify for free from [atlassify.io][website]. 2. **Install** and launch the app for your platform. 3. **Authenticate** with your Atlassian account and start receiving notifications. -#### Experimental +#### Experimental + macOS users can also install via [Homebrew][brew] + ```shell brew install --cask setchy/brews/atlassify ``` - ## Build & Development To build and run Atlassify locally: @@ -44,23 +43,19 @@ pnpm dev See [CONTRIBUTING.md](CONTRIBUTING.md) for full development and contribution instructions. - ## FAQ See our [Atlassify FAQs][faqs] for answers to common questions. - ## Community & Support - Open an [issue][github-issues] for bugs or feature requests - See [CONTRIBUTING.md](CONTRIBUTING.md) for more ways to get involved - ## License Atlassify is licensed under the MIT Open Source license. See [LICENSE](LICENSE) for details. - ## Acknowledgements I would like to acknowledge the following projects and resources that have inspired and contributed to the development of Atlassify: @@ -68,15 +63,13 @@ I would like to acknowledge the following projects and resources that have inspi 1. [Gitify][attribution-gitify] – An open-source GitHub notification app, which I am the lead maintainer of, served as the launchpad for Atlassify. 2. [Atlassian Design System][attribution-atlassian] – The design principles and UI components (@atlaskit) from Atlassian have helped shape the user interface of Atlassify. - + [social]: docs/public/images//social.png [website]: https://atlassify.io [faqs]: https://atlassify.io/faq - [attribution-gitify]: https://gitify.io [attribution-atlassian]: https://atlassian.design - [github]: https://github.com/setchy/atlassify [github-actions]: https://github.com/setchy/atlassify/actions [github-issues]: https://github.com/setchy/atlassify/issues @@ -84,12 +77,10 @@ I would like to acknowledge the following projects and resources that have inspi [github-website]: https://github.com/setchy/atlassify-website [github-website-pulls]: https://github.com/setchy/atlassify-website/pulls [brew]: https://brew.sh - [coverage-badge]: https://img.shields.io/sonar/coverage/setchy_atlassify?server=https%3A%2F%2Fsonarcloud.io&logo=sonarqubecloud [coverage]: https://sonarcloud.io/summary/new_code?id=setchy_atlassify [quality-badge]: https://img.shields.io/sonar/quality_gate/setchy_atlassify?server=https%3A%2F%2Fsonarcloud.io&logo=sonarqubecloud [quality]: https://sonarcloud.io/summary/new_code?id=setchy_atlassify - [ci-workflow-badge]: https://img.shields.io/github/actions/workflow/status/setchy/atlassify/ci.yml?logo=github&label=CI [release-workflow-badge]: https://img.shields.io/github/actions/workflow/status/setchy/atlassify/release.yml?logo=github&label=Release [downloads-total-badge]: https://img.shields.io/github/downloads/setchy/atlassify/total?label=downloads@all&logo=github diff --git a/ROADMAP.md b/ROADMAP.md index 4f63c9a12..b58a306e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,13 +1,15 @@ -# Atlassify Roadmap +# Atlassify Roadmap -Please see our [GitHub Issues registry][github-issues] for items we want to incorporate into the product offering in the near future. +Please see our [GitHub Issues registry][github-issues] for items we want to incorporate into the product offering in the near future. [Contributions][contributing] are always welcomed! # Atlassian Roadmap + - https://www.atlassian.com/wac/roadmap/cloud?&search=Atlassian%20Home - https://www.atlassian.com/wac/roadmap/cloud/work-suggestions-for-atlassian-home?p=8721f558-a6 + [contributing]: CONTRIBUTING -[github-issues]: https://github.com/setchy/atlassify/issues \ No newline at end of file +[github-issues]: https://github.com/setchy/atlassify/issues diff --git a/assets/images/README.md b/assets/images/README.md index aa72160d3..b250a9023 100644 --- a/assets/images/README.md +++ b/assets/images/README.md @@ -1,3 +1,3 @@ # Resources -- https://atlassian.design/resources/logo-library \ No newline at end of file +- https://atlassian.design/resources/logo-library diff --git a/biome.json b/biome.json deleted file mode 100644 index c2238f086..000000000 --- a/biome.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", - "files": { - "includes": ["**", "!**/docs/**/*", "!**/generated/**/*"] - }, - "assist": { - "actions": { - "source": { - "organizeImports": { - "level": "on", - "options": { - "groups": [ - ":NODE:", - ":BLANK_LINE:", - ["react*", "@testing-library/**"], - ":BLANK_LINE:", - ["*electron*", "menubar"], - ":BLANK_LINE:", - "@atlaskit/**", - ":BLANK_LINE:", - ":PACKAGE:", - ":BLANK_LINE:", - ["**/__mocks__/**", "**/__helpers__/**"], - ":BLANK_LINE:", - "**/shared/**", - ":BLANK_LINE:", - ["**/constants", "**/constants/**"], - ":BLANK_LINE:", - [ - "**/context/**", - "**/hooks/**", - "**/routes/**", - "**/stores", - "**/stores/**" - ], - ":BLANK_LINE:", - [ - "**/layout/**", - "**/components/**", - "**/fields/**", - "**/primitives/**" - ], - ":BLANK_LINE:", - "**/types", - ":BLANK_LINE:", - "**" - ] - } - }, - "useSortedAttributes": "on" - } - } - }, - "linter": { - "enabled": true, - "domains": { - "react": "recommended", - "test": "recommended" - }, - "rules": { - "recommended": true, - "correctness": { - "noUnusedFunctionParameters": "error", - "useExhaustiveDependencies": { - "level": "warn", - "options": { - "hooks": [ - { - "name": "useNavigate", - "stableResult": true - } - ] - } - }, - "useHookAtTopLevel": { - "level": "error", - "options": { - "ignore": ["useAlternateIdleIcon", "useUnreadActiveIcon"] - } - } - }, - "style": { - "noInferrableTypes": "error", - "noParameterAssign": "error", - "noUnusedTemplateLiteral": "error", - "noUselessElse": "error", - "useAsConstAssertion": "error", - "useBlockStatements": "error", - "useDefaultParameterLast": "error", - "useDefaultSwitchClause": "error", - "useEnumInitializers": "error", - "useNumberNamespace": "error", - "useSelfClosingElements": "error", - "useSingleVarDeclarator": "error" - }, - "suspicious": { - "noConsole": "error", - "noUnknownAtRules": "warn" - } - } - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "css": { - "formatter": { - "enabled": true - }, - "parser": { - "tailwindDirectives": true - } - }, - "html": { - "formatter": { - "enabled": true - }, - "experimentalFullSupportEnabled": true - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "jsxQuoteStyle": "double" - } - }, - "json": { - "parser": { - "allowComments": true - } - }, - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - } -} diff --git a/docs/README.md b/docs/README.md index fb758adfe..20ad0669c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# atlassify.io +# atlassify.io > The source code for our atlassify.io website diff --git a/docs/astro.config.ts b/docs/astro.config.ts index 9285dd312..d54e982f3 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -1,7 +1,6 @@ -import { defineConfig } from 'astro/config'; -import icon from 'astro-icon'; - import tailwindcss from '@tailwindcss/vite'; +import icon from 'astro-icon'; +import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ diff --git a/docs/package.json b/docs/package.json index 3f92d07f4..ce85ee4c7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,6 +3,27 @@ "version": "latest", "private": true, "description": "The source code of atlassify.io", + "keywords": [ + "astro", + "atlassian", + "atlassify", + "notifications", + "notifier", + "website" + ], + "homepage": "https://atlassify.io/", + "bugs": { + "url": "https://github.com/setchy/atlassify/issues" + }, + "license": "MIT", + "author": { + "name": "Adam Setch", + "url": "https://github.com/setchy" + }, + "repository": { + "type": "git", + "url": "https://github.com/setchy/atlassify" + }, "type": "module", "scripts": { "prepare": "cd .. && husky", @@ -13,27 +34,6 @@ "lint": "biome check --fix", "lint:check": "biome check" }, - "repository": { - "type": "git", - "url": "https://github.com/setchy/atlassify" - }, - "keywords": [ - "atlassify", - "atlassian", - "notifier", - "notifications", - "website", - "astro" - ], - "author": { - "name": "Adam Setch", - "url": "https://github.com/setchy" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/setchy/atlassify/issues" - }, - "homepage": "https://atlassify.io/", "dependencies": { "@iconify-json/line-md": "1.2.16", "@iconify-json/mdi": "1.2.3", @@ -56,8 +56,8 @@ "@biomejs/biome": "2.4.16", "husky": "9.1.7" }, - "packageManager": "pnpm@11.5.2", "lint-staged": { "*.{js,ts,cjs,mjs,astro}": "biome format --fix" - } + }, + "packageManager": "pnpm@11.5.2" } diff --git a/docs/src/components/GitHubRepo.astro b/docs/src/components/GitHubRepo.astro index 2c0c34f6e..8f9677dc0 100644 --- a/docs/src/components/GitHubRepo.astro +++ b/docs/src/components/GitHubRepo.astro @@ -1,17 +1,17 @@ --- -import { Icon } from 'astro-icon/components'; +import { Icon } from "astro-icon/components"; -import { Octokit } from 'octokit'; +import { Octokit } from "octokit"; -import { siteMetadata, URLs } from '~/constants'; +import { siteMetadata, URLs } from "~/constants"; -import type { RepoStats } from '~/types'; +import type { RepoStats } from "~/types"; const octokit = new Octokit(); const formatCount = (count: number) => { - return new Intl.NumberFormat('en', { - notation: 'compact', + return new Intl.NumberFormat("en", { + notation: "compact", maximumFractionDigits: 1, }).format(count); }; @@ -31,14 +31,15 @@ const loadRepoStats = async (): Promise => { return { forks: formatCount(repository.data.forks_count), stars: formatCount(repository.data.stargazers_count), - latestReleaseName: latestRelease.data.name?.replace('v', '') ?? '', + latestReleaseName: latestRelease.data.name?.replace("v", "") ?? "", }; } catch (err) { - console.error('Failed to load repo stats', err); + // oxlint-disable-next-line no-console + console.error("Failed to load repo stats", err); return { - forks: '', - stars: '', - latestReleaseName: '', + forks: "", + stars: "", + latestReleaseName: "", }; } }; diff --git a/docs/src/faqs/1.md b/docs/src/faqs/1.md index f8e8478ae..08bc0f0fb 100644 --- a/docs/src/faqs/1.md +++ b/docs/src/faqs/1.md @@ -1,7 +1,9 @@ --- -title: "How do I authenticate Atlassify with my Atlassian account?" +title: 'How do I authenticate Atlassify with my Atlassian account?' --- + Atlassify currently supports authentication via Atlassian API Tokens, either: + - βœ… API token with scopes **[strongly recommended]** - ⚠️ API token _[supported, but not recommended]_ @@ -11,4 +13,4 @@ When authenticating with **API token with scopes** please choose the following J
-Please refer to the [official docs](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/#Create-an-API-token-with-scopes) for help creating and managing an **API token with scopes**. \ No newline at end of file +Please refer to the [official docs](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/#Create-an-API-token-with-scopes) for help creating and managing an **API token with scopes**. diff --git a/docs/src/faqs/2.md b/docs/src/faqs/2.md index ac83744e4..9416de21b 100644 --- a/docs/src/faqs/2.md +++ b/docs/src/faqs/2.md @@ -1,5 +1,5 @@ --- -title: "How do Filters work?" +title: 'How do Filters work?' --- Filters in Atlassify help you manage which notifications are shown in the app, allowing you to concentrate on the most crucial ones for your workflow. @@ -11,4 +11,3 @@ Filters can be accessed from the Atlassify sidebar. By default, all notification
If you apply filters, your inbox will display notifications based on a `LOGICAL AND` condition. - diff --git a/docs/src/faqs/3.md b/docs/src/faqs/3.md index cfd52358d..3d14b3a8b 100644 --- a/docs/src/faqs/3.md +++ b/docs/src/faqs/3.md @@ -1,7 +1,9 @@ --- -title: "Something is not working as expected, how can I debug Atlassify?" +title: 'Something is not working as expected, how can I debug Atlassify?' --- + Using **Chrome Developer Tools** (console logs, network requests, etc): + - All platforms: right click tray icon then _Developer β†’ Toggle Developer Tools_ - macOS: `command + opt + i` - Windows: `ctrl + shift + i` @@ -10,7 +12,8 @@ Using **Chrome Developer Tools** (console logs, network requests, etc):
Using **Application Log Files**: + - All platforms: right click tray icon then _Developer β†’ View Application Logs_ - macOS: `~/Library/Logs/atlassify` - Windows: `%USERPROFILE%\\AppData\\Roaming\\atlassify\\logs` -- Linux: `~/.config/atlassify/logs` \ No newline at end of file +- Linux: `~/.config/atlassify/logs` diff --git a/docs/src/faqs/4.md b/docs/src/faqs/4.md index f5afeab2e..684393015 100644 --- a/docs/src/faqs/4.md +++ b/docs/src/faqs/4.md @@ -1,4 +1,5 @@ --- -title: "Does Atlassify support Atlassian Data Center or Server products?" +title: 'Does Atlassify support Atlassian Data Center or Server products?' --- -Currently Atlassify supports Atlassian Cloud products. If you would like to see support for other product types, please raise a [GitHub issue](https://github.com/setchy/atlassify/issues). \ No newline at end of file + +Currently Atlassify supports Atlassian Cloud products. If you would like to see support for other product types, please raise a [GitHub issue](https://github.com/setchy/atlassify/issues). diff --git a/docs/src/faqs/5.md b/docs/src/faqs/5.md index 02aa1b811..e87c1e814 100644 --- a/docs/src/faqs/5.md +++ b/docs/src/faqs/5.md @@ -1,8 +1,9 @@ --- -title: "How can I contribute to Atlassify?" +title: 'How can I contribute to Atlassify?' --- + You can contribute to Atlassify by opening an issue or pull request on GitHub at [setchy/atlassify](https://github.com/setchy/atlassify).
-Check out our [open issues](https://github.com/setchy/atlassify/issues) and see if there is any existing ideas that you would like to work on. \ No newline at end of file +Check out our [open issues](https://github.com/setchy/atlassify/issues) and see if there is any existing ideas that you would like to work on. diff --git a/docs/src/styles/app.css b/docs/src/styles/app.css index f0a7383b9..03130b9a7 100644 --- a/docs/src/styles/app.css +++ b/docs/src/styles/app.css @@ -1,5 +1,5 @@ /** Tailwind CSS */ -@import "tailwindcss"; +@import 'tailwindcss'; /** Tailwind CSS Configuration */ @config "../../tailwind.config.ts"; diff --git a/docs/tailwind.config.ts b/docs/tailwind.config.ts index ebef15294..0e536eac3 100644 --- a/docs/tailwind.config.ts +++ b/docs/tailwind.config.ts @@ -1,6 +1,6 @@ import type { Config } from 'tailwindcss'; -import colors from 'tailwindcss/colors'; import TailwindCSSMotion from 'tailwindcss-motion'; +import colors from 'tailwindcss/colors'; const config: Config = { content: ['./src/**/*.{astro,ts,tsx}'], diff --git a/index.html b/index.html new file mode 100644 index 000000000..54cf04086 --- /dev/null +++ b/index.html @@ -0,0 +1,9 @@ + + + + vite-plugin-electron + + +
An entry file for electron renderer process.
+ + \ No newline at end of file diff --git a/netlify.toml b/netlify.toml index af9d86363..caada32d8 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,7 +1,6 @@ [build] - base = "docs" - publish = "dist" - command = "pnpm build" - - # Only build when there are changes in docs/ - ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- docs/" \ No newline at end of file +base = "docs" +publish = "dist" +command = "pnpm build" +# Only build when there are changes in docs/ +ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- docs/" diff --git a/package.json b/package.json index 9efd135dc..2fd2dce2e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,32 @@ "version": "3.9.0", "private": true, "description": "Atlassian notifications on your menu bar.", + "keywords": [ + "atlassian", + "atlassify", + "bitbucket", + "compass", + "confluence", + "electron", + "jira", + "menubar", + "notifications", + "taskbar", + "tray" + ], + "homepage": "https://atlassify.io", + "bugs": { + "url": "https://github.com/setchy/atlassify/issues" + }, + "license": "MIT", + "author": { + "name": "Adam Setch", + "url": "https://github.com/setchy" + }, + "repository": { + "type": "git", + "url": "https://github.com/setchy/atlassify.git" + }, "main": "build/main.js", "scripts": { "clean": "rimraf build coverage dist node_modules", @@ -20,41 +46,16 @@ "i18n:check": "i18next-cli extract --ci --quiet", "i18n:lint": "i18next-cli lint", "i18n:status": "i18next-cli status", - "lint": "biome check --fix", - "lint:check": "biome check", - "test": "vitest --coverage --run", - "test:watch": "vitest --watch --coverage", - "test:ui": "vitest --ui" - }, - "engines": { - "node": ">=24" + "check": "vp check", + "check:fix": "vp check --fix", + "lint": "vp lint --fix", + "lint:check": "vp lint", + "format": "vp fmt", + "format:check": "vp fmt --check", + "test": "vp test --coverage --run", + "test:watch": "vp test --watch --coverage", + "test:ui": "vp test --ui" }, - "repository": { - "type": "git", - "url": "https://github.com/setchy/atlassify.git" - }, - "keywords": [ - "atlassify", - "atlassian", - "bitbucket", - "compass", - "confluence", - "jira", - "notifications", - "electron", - "menubar", - "taskbar", - "tray" - ], - "author": { - "name": "Adam Setch", - "url": "https://github.com/setchy" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/setchy/atlassify/issues" - }, - "homepage": "https://atlassify.io", "dependencies": { "electron-log": "5.4.4", "electron-updater": "6.8.9", @@ -90,7 +91,6 @@ "@atlaskit/toggle": "16.1.0", "@atlaskit/tokens": "13.2.0", "@atlaskit/tooltip": "22.6.0", - "@biomejs/biome": "2.4.16", "@compiled/vite-plugin": "1.1.4", "@discordapp/twemoji": "16.0.1", "@electron/notarize": "3.1.1", @@ -133,16 +133,12 @@ "vite-plugin-checker": "0.14.1", "vite-plugin-electron": "0.29.1", "vite-plugin-electron-renderer": "0.14.7", + "vite-plus": "0.1.20", "vitest": "4.1.8", "zustand": "5.0.14" }, - "packageManager": "pnpm@11.5.2", - "lint-staged": { - "*": "biome check --fix --no-errors-on-unmatched", - "*.{js,ts,tsx}": [ - "bash -c 'tsc --noEmit'", - "bash -c 'pnpm i18n:lint'", - "pnpm test --changed --passWithNoTests --update" - ] - } + "engines": { + "node": ">=24" + }, + "packageManager": "pnpm@11.5.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6359c15d..364478795 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,9 +111,6 @@ importers: '@atlaskit/tooltip': specifier: 22.6.0 version: 22.6.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@biomejs/biome': - specifier: 2.4.16 - version: 2.4.16 '@compiled/vite-plugin': specifier: 1.1.4 version: 1.1.4(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -233,13 +230,16 @@ importers: version: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0) vite-plugin-checker: specifier: 0.14.1 - version: 0.14.1(@biomejs/biome@2.4.16)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 0.14.1(@biomejs/biome@2.4.16)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)) vite-plugin-electron: specifier: 0.29.1 version: 0.29.1(vite-plugin-electron-renderer@0.14.7) vite-plugin-electron-renderer: specifier: 0.14.7 version: 0.14.7 + vite-plus: + specifier: 0.1.20 + version: 0.1.20(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(happy-dom@20.10.1)(jiti@2.7.0)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0) vitest: specifier: 4.1.8 version: 4.1.8(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(happy-dom@20.10.1)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -1281,9 +1281,290 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/runtime@0.127.0': + resolution: {integrity: sha512-UQYLxAhDDPHm++szfa4z0RTdcPq5vaywrAoEA2n1YaAKeanXQdjHsoT6x1gP3U97RN8LZ7yHsSOrKPCcA6mCqw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxfmt/binding-android-arm-eabi@0.46.0': + resolution: {integrity: sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.46.0': + resolution: {integrity: sha512-v6+HhjsoV3GO0u2u9jLSAZrvWfTraDxKofUIQ7/ktS7tzS+epVsxdHmeM+XxuNcAY/nWxxU1Sg4JcGTNRXraBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.46.0': + resolution: {integrity: sha512-3eeooJGrqGIlI5MyryDZsAcKXSmKIgAD4yYtfRrRJzXZ0UTFZtiSveIur56YPrGMYZwT4XyVhHsMqrNwr1XeFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.46.0': + resolution: {integrity: sha512-QG8BDM0CXWbu84k2SKmCqfEddPQPFiBicwtYnLqHRWZZl57HbtOLRMac/KTq2NO4AEc4ICCBpFxJIV9zcqYfkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.46.0': + resolution: {integrity: sha512-9DdCqS/n2ncu/Chazvt3cpgAjAmIGQDz7hFKSrNItMApyV/Ja9mz3hD4JakIE3nS8PW9smEbPWnb389QLBY4nw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.46.0': + resolution: {integrity: sha512-Dgs7VeE2jT0LHMhw6tPEt0xQYe54kBqHEovmWsv4FVQlegCOvlIJNx0S8n4vj8WUtpT+Z6BD2HhKJPLglLxvZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.46.0': + resolution: {integrity: sha512-Zxn3adhTH13JKnU4xXJj8FeEfF680XjXh3gSShKl57HCMBRde2tUJTgogV/1MSHA80PJEVrDa7r66TLVq3Ia7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.46.0': + resolution: {integrity: sha512-+TWipjrgVM8D7aIdDD0tlr3teLTTvQTn7QTE5BpT10H1Fj82gfdn9X6nn2sDgx/MepuSCfSnzFNJq2paLL0OiA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.46.0': + resolution: {integrity: sha512-aAUPBWJ1lGwwnxZUEDLJ94+Iy6MuwJwPxUgO4sCA5mEEyDk7b+cDQ+JpX1VR150Zoyd+D49gsrUzpUK5h587Eg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.46.0': + resolution: {integrity: sha512-ufBCJukyFX/UDrokP/r6BGDoTInnsDs7bxyzKAgMiZlt2Qu8GPJSJ6Zm6whIiJzKk0naxA8ilwmbO1LMw6Htxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.46.0': + resolution: {integrity: sha512-eqtlC2YmPqjun76R1gVfGLuKWx7NuEnLEAudZ7n6ipSKbCZTqIKSs1b5Y8K/JHZsRpLkeSmAAjig5HOIg8fQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.46.0': + resolution: {integrity: sha512-yccVOO2nMXkQLGgy0He3EQEwKD7NF0zEk+/OWmroznkqXyJdN6bfK0LtNnr6/14Bh3FjpYq7bP33l/VloCnxpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.46.0': + resolution: {integrity: sha512-aAf7fG23OQCey6VRPj9IeCraoYtpgtx0ZyJ1CXkPyT1wjzBE7c3xtuxHe/AdHaJfVVb/SXpSk8Gl1LzyQupSqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.46.0': + resolution: {integrity: sha512-q0JPsTMyJNjYrBvYFDz4WbVsafNZaPCZv4RnFypRotLqpKROtBZcEaXQW4eb9YmvLU3NckVemLJnzkSZSdmOxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.46.0': + resolution: {integrity: sha512-7LsLY9Cw57GPkhSR+duI3mt9baRczK/DtHYSldQ4BEU92da9igBQNl4z7Vq5U9NNPsh1FmpKvv1q9WDtiUQR1A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.46.0': + resolution: {integrity: sha512-lHiBOz8Duaku7JtRNLlps3j++eOaICPZSd8FCVmTDM4DFOPT71Bjn7g6iar1z7StXlKRweUKxWUs4sA+zWGDXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.46.0': + resolution: {integrity: sha512-/5ktYUliP89RhgC37DBH1x20U5zPSZMy3cMEcO0j3793rbHP9MWsknBwQB6eozRzWmYrh0IFM/p20EbPvDlYlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.46.0': + resolution: {integrity: sha512-3WTnoiuIr8XvV0DIY7SN+1uJSwKf4sPpcbHfobcRT9JutGcLaef/miyBB87jxd3aqH+mS0+G5lsgHuXLUwjjpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.46.0': + resolution: {integrity: sha512-IXxiQpkYnOwNfP23vzwSfhdpxJzyiPTY7eTn6dn3DsriKddESzM8i6kfq9R7CD/PUJwCvQT22NgtygBeug3KoA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.22.0': + resolution: {integrity: sha512-/exgXceakHbQrzaHTtKOe7MuDATaWMCCWpsCDQCZKeYhLGXzComipTrCYnHzAXrdnNBb5r5K+RRf5A6ormrhMA==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.22.0': + resolution: {integrity: sha512-xFGdIahlmUbK+/MpZ5y08D0ewMGLDbd2Vki5wxVFYg50lSrtgPAtdDl+kqKZLNaFu0zpMar8n9wv1le05sL/jw==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.22.0': + resolution: {integrity: sha512-53RvC9f77eUo+V1dfQNwGVnsIfPJFMibRR0ee128EUpYNDOZe/ojmCfuXJeU7cY91V7r7fZSm42KPJocXUX8og==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.22.0': + resolution: {integrity: sha512-evZcJAZ9hjNyuN69RnXwbt+U2pAOcYt+yvqukgugiCkRm4iBZ0R0CvpY1tgfG2XcGUhEPh8dljO+nPZTEVGpCQ==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.22.0': + resolution: {integrity: sha512-7jTO+k1mr5BxRAI2fxc1NRcE3MAbHNZ0Vef9SD1yAR6d1E6qEv5D/D7yuHpQpw6AO3qoecSVo2Jzr+JirN61+w==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.22.0': + resolution: {integrity: sha512-7lbl9XFcqO+scsynxMzTQdl0XUe6sBUCyY/oGWvCB+JmV4U+70vzSyZJdTEzzxtkZiNnUVFFh9RJLmoiQSne+w==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.61.0': + resolution: {integrity: sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.61.0': + resolution: {integrity: sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.61.0': + resolution: {integrity: sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.61.0': + resolution: {integrity: sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.61.0': + resolution: {integrity: sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.61.0': + resolution: {integrity: sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.61.0': + resolution: {integrity: sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.61.0': + resolution: {integrity: sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.61.0': + resolution: {integrity: sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.61.0': + resolution: {integrity: sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.61.0': + resolution: {integrity: sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.61.0': + resolution: {integrity: sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.61.0': + resolution: {integrity: sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.61.0': + resolution: {integrity: sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.61.0': + resolution: {integrity: sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.61.0': + resolution: {integrity: sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.61.0': + resolution: {integrity: sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.61.0': + resolution: {integrity: sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.61.0': + resolution: {integrity: sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -1876,6 +2157,149 @@ packages: '@vitest/utils@4.1.8': resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@voidzero-dev/vite-plus-core@0.1.20': + resolution: {integrity: sha512-4KmzRfzwTeG3JuvDijrdqWusSgRvLMKDPrVsDdtbDVVjEMq0VnM8lSH+Nvepd6Pg+SuSVUP212OIfH/3Yn1bfA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.10 + '@tsdown/exe': 0.21.10 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.20': + resolution: {integrity: sha512-ykCOJk91h0IEMvljYGTauI4Svxr/CatZAitofvtEFqaTCLE3n06QCHD8qWphMM784VnPz1G/J2xuewxbQduNlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@voidzero-dev/vite-plus-darwin-x64@0.1.20': + resolution: {integrity: sha512-5XxNW9cYEh85Z4BErALyWh/tLP/NZmxNXzUQ0FanhHreI2Zq7FfgbSqQNvC7/sYsPYTWf74RlxmIjzV7R/Lb5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.20': + resolution: {integrity: sha512-Mc7npPBd9t/h0haURVCZGae+TfB0Yx2Ex8HbPKOVA4hnN9ynlMhMpLRFfTQAicDKYbEGDhfBcbCIX0vVv4vacA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.20': + resolution: {integrity: sha512-Oh/pxMdTLR/wsDl/OONjItjLOeTewFBLuKkH5RQmcI9g3AVqKzLj1/uawujgysBI5E25tonRRK7I2q/zu8Uqvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.20': + resolution: {integrity: sha512-msO1ZoUX5aSK8L6kN1C3XQO4CcH9aFsNPRSNcO1cjk1kTnaLyVYzkVxgvbh3vk7nzZAAMkmyZ4SlMpqJrdahrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.20': + resolution: {integrity: sha512-U93urREvg23ZFDkxKkkfWWIOI4GI9erhbWAZpXG+GeYqygWKrVC6PUTXiuexVg3/CFg2sSMTdm1W6V7TFG5hYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-test@0.1.20': + resolution: {integrity: sha512-vy2dJYw1bhgQ/+BrQrfwPlSKzQ2mm3YLJ9kGF7Yo0UJ2P3XKpshtgFIWLjSg/IASnC93OAx0c/7j3NM0I1RMuA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.20': + resolution: {integrity: sha512-deXfe3h2OpzKV88s1PMUgVOJfN9LlnDDpIEVH6y2+YAXwlTSO7YeKBj2QmyS6ALZCI4Rfp4HOsB0OKMVBfEqww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.20': + resolution: {integrity: sha512-ygdgQgo0N9oUI1Q2IdYBcvr+KLY6riaqLY/bkWNYtvHS4uk8a4GuEd0F08znWt2E8sFm29i35bYIzI6fFY2EBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -2583,6 +3007,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -3685,6 +4112,25 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + oxfmt@0.46.0: + resolution: {integrity: sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint-tsgolint@0.22.0: + resolution: {integrity: sha512-ku4MecLmCQIj1ScCtzNAqTuyl0BJQ02B36fJT+c5XQihHpYSFak+FC3GYO5fPyYk4oDwi0w0S7hTvrpNzuZhig==} + hasBin: true + + oxlint@1.61.0: + resolution: {integrity: sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.18.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -3799,6 +4245,10 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} + pixelmatch@7.2.0: + resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==} + hasBin: true + pkijs@3.4.0: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} @@ -3807,6 +4257,10 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + postcss-calc@8.2.4: resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: @@ -4573,6 +5027,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -4760,6 +5218,11 @@ packages: vite-plugin-electron-renderer: optional: true + vite-plus@0.1.20: + resolution: {integrity: sha512-hxJqXTxiiFhszwAeD0MvKlztVuXE4TztTdJ64BPxGqgY67F0PDa5eZkUsrN91Ae8aYUMfweW6V/J57OUO9/0zw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5846,6 +6309,7 @@ snapshots: '@biomejs/cli-linux-x64-musl': 2.4.16 '@biomejs/cli-win32-arm64': 2.4.16 '@biomejs/cli-win32-x64': 2.4.16 + optional: true '@biomejs/cli-darwin-arm64@2.4.16': optional: true @@ -6785,8 +7249,144 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-project/runtime@0.127.0': {} + + '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.133.0': {} + '@oxfmt/binding-android-arm-eabi@0.46.0': + optional: true + + '@oxfmt/binding-android-arm64@0.46.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.46.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.46.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.46.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.46.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.46.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.46.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.46.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.46.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.46.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.46.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.46.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.46.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.46.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.46.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.46.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.46.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.46.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.22.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.22.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.22.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.22.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.22.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.22.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.61.0': + optional: true + + '@oxlint/binding-android-arm64@1.61.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.61.0': + optional: true + + '@oxlint/binding-darwin-x64@1.61.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.61.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.61.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.61.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.61.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.61.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.61.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.61.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.61.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.61.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.61.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.61.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.61.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.61.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.61.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.61.0': + optional: true + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -7272,6 +7872,85 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@voidzero-dev/vite-plus-core@0.1.20(@types/node@24.13.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)': + dependencies: + '@oxc-project/runtime': 0.127.0 + '@oxc-project/types': 0.127.0 + lightningcss: 1.32.0 + postcss: 8.5.15 + optionalDependencies: + '@types/node': 24.13.1 + fsevents: 2.3.3 + jiti: 2.7.0 + typescript: 6.0.3 + yaml: 2.9.0 + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-test@0.1.20(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(happy-dom@20.10.1)(jiti@2.7.0)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@24.13.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0) + es-module-lexer: 1.7.0 + obug: 2.1.2 + pixelmatch: 7.2.0 + pngjs: 7.0.0 + sirv: 3.0.2 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0) + ws: 8.21.0 + optionalDependencies: + '@types/node': 24.13.1 + '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) + '@vitest/ui': 4.1.8(vitest@4.1.8) + happy-dom: 20.10.1 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@tsdown/css' + - '@tsdown/exe' + - '@vitejs/devtools' + - bufferutil + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - yaml + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.20': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.20': + optional: true + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -8080,6 +8759,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} es-object-atoms@1.1.2: @@ -9183,6 +9864,62 @@ snapshots: outvariant@1.4.3: {} + oxfmt@0.46.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.46.0 + '@oxfmt/binding-android-arm64': 0.46.0 + '@oxfmt/binding-darwin-arm64': 0.46.0 + '@oxfmt/binding-darwin-x64': 0.46.0 + '@oxfmt/binding-freebsd-x64': 0.46.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.46.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.46.0 + '@oxfmt/binding-linux-arm64-gnu': 0.46.0 + '@oxfmt/binding-linux-arm64-musl': 0.46.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.46.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.46.0 + '@oxfmt/binding-linux-riscv64-musl': 0.46.0 + '@oxfmt/binding-linux-s390x-gnu': 0.46.0 + '@oxfmt/binding-linux-x64-gnu': 0.46.0 + '@oxfmt/binding-linux-x64-musl': 0.46.0 + '@oxfmt/binding-openharmony-arm64': 0.46.0 + '@oxfmt/binding-win32-arm64-msvc': 0.46.0 + '@oxfmt/binding-win32-ia32-msvc': 0.46.0 + '@oxfmt/binding-win32-x64-msvc': 0.46.0 + + oxlint-tsgolint@0.22.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.22.0 + '@oxlint-tsgolint/darwin-x64': 0.22.0 + '@oxlint-tsgolint/linux-arm64': 0.22.0 + '@oxlint-tsgolint/linux-x64': 0.22.0 + '@oxlint-tsgolint/win32-arm64': 0.22.0 + '@oxlint-tsgolint/win32-x64': 0.22.0 + + oxlint@1.61.0(oxlint-tsgolint@0.22.0): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.61.0 + '@oxlint/binding-android-arm64': 1.61.0 + '@oxlint/binding-darwin-arm64': 1.61.0 + '@oxlint/binding-darwin-x64': 1.61.0 + '@oxlint/binding-freebsd-x64': 1.61.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.61.0 + '@oxlint/binding-linux-arm-musleabihf': 1.61.0 + '@oxlint/binding-linux-arm64-gnu': 1.61.0 + '@oxlint/binding-linux-arm64-musl': 1.61.0 + '@oxlint/binding-linux-ppc64-gnu': 1.61.0 + '@oxlint/binding-linux-riscv64-gnu': 1.61.0 + '@oxlint/binding-linux-riscv64-musl': 1.61.0 + '@oxlint/binding-linux-s390x-gnu': 1.61.0 + '@oxlint/binding-linux-x64-gnu': 1.61.0 + '@oxlint/binding-linux-x64-musl': 1.61.0 + '@oxlint/binding-openharmony-arm64': 1.61.0 + '@oxlint/binding-win32-arm64-msvc': 1.61.0 + '@oxlint/binding-win32-ia32-msvc': 1.61.0 + '@oxlint/binding-win32-x64-msvc': 1.61.0 + oxlint-tsgolint: 0.22.0 + p-cancelable@2.1.1: {} p-event@4.2.0: @@ -9273,6 +10010,10 @@ snapshots: pify@3.0.0: {} + pixelmatch@7.2.0: + dependencies: + pngjs: 7.0.0 + pkijs@3.4.0: dependencies: '@noble/hashes': 1.4.0 @@ -9288,6 +10029,8 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + pngjs@7.0.0: {} + postcss-calc@8.2.4(postcss@8.5.15): dependencies: postcss: 8.5.15 @@ -10033,6 +10776,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} title-case@3.0.3: @@ -10157,7 +10902,7 @@ snapshots: util-deprecate@1.0.2: {} - vite-plugin-checker@0.14.1(@biomejs/biome@2.4.16)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)): + vite-plugin-checker@0.14.1(@biomejs/biome@2.4.16)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -10169,6 +10914,7 @@ snapshots: vite: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0) optionalDependencies: '@biomejs/biome': 2.4.16 + oxlint: 1.61.0(oxlint-tsgolint@0.22.0) typescript: 6.0.3 vite-plugin-electron-renderer@0.14.7: {} @@ -10177,6 +10923,53 @@ snapshots: optionalDependencies: vite-plugin-electron-renderer: 0.14.7 + vite-plus@0.1.20(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(happy-dom@20.10.1)(jiti@2.7.0)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.127.0 + '@voidzero-dev/vite-plus-core': 0.1.20(@types/node@24.13.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0) + '@voidzero-dev/vite-plus-test': 0.1.20(@types/node@24.13.1)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(happy-dom@20.10.1)(jiti@2.7.0)(typescript@6.0.3)(vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0))(yaml@2.9.0) + oxfmt: 0.46.0 + oxlint: 1.61.0(oxlint-tsgolint@0.22.0) + oxlint-tsgolint: 0.22.0 + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.20 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.20 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.20 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.20 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.20 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.20 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.20 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.20 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@tsdown/css' + - '@tsdown/exe' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - vite + - yaml + vite@8.0.16(@types/node@24.13.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a0c335892..cbbcc6f02 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,4 +14,4 @@ overrides: # scanners. None of these ship in the packaged app. cross-spawn: '^7.0.6' got: '^11.8.5' - electron: '$electron' \ No newline at end of file + electron: '$electron' diff --git a/scripts/afterPack.js b/scripts/afterPack.js index 043080cae..4a7f82d45 100644 --- a/scripts/afterPack.js +++ b/scripts/afterPack.js @@ -5,7 +5,7 @@ const builderConfig = require('../electron-builder'); const electronLanguages = builderConfig.electronLanguages; function logAfterPackProgress(msg) { - // biome-ignore lint/suspicious/noConsole: log notarizing progress + // oxlint-disable-next-line no-console -- log notarizing progress console.log(` β€’ [afterPack]: ${msg}`); } @@ -46,9 +46,7 @@ const removeUnusedLocales = (appOutDir, appName) => { ); // Get all locale directories - const allLocales = fs - .readdirSync(resourcesPath) - .filter((file) => file.endsWith('.lproj')); + const allLocales = fs.readdirSync(resourcesPath).filter((file) => file.endsWith('.lproj')); const langLocales = electronLanguages.map((lang) => `${lang}.lproj`); diff --git a/scripts/afterSign.js b/scripts/afterSign.js index 11436d85a..6b95e0b64 100644 --- a/scripts/afterSign.js +++ b/scripts/afterSign.js @@ -1,7 +1,7 @@ const { notarize } = require('@electron/notarize'); function logAfterSignProgress(msg) { - // biome-ignore lint/suspicious/noConsole: log notarizing progress + // oxlint-disable-next-line no-console -- log notarizing progress console.log(` β€’ [afterSign]: ${msg}`); } @@ -16,9 +16,7 @@ const afterSign = async (context) => { const shouldNotarize = process.env.NOTARIZE === 'true'; if (!shouldNotarize) { - logAfterSignProgress( - 'skipping notarize step as NOTARIZE env flag was not set', - ); + logAfterSignProgress('skipping notarize step as NOTARIZE env flag was not set'); return; } diff --git a/src/main/config.ts b/src/main/config.ts index 84501bc49..50a3d9a4e 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -21,18 +21,12 @@ export const Paths = { get notificationSound(): string { return pathToFileURL( - path.resolve( - __dirname, - 'assets', - 'sounds', - APPLICATION.NOTIFICATION_SOUND, - ), + path.resolve(__dirname, 'assets', 'sounds', APPLICATION.NOTIFICATION_SOUND), ).href; }, get twemojiFolder(): string { - return pathToFileURL(path.resolve(__dirname, 'assets', 'images', 'twemoji')) - .href; + return pathToFileURL(path.resolve(__dirname, 'assets', 'images', 'twemoji')).href; }, }; diff --git a/src/main/events.test.ts b/src/main/events.test.ts index 0f6bdd675..21cc95c1c 100644 --- a/src/main/events.test.ts +++ b/src/main/events.test.ts @@ -35,10 +35,7 @@ describe('main/events', () => { handleMainEvent( EVENTS.VERSION, - listenerMock as unknown as ( - e: Electron.IpcMainInvokeEvent, - d: unknown, - ) => void, + listenerMock as unknown as (e: Electron.IpcMainInvokeEvent, d: unknown) => void, ); expect(handleMock).toHaveBeenCalledWith(EVENTS.VERSION, listenerMock); @@ -48,11 +45,7 @@ describe('main/events', () => { const sendMock = vi.fn(); const mb: MockMenubar = { window: { webContents: { send: sendMock } } }; - sendRendererEvent( - mb as unknown as Menubar, - EVENTS.UPDATE_ICON_TITLE, - 'title', - ); + sendRendererEvent(mb as unknown as Menubar, EVENTS.UPDATE_ICON_TITLE, 'title'); expect(sendMock).toHaveBeenCalledWith(EVENTS.UPDATE_ICON_TITLE, 'title'); }); diff --git a/src/main/events.ts b/src/main/events.ts index c16ff3a5f..e0e39d158 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -38,10 +38,6 @@ export function handleMainEvent( * @param event - The IPC channel/event name to emit. * @param data - Optional payload sent with the event. */ -export function sendRendererEvent( - mb: Menubar, - event: EventType, - data?: string, -) { +export function sendRendererEvent(mb: Menubar, event: EventType, data?: string) { mb.window.webContents.send(event, data); } diff --git a/src/main/handlers/analytics.test.ts b/src/main/handlers/analytics.test.ts index c3069a6ae..2ae843734 100644 --- a/src/main/handlers/analytics.test.ts +++ b/src/main/handlers/analytics.test.ts @@ -49,10 +49,7 @@ describe('main/handlers/analytics.ts', () => { await initializeAnalytics(); expect(initializeMock).toHaveBeenCalledWith('test-key-123'); - expect(logInfoMock).toHaveBeenCalledWith( - 'analytics', - 'Aptabase initialized successfully', - ); + expect(logInfoMock).toHaveBeenCalledWith('analytics', 'Aptabase initialized successfully'); expect(logErrorMock).not.toHaveBeenCalled(); }); @@ -81,9 +78,7 @@ describe('main/handlers/analytics.ts', () => { it('registers the APTABASE_TRACK_EVENT handler', () => { registerAnalyticsHandlers(); - const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], - ); + const registeredEvents = onMock.mock.calls.map((call: [string]) => call[0]); expect(registeredEvents).toContain(EVENTS.APTABASE_TRACK_EVENT); }); diff --git a/src/main/handlers/app.test.ts b/src/main/handlers/app.test.ts index 9059e01b2..d7b5c32e6 100644 --- a/src/main/handlers/app.test.ts +++ b/src/main/handlers/app.test.ts @@ -43,12 +43,8 @@ describe('main/handlers/app.ts', () => { it('registers expected app IPC event handlers', () => { registerAppHandlers(menubar); - const registeredHandlers = handleMock.mock.calls.map( - (call: [string]) => call[0], - ); - const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], - ); + const registeredHandlers = handleMock.mock.calls.map((call: [string]) => call[0]); + const registeredEvents = onMock.mock.calls.map((call: [string]) => call[0]); expect(registeredHandlers).toContain(EVENTS.VERSION); expect(registeredHandlers).toContain(EVENTS.NOTIFICATION_SOUND_PATH); diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index e59ff4102..f9af9b8da 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -28,9 +28,7 @@ describe('main/handlers/storage.ts', () => { it('registers expected storage IPC event handlers', () => { registerStorageHandlers(); - const registeredHandlers = handleMock.mock.calls.map( - (call: [string]) => call[0], - ); + const registeredHandlers = handleMock.mock.calls.map((call: [string]) => call[0]); expect(registeredHandlers).toContain(EVENTS.SAFE_STORAGE_ENCRYPT); expect(registeredHandlers).toContain(EVENTS.SAFE_STORAGE_DECRYPT); diff --git a/src/main/handlers/system.test.ts b/src/main/handlers/system.test.ts index 14703289c..2f60611ea 100644 --- a/src/main/handlers/system.test.ts +++ b/src/main/handlers/system.test.ts @@ -46,9 +46,7 @@ describe('main/handlers/system.ts', () => { it('registers expected system IPC event handlers', () => { registerSystemHandlers(menubar); - const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], - ); + const registeredEvents = onMock.mock.calls.map((call: [string]) => call[0]); expect(registeredEvents).toContain(EVENTS.OPEN_EXTERNAL); expect(registeredEvents).toContain(EVENTS.UPDATE_KEYBOARD_SHORTCUT); diff --git a/src/main/handlers/system.ts b/src/main/handlers/system.ts index 291f997d3..3087ac22e 100644 --- a/src/main/handlers/system.ts +++ b/src/main/handlers/system.ts @@ -30,10 +30,7 @@ export function registerSystemHandlers(mb: Menubar): void { */ powerMonitor.on('unlock-screen', () => { sendRendererEvent(mb, EVENTS.SYSTEM_WAKE); - logInfo( - 'power-monitor', - 'unlock-screen event triggered, will refetch data', - ); + logInfo('power-monitor', 'unlock-screen event triggered, will refetch data'); }); /** diff --git a/src/main/handlers/tray.test.ts b/src/main/handlers/tray.test.ts index 4aa69df9d..f8ce4b4c7 100644 --- a/src/main/handlers/tray.test.ts +++ b/src/main/handlers/tray.test.ts @@ -34,9 +34,7 @@ describe('main/handlers/tray.ts', () => { it('registers expected tray IPC event handlers', () => { registerTrayHandlers(menubar); - const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], - ); + const registeredEvents = onMock.mock.calls.map((call: [string]) => call[0]); expect(registeredEvents).toContain(EVENTS.UPDATE_ICON_COLOR); expect(registeredEvents).toContain(EVENTS.UPDATE_ICON_TITLE); @@ -52,15 +50,11 @@ describe('main/handlers/tray.ts', () => { }; it('returns offline icon when appState is offline', () => { - expect(selectTrayIcon({ ...base, appState: 'offline' })).toBe( - TrayIcons.offline, - ); + expect(selectTrayIcon({ ...base, appState: 'offline' })).toBe(TrayIcons.offline); }); it('returns error icon when appState is error', () => { - expect(selectTrayIcon({ ...base, appState: 'error' })).toBe( - TrayIcons.error, - ); + expect(selectTrayIcon({ ...base, appState: 'error' })).toBe(TrayIcons.error); }); it('returns active icon when there are notifications and unreadIconVariant is active', () => { @@ -95,9 +89,7 @@ describe('main/handlers/tray.ts', () => { }); it('returns idle icon when there are no notifications', () => { - expect(selectTrayIcon({ ...base, notificationsCount: 0 })).toBe( - TrayIcons.idle, - ); + expect(selectTrayIcon({ ...base, notificationsCount: 0 })).toBe(TrayIcons.idle); }); it('returns idleAlternate icon when there are no notifications and idleIconVariant is alternative', () => { diff --git a/src/main/handlers/tray.ts b/src/main/handlers/tray.ts index 4280fe3c3..b0d71fe04 100644 --- a/src/main/handlers/tray.ts +++ b/src/main/handlers/tray.ts @@ -12,8 +12,7 @@ import { TrayIcons } from '../icons'; * @returns The icon path to display. */ export function selectTrayIcon(state: ITrayColorUpdate): string { - const { notificationsCount, appState, idleIconVariant, unreadIconVariant } = - state; + const { notificationsCount, appState, idleIconVariant, unreadIconVariant } = state; if (appState === 'error') { return TrayIcons.error; @@ -27,9 +26,7 @@ export function selectTrayIcon(state: ITrayColorUpdate): string { return TrayIcons.active; } - return idleIconVariant === 'alternative' - ? TrayIcons.idleAlternate - : TrayIcons.idle; + return idleIconVariant === 'alternative' ? TrayIcons.idleAlternate : TrayIcons.idle; } /** diff --git a/src/main/icons.test.ts b/src/main/icons.test.ts index a7093f2f9..edb0f53be 100644 --- a/src/main/icons.test.ts +++ b/src/main/icons.test.ts @@ -6,9 +6,7 @@ describe('main/icons.ts', () => { expect(TrayIcons.idle).toContain('assets/images/tray-idleTemplate.png'); - expect(TrayIcons.idleAlternate).toContain( - 'assets/images/tray-idle-white.png', - ); + expect(TrayIcons.idleAlternate).toContain('assets/images/tray-idle-white.png'); expect(TrayIcons.error).toContain('assets/images/tray-error.png'); diff --git a/src/main/index.ts b/src/main/index.ts index 1937fefbe..ea85c6482 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,11 +13,7 @@ import { trackEvent, } from './handlers'; import { TrayIcons } from './icons'; -import { - configureWindowEvents, - initializeAppLifecycle, - onFirstRunMaybe, -} from './lifecycle'; +import { configureWindowEvents, initializeAppLifecycle, onFirstRunMaybe } from './lifecycle'; import MenuBuilder from './menu'; import AppUpdater from './updater'; diff --git a/src/main/lifecycle/startup.ts b/src/main/lifecycle/startup.ts index 1c70602b9..4dfe78d38 100644 --- a/src/main/lifecycle/startup.ts +++ b/src/main/lifecycle/startup.ts @@ -14,10 +14,7 @@ import { sendRendererEvent } from '../events'; * @param mb - The menubar instance to attach lifecycle events to. * @param contextMenu - The tray context menu to pop up on right-click. */ -export function initializeAppLifecycle( - mb: Menubar, - contextMenu: Electron.Menu, -): void { +export function initializeAppLifecycle(mb: Menubar, contextMenu: Electron.Menu): void { mb.on('ready', () => { mb.app.setAppUserModelId(APPLICATION.ID); diff --git a/src/main/lifecycle/window.test.ts b/src/main/lifecycle/window.test.ts index 7a64efe7c..b93ceffe3 100644 --- a/src/main/lifecycle/window.test.ts +++ b/src/main/lifecycle/window.test.ts @@ -16,9 +16,7 @@ describe('main/lifecycle/window.ts', () => { menubar = { hideWindow: vi.fn(), tray: { - getBounds: vi - .fn() - .mockReturnValue({ x: 100, y: 100, width: 22, height: 22 }), + getBounds: vi.fn().mockReturnValue({ x: 100, y: 100, width: 22, height: 22 }), }, window: { setSize: vi.fn(), diff --git a/src/main/menu.test.ts b/src/main/menu.test.ts index 8ac24779f..c206cc2f6 100644 --- a/src/main/menu.test.ts +++ b/src/main/menu.test.ts @@ -82,9 +82,7 @@ describe('main/menu.ts', () => { /** Helper: build menu & return template (first arg passed to buildFromTemplate) */ const buildAndGetTemplate = () => { menuBuilder.buildMenu(); - return (Menu.buildFromTemplate as Mock).mock.calls.slice( - -1, - )[0][0] as TemplateItem[]; + return (Menu.buildFromTemplate as Mock).mock.calls.slice(-1)[0][0] as TemplateItem[]; }; beforeEach(() => { @@ -216,13 +214,10 @@ describe('main/menu.ts', () => { it('developer submenu click actions execute expected functions', () => { const template = buildAndGetTemplate(); - const devEntry = template.find( - (item) => item?.label === 'Developer', - ) as TemplateItem; + const devEntry = template.find((item) => item?.label === 'Developer') as TemplateItem; expect(devEntry).toBeDefined(); const submenu = devEntry.submenu; - const clickByLabel = (label: string) => - submenu.find((i) => i.label === label)?.click?.(); + const clickByLabel = (label: string) => submenu.find((i) => i.label === label)?.click?.(); clickByLabel('Take Screenshot'); expect(takeScreenshot).toHaveBeenCalledWith(menubar); @@ -257,9 +252,7 @@ describe('main/menu.ts', () => { it('developer submenu includes expected static accelerators', () => { const template = buildAndGetTemplate(); - const devEntry = template.find( - (item) => item?.label === 'Developer', - ) as TemplateItem; + const devEntry = template.find((item) => item?.label === 'Developer') as TemplateItem; const reloadItem = devEntry.submenu.find((i) => i.role === 'reload'); expect(reloadItem?.accelerator).toBe('CommandOrControl+R'); @@ -283,12 +276,8 @@ describe('main/menu.ts', () => { const template = (Menu.buildFromTemplate as Mock).mock.calls.slice( -1, )[0][0] as TemplateItem[]; - const devEntry = template.find( - (i) => i?.label === 'Developer', - ) as TemplateItem; - const toggleItem = devEntry.submenu?.find( - (i) => i.role === 'toggleDevTools', - ); + const devEntry = template.find((i) => i?.label === 'Developer') as TemplateItem; + const toggleItem = devEntry.submenu?.find((i) => i.role === 'toggleDevTools'); expect(toggleItem?.accelerator).toBe('Alt+Cmd+I'); }); @@ -305,12 +294,8 @@ describe('main/menu.ts', () => { const template = (Menu.buildFromTemplate as Mock).mock.calls.slice( -1, )[0][0] as TemplateItem[]; - const devEntry = template.find( - (i) => i?.label === 'Developer', - ) as TemplateItem; - const toggleItem = devEntry.submenu?.find( - (i) => i.role === 'toggleDevTools', - ); + const devEntry = template.find((i) => i?.label === 'Developer') as TemplateItem; + const toggleItem = devEntry.submenu?.find((i) => i.role === 'toggleDevTools'); expect(toggleItem?.accelerator).toBe('Ctrl+Shift+I'); }); diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index 3952d395d..570e9429c 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -110,12 +110,8 @@ describe('main/updater.ts', () => { }), ); expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled(); - expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith( - false, - ); - expect( - menuBuilder.setUpdateReadyForInstallMenuVisibility, - ).toHaveBeenCalledWith(true); + expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith(false); + expect(menuBuilder.setUpdateReadyForInstallMenuVisibility).toHaveBeenCalledWith(true); }); it('invokes quitAndInstall when user clicks Restart', async () => { @@ -153,12 +149,8 @@ describe('main/updater.ts', () => { emit('checking-for-update'); - expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith( - false, - ); - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenCalledWith(false); + expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith(false); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenCalledWith(false); }); it('handles update-available', async () => { @@ -166,9 +158,7 @@ describe('main/updater.ts', () => { emit('update-available'); - expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith( - true, - ); + expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith(true); expect(menubar.tray.setToolTip).toHaveBeenCalledWith( expect.stringContaining('A new update is available'), ); @@ -179,9 +169,7 @@ describe('main/updater.ts', () => { emit('download-progress', { percent: 12.3456 }); - expect(menubar.tray.setToolTip).toHaveBeenCalledWith( - expect.stringContaining('12.35%'), - ); + expect(menubar.tray.setToolTip).toHaveBeenCalledWith(expect.stringContaining('12.35%')); }); it('handles update-not-available', async () => { @@ -189,18 +177,10 @@ describe('main/updater.ts', () => { emit('update-not-available'); - expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith( - true, - ); - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenCalledWith(true); - expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith( - false, - ); - expect( - menuBuilder.setUpdateReadyForInstallMenuVisibility, - ).toHaveBeenCalledWith(false); + expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith(true); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenCalledWith(true); + expect(menuBuilder.setUpdateAvailableMenuVisibility).toHaveBeenCalledWith(false); + expect(menuBuilder.setUpdateReadyForInstallMenuVisibility).toHaveBeenCalledWith(false); }); it('auto-hides "No updates available" after configured timeout', async () => { @@ -211,15 +191,11 @@ describe('main/updater.ts', () => { emit('update-not-available'); // Immediately shows the message - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenCalledWith(true); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenCalledWith(true); // Then hides it after the configured timeout vi.advanceTimersByTime(APPLICATION.UPDATE_NOT_AVAILABLE_DISPLAY_MS); - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenLastCalledWith(false); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenLastCalledWith(false); } finally { vi.useRealTimers(); } @@ -233,24 +209,17 @@ describe('main/updater.ts', () => { emit('update-not-available'); // Message shown - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenCalledWith(true); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenCalledWith(true); // New check should hide immediately and clear pending timeout emit('checking-for-update'); - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility, - ).toHaveBeenLastCalledWith(false); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility).toHaveBeenLastCalledWith(false); - const callsBefore = - menuBuilder.setNoUpdateAvailableMenuVisibility.mock.calls.length; + const callsBefore = menuBuilder.setNoUpdateAvailableMenuVisibility.mock.calls.length; vi.advanceTimersByTime(APPLICATION.UPDATE_NOT_AVAILABLE_DISPLAY_MS * 2); // No additional hide call due to cleared timeout - expect( - menuBuilder.setNoUpdateAvailableMenuVisibility.mock.calls.length, - ).toBe(callsBefore); + expect(menuBuilder.setNoUpdateAvailableMenuVisibility.mock.calls.length).toBe(callsBefore); } finally { vi.useRealTimers(); } @@ -262,9 +231,7 @@ describe('main/updater.ts', () => { emit('update-cancelled'); expect(menubar.tray.setToolTip).toHaveBeenCalledWith(APPLICATION.NAME); - expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith( - true, - ); + expect(menuBuilder.setCheckForUpdatesMenuEnabled).toHaveBeenCalledWith(true); }); it('handles error (reset + logError)', async () => { @@ -273,28 +240,23 @@ describe('main/updater.ts', () => { const err = new Error('failure'); emit('error', err); - expect(logError).toHaveBeenCalledWith( - 'auto updater', - 'Error checking for update', - err, - ); + expect(logError).toHaveBeenCalledWith('auto updater', 'Error checking for update', err); expect(menubar.tray.setToolTip).toHaveBeenCalledWith(APPLICATION.NAME); }); it('performs initial check and schedules periodic checks', async () => { const originalSetInterval = globalThis.setInterval; - const setIntervalSpy = vi - .spyOn(globalThis, 'setInterval') - .mockImplementation(((fn: () => void) => { - fn(); - return 0 as unknown as NodeJS.Timeout; - }) as unknown as typeof setInterval); + const setIntervalSpy = vi.spyOn(globalThis, 'setInterval').mockImplementation((( + fn: () => void, + ) => { + fn(); + return 0 as unknown as NodeJS.Timeout; + }) as unknown as typeof setInterval); try { await updater.start(); // At minimum the initial check should have occurred - const callCount = vi.mocked(autoUpdater.checkForUpdatesAndNotify).mock - .calls.length; + const callCount = vi.mocked(autoUpdater.checkForUpdatesAndNotify).mock.calls.length; expect(callCount).toBeGreaterThanOrEqual(1); // If the periodic interval was scheduled during this test run, assert its arguments diff --git a/src/main/updater.ts b/src/main/updater.ts index 877ac7dfb..c8aac46d7 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -48,10 +48,7 @@ export default class AppUpdater { } if (!this.menubar.app.isPackaged) { - logInfo( - 'app updater', - 'Skipping updater since app is in development mode', - ); + logInfo('app updater', 'Skipping updater since app is in development mode'); return; } @@ -84,9 +81,7 @@ export default class AppUpdater { }); autoUpdater.on('download-progress', (progressObj) => { - this.setTooltipWithStatus( - `Downloading update: ${progressObj.percent.toFixed(2)}%`, - ); + this.setTooltipWithStatus(`Downloading update: ${progressObj.percent.toFixed(2)}%`); }); autoUpdater.on('update-downloaded', (event) => { @@ -151,10 +146,7 @@ export default class AppUpdater { // This avoids an immediate duplicate check on startup. setTimeout(async () => { await runScheduledCheck(); - this.periodicInterval = setInterval( - runScheduledCheck, - APPLICATION.UPDATE_CHECK_INTERVAL_MS, - ); + this.periodicInterval = setInterval(runScheduledCheck, APPLICATION.UPDATE_CHECK_INTERVAL_MS); }, APPLICATION.UPDATE_CHECK_INTERVAL_MS); } @@ -209,8 +201,7 @@ export default class AppUpdater { buttons: ['Restart', 'Later'], title: 'Application Update', message: `${APPLICATION.NAME} ${releaseName} has been downloaded`, - detail: - 'Restart to apply the update. You can also restart later from the tray menu.', + detail: 'Restart to apply the update. You can also restart later from the tray menu.', }; dialog.showMessageBox(dialogOpts).then((returnValue) => { diff --git a/src/main/utils.test.ts b/src/main/utils.test.ts index 753310b82..6354499d3 100644 --- a/src/main/utils.test.ts +++ b/src/main/utils.test.ts @@ -51,8 +51,7 @@ import { openLogsDirectory, takeScreenshot } from './utils'; function createMb() { return { window: { - capturePage: () => - Promise.resolve({ toPNG: () => Buffer.from('image-bytes') }), + capturePage: () => Promise.resolve({ toPNG: () => Buffer.from('image-bytes') }), }, app: { quit: vi.fn() }, }; diff --git a/src/preload/index.test.ts b/src/preload/index.test.ts index 2f5756ace..e8f8dfd68 100644 --- a/src/preload/index.test.ts +++ b/src/preload/index.test.ts @@ -38,8 +38,7 @@ vi.mock('../shared/logger', () => ({ vi.mock('electron', () => ({ contextBridge: { - exposeInMainWorld: (key: string, value: unknown) => - exposeInMainWorldMock(key, value), + exposeInMainWorld: (key: string, value: unknown) => exposeInMainWorldMock(key, value), }, webFrame: { getZoomLevel: () => getZoomLevelMock(), @@ -63,8 +62,7 @@ class MockNotification { } // Attach to global before importing preload -(global as unknown as { Notification: unknown }).Notification = - MockNotification; +(global as unknown as { Notification: unknown }).Notification = MockNotification; interface TestApi { tray: { @@ -107,16 +105,12 @@ describe('preload/index', () => { api.tray.updateColor(5, 'online', 'default', 'active'); - expect(sendMainEventMock).toHaveBeenNthCalledWith( - 1, - EVENTS.UPDATE_ICON_COLOR, - { - notificationsCount: 5, - appState: 'online', - idleIconVariant: 'default', - unreadIconVariant: 'active', - }, - ); + expect(sendMainEventMock).toHaveBeenNthCalledWith(1, EVENTS.UPDATE_ICON_COLOR, { + notificationsCount: 5, + appState: 'online', + idleIconVariant: 'default', + unreadIconVariant: 'active', + }); }); it('openExternalLink sends event with payload', async () => { @@ -155,10 +149,7 @@ describe('preload/index', () => { const callback = vi.fn(); api.onSystemThemeUpdate(callback); - expect(onRendererEventMock).toHaveBeenCalledWith( - EVENTS.UPDATE_THEME, - expect.any(Function), - ); + expect(onRendererEventMock).toHaveBeenCalledWith(EVENTS.UPDATE_THEME, expect.any(Function)); // Simulate event const listener = onRendererEventMock.mock.calls[0][1]; @@ -171,10 +162,7 @@ describe('preload/index', () => { it('raiseNativeNotification without url calls app.show', async () => { const api = getExposedApi(); - const notification = api.raiseNativeNotification( - 'Title', - 'Body', - ) as MockNotification; + const notification = api.raiseNativeNotification('Title', 'Body') as MockNotification; notification.triggerClick(); diff --git a/src/preload/index.ts b/src/preload/index.ts index 4e6fb0f24..ad8be181b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -37,8 +37,7 @@ export const api = { * @param value - The plaintext string to encrypt. * @returns A promise resolving to the encrypted string. */ - encryptValue: (value: string) => - invokeMainEvent(EVENTS.SAFE_STORAGE_ENCRYPT, value), + encryptValue: (value: string) => invokeMainEvent(EVENTS.SAFE_STORAGE_ENCRYPT, value), /** * Decrypt an encrypted string using Electron's safe storage. @@ -46,8 +45,7 @@ export const api = { * @param value - The encrypted string to decrypt. * @returns A promise resolving to the plaintext string. */ - decryptValue: (value: string) => - invokeMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, value), + decryptValue: (value: string) => invokeMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, value), /** * Enable or disable launching the application at system login. @@ -221,10 +219,8 @@ export const api = { /** Aptabase analytics helpers. */ aptabase: { - trackEvent: ( - eventName: string, - props?: Record, - ) => sendMainEvent(EVENTS.APTABASE_TRACK_EVENT, { eventName, props }), + trackEvent: (eventName: string, props?: Record) => + sendMainEvent(EVENTS.APTABASE_TRACK_EVENT, { eventName, props }), }, }; @@ -235,9 +231,6 @@ export const api = { try { contextBridge.exposeInMainWorld('atlassify', api); } catch (err) { - // biome-ignore lint/suspicious/noConsole: preload environment is strictly sandboxed - console.error( - '[preload] Failed to expose Atlassify Bridge API to renderer', - err, - ); + // oxlint-disable-next-line no-console -- preload environment is strictly sandboxed + console.error('[preload] Failed to expose Atlassify Bridge API to renderer', err); } diff --git a/src/preload/utils.test.ts b/src/preload/utils.test.ts index 5a24810b4..943b31630 100644 --- a/src/preload/utils.test.ts +++ b/src/preload/utils.test.ts @@ -32,10 +32,7 @@ describe('preload/utils', () => { it('sendMainEvent forwards to ipcRenderer.send', () => { sendMainEvent(EVENTS.WINDOW_SHOW); - expect(ipcRenderer.send).toHaveBeenCalledWith( - EVENTS.WINDOW_SHOW, - undefined, - ); + expect(ipcRenderer.send).toHaveBeenCalledWith(EVENTS.WINDOW_SHOW, undefined); }); it('invokeMainEvent forwards and resolves', async () => { @@ -49,10 +46,7 @@ describe('preload/utils', () => { const handlerMock = vi.fn(); onRendererEvent( EVENTS.UPDATE_ICON_TITLE, - handlerMock as unknown as ( - e: Electron.IpcRendererEvent, - args: string, - ) => void, + handlerMock as unknown as (e: Electron.IpcRendererEvent, args: string) => void, ); ( ipcRenderer as unknown as { @@ -60,10 +54,7 @@ describe('preload/utils', () => { } ).__emit(EVENTS.UPDATE_ICON_TITLE, 'payload'); - expect(ipcRenderer.on).toHaveBeenCalledWith( - EVENTS.UPDATE_ICON_TITLE, - handlerMock, - ); + expect(ipcRenderer.on).toHaveBeenCalledWith(EVENTS.UPDATE_ICON_TITLE, handlerMock); expect(handlerMock).toHaveBeenCalledWith({}, 'payload'); }); }); diff --git a/src/preload/utils.ts b/src/preload/utils.ts index 43b90383e..026c9b702 100644 --- a/src/preload/utils.ts +++ b/src/preload/utils.ts @@ -19,14 +19,11 @@ export function sendMainEvent(event: EventType, data?: EventData): void { * @param data - Optional string payload to include with the event. * @returns A promise that resolves to the string response from the main process. */ -export async function invokeMainEvent( - event: EventType, - data?: EventData, -): Promise { +export async function invokeMainEvent(event: EventType, data?: EventData): Promise { try { return await ipcRenderer.invoke(event, data); } catch (err) { - // biome-ignore lint/suspicious/noConsole: preload environment is strictly sandboxed + // oxlint-disable-next-line no-console -- preload environment is strictly sandboxed console.error(`[IPC] invoke failed: ${event}`, err); throw err; } diff --git a/src/renderer/App.css b/src/renderer/App.css index f97c61e32..0e67d2e84 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -1,5 +1,5 @@ /** Tailwind CSS */ -@import "tailwindcss"; +@import 'tailwindcss'; /** Tailwind CSS Configuration */ @config "../../tailwind.config.mts"; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 3ee4e45a4..5ed87cb16 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,16 +1,9 @@ import { type FC, useEffect } from 'react'; -import { - Navigate, - Route, - HashRouter as Router, - Routes, - useLocation, -} from 'react-router-dom'; +import { Navigate, Route, HashRouter as Router, Routes, useLocation } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; import './App.css'; - import { AppProvider } from './context/AppContext'; import { AccountsRoute } from './routes/Accounts'; import { FiltersRoute } from './routes/Filters'; @@ -35,11 +28,7 @@ function RequireAuth({ children }) { const isLoggedIn = useAccountsStore((s) => s.isLoggedIn()); - return isLoggedIn ? ( - children - ) : ( - - ); + return isLoggedIn ? children : ; } export const App: FC = () => { diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx index fccd324d0..c17a0ba61 100644 --- a/src/renderer/__helpers__/test-utils.tsx +++ b/src/renderer/__helpers__/test-utils.tsx @@ -1,8 +1,4 @@ -import { - type RenderHookOptions, - render, - renderHook, -} from '@testing-library/react'; +import { type RenderHookOptions, render, renderHook } from '@testing-library/react'; import { type ReactElement, type ReactNode, useMemo } from 'react'; import { MemoryRouter } from 'react-router-dom'; @@ -11,11 +7,7 @@ import axios from 'axios'; import { AppContext, type AppContextState } from '../context/AppContext'; import { useAccountsStore, useFiltersStore, useSettingsStore } from '../stores'; -import type { - AccountsStore, - FiltersStore, - SettingsStore, -} from '../stores/types'; +import type { AccountsStore, FiltersStore, SettingsStore } from '../stores/types'; export { navigateMock } from './vitest.setup'; @@ -64,9 +56,7 @@ function AppContextProvider({ return ( - - {children} - + {children} ); } @@ -113,13 +103,7 @@ export function renderHookWithProviders( */ export function renderWithProviders( ui: ReactElement, - { - initialEntries, - accounts, - settings, - filters, - ...context - }: RenderOptions = {}, + { initialEntries, accounts, settings, filters, ...context }: RenderOptions = {}, ) { if (accounts) { useAccountsStore.setState(accounts); diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts index 60a877c03..883619ec0 100644 --- a/src/renderer/__helpers__/vitest.setup.ts +++ b/src/renderer/__helpers__/vitest.setup.ts @@ -1,5 +1,4 @@ import '@testing-library/jest-dom/vitest'; - import { mockAtlassianCloudAccount } from '../__mocks__/account-mocks'; import { useAccountsStore, useFiltersStore, useSettingsStore } from '../stores'; @@ -21,9 +20,7 @@ vi.mock('../utils/core/random', () => ({ vi.mock('axios', async (importOriginal) => { const actual = await importOriginal(); const realAxios = actual.default; - const wrapped = vi.fn((...args: Parameters) => - realAxios(...args), - ); + const wrapped = vi.fn((...args: Parameters) => realAxios(...args)); Object.assign(wrapped, realAxios); diff --git a/src/renderer/__mocks__/account-mocks.ts b/src/renderer/__mocks__/account-mocks.ts index 9c998b436..e7cef73d1 100644 --- a/src/renderer/__mocks__/account-mocks.ts +++ b/src/renderer/__mocks__/account-mocks.ts @@ -23,9 +23,7 @@ export const mockAtlassianCloudAccountTwo: Account = { avatar: 'https://avatar.atlassify.io' as Link, }; -export function createMockAccountWithError( - error: AtlassifyError, -): AccountNotifications { +export function createMockAccountWithError(error: AtlassifyError): AccountNotifications { return { account: mockAtlassianCloudAccount, notifications: [], diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index 3e0cc69ee..e5d2e38bf 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -1,9 +1,4 @@ -import type { - AccountNotifications, - AtlassifyNotification, - Link, - ProductType, -} from '../types'; +import type { AccountNotifications, AtlassifyNotification, Link, ProductType } from '../types'; import { PRODUCTS } from '../utils/products'; import { mockAtlassianCloudAccount } from './account-mocks'; @@ -79,8 +74,7 @@ export const mockAtlassifyNotifications: AtlassifyNotification[] = [ }, ]; -export const mockSingleAtlassifyNotification: AtlassifyNotification = - mockAtlassifyNotifications[0]; +export const mockSingleAtlassifyNotification: AtlassifyNotification = mockAtlassifyNotifications[0]; export const mockAccountNotifications: AccountNotifications[] = [ { diff --git a/src/renderer/components/AllRead.tsx b/src/renderer/components/AllRead.tsx index ea440049a..84ab53113 100644 --- a/src/renderer/components/AllRead.tsx +++ b/src/renderer/components/AllRead.tsx @@ -16,9 +16,7 @@ export const AllRead: FC = () => { const emoji = useMemo(() => randomElement(Constants.EMOJIS.ALL_READ), []); - const heading = hasFilters - ? t('allRead.headingFiltered') - : t('allRead.heading'); + const heading = hasFilters ? t('allRead.headingFiltered') : t('allRead.heading'); return ; }; diff --git a/src/renderer/components/GlobalShortcuts.test.tsx b/src/renderer/components/GlobalShortcuts.test.tsx index 63820ca76..ce3d0d693 100644 --- a/src/renderer/components/GlobalShortcuts.test.tsx +++ b/src/renderer/components/GlobalShortcuts.test.tsx @@ -71,9 +71,7 @@ describe('components/GlobalShortcuts.tsx', () => { await userEvent.keyboard('u'); - expect(toggleSettingsSpy).toHaveBeenCalledWith( - 'fetchOnlyUnreadNotifications', - ); + expect(toggleSettingsSpy).toHaveBeenCalledWith('fetchOnlyUnreadNotifications'); }); it('does not toggle read/unread when logged out', async () => { @@ -105,9 +103,7 @@ describe('components/GlobalShortcuts.tsx', () => { await userEvent.keyboard('p'); - expect(toggleSettingsSpy).toHaveBeenCalledWith( - 'groupNotificationsByProduct', - ); + expect(toggleSettingsSpy).toHaveBeenCalledWith('groupNotificationsByProduct'); }); it('does not toggle group by product when logged out', async () => { @@ -129,9 +125,7 @@ describe('components/GlobalShortcuts.tsx', () => { await userEvent.keyboard('t'); - expect(toggleSettingsSpy).toHaveBeenCalledWith( - 'groupNotificationsByTitle', - ); + expect(toggleSettingsSpy).toHaveBeenCalledWith('groupNotificationsByTitle'); }); it('does not toggle group by title when logged out', async () => { @@ -268,9 +262,7 @@ describe('components/GlobalShortcuts.tsx', () => { , ); - const input = document.getElementById( - 'test-input', - ) as HTMLTextAreaElement; + const input = document.getElementById('test-input') as HTMLTextAreaElement; input.focus(); await userEvent.type(input, 'h'); @@ -285,9 +277,7 @@ describe('components/GlobalShortcuts.tsx', () => { , ); - const textarea = document.getElementById( - 'test-textarea', - ) as HTMLTextAreaElement; + const textarea = document.getElementById('test-textarea') as HTMLTextAreaElement; textarea.focus(); await userEvent.type(textarea, 'h'); diff --git a/src/renderer/components/GlobalShortcuts.tsx b/src/renderer/components/GlobalShortcuts.tsx index 5defa7059..71813aebf 100644 --- a/src/renderer/components/GlobalShortcuts.tsx +++ b/src/renderer/components/GlobalShortcuts.tsx @@ -3,10 +3,7 @@ import { type FC, useEffect } from 'react'; import { useAppContext } from '../hooks/useAppContext'; import { useGlobalShortcuts } from '../hooks/useGlobalShortcuts'; -import { - getNormalizedKey, - shouldIgnoreKeyboardEvent, -} from '../utils/ui/keyboard'; +import { getNormalizedKey, shouldIgnoreKeyboardEvent } from '../utils/ui/keyboard'; /** * Component that registers global keyboard shortcuts for the renderer app. @@ -19,10 +16,7 @@ export const GlobalShortcuts: FC = () => { useEffect(() => { const keyToName = new Map( - Object.entries(shortcuts).map(([name, cfg]) => [ - cfg.key, - name as keyof typeof shortcuts, - ]), + Object.entries(shortcuts).map(([name, cfg]) => [cfg.key, name as keyof typeof shortcuts]), ); const handler = (event: KeyboardEvent) => { diff --git a/src/renderer/components/Sidebar.test.tsx b/src/renderer/components/Sidebar.test.tsx index 8dc85171c..2bc883033 100644 --- a/src/renderer/components/Sidebar.test.tsx +++ b/src/renderer/components/Sidebar.test.tsx @@ -26,9 +26,7 @@ const mockThemeObserverColorMode = (mode: 'light' | 'dark') => { describe('renderer/components/Sidebar.tsx', () => { const fetchNotificationsMock = vi.fn(); - const openExternalLinkSpy = vi - .spyOn(comms, 'openExternalLink') - .mockImplementation(vi.fn()); + const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); describe('logged in', () => { it('should render itself & its children - light mode', () => { @@ -92,9 +90,7 @@ describe('renderer/components/Sidebar.tsx', () => { await userEvent.click(screen.getByTestId('sidebar-notifications')); - expect(openExternalLinkSpy).toHaveBeenCalledWith( - 'https://team.atlassian.com/notifications', - ); + expect(openExternalLinkSpy).toHaveBeenCalledWith('https://team.atlassian.com/notifications'); }); it('renders correct icon when there are no notifications', () => { @@ -142,17 +138,11 @@ describe('renderer/components/Sidebar.tsx', () => { it('should toggle show only unread notifications', async () => { renderWithProviders(); - expect( - screen.getByTestId('sidebar-toggle-unread-only--input'), - ).toBeChecked(); + expect(screen.getByTestId('sidebar-toggle-unread-only--input')).toBeChecked(); - await userEvent.click( - screen.getByTestId('sidebar-toggle-unread-only--toggle-cross-icon'), - ); + await userEvent.click(screen.getByTestId('sidebar-toggle-unread-only--toggle-cross-icon')); - expect( - screen.getByTestId('sidebar-toggle-unread-only--input'), - ).not.toBeChecked(); + expect(screen.getByTestId('sidebar-toggle-unread-only--input')).not.toBeChecked(); }); }); @@ -214,9 +204,7 @@ describe('renderer/components/Sidebar.tsx', () => { await userEvent.click(screen.getByTestId('sidebar-filter-notifications')); - expect( - screen.getByTestId('sidebar-filter-notifications'), - ).toMatchSnapshot(); + expect(screen.getByTestId('sidebar-filter-notifications')).toMatchSnapshot(); expect(navigateMock).toHaveBeenCalledTimes(1); expect(navigateMock).toHaveBeenCalledWith('/filters'); }); @@ -228,9 +216,7 @@ describe('renderer/components/Sidebar.tsx', () => { await userEvent.click(screen.getByTestId('sidebar-filter-notifications')); - expect( - screen.getByTestId('sidebar-filter-notifications'), - ).toMatchSnapshot(); + expect(screen.getByTestId('sidebar-filter-notifications')).toMatchSnapshot(); expect(navigateMock).toHaveBeenCalledTimes(1); expect(navigateMock).toHaveBeenCalledWith('/', { replace: true }); }); @@ -240,9 +226,7 @@ describe('renderer/components/Sidebar.tsx', () => { filters: { products: ['bitbucket'] }, }); - expect( - screen.getByTestId('sidebar-filter-notifications'), - ).toMatchSnapshot(); + expect(screen.getByTestId('sidebar-filter-notifications')).toMatchSnapshot(); }); }); diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index 159ab6aeb..1fb6d4ea7 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -43,15 +43,9 @@ const SidebarComponent: FC = () => { const isLoggedIn = useAccountsStore((s) => s.isLoggedIn()); // Setting store values - const fetchOnlyUnreadNotifications = useSettingsStore( - (s) => s.fetchOnlyUnreadNotifications, - ); - const groupNotificationsByProduct = useSettingsStore( - (s) => s.groupNotificationsByProduct, - ); - const groupNotificationsByTitle = useSettingsStore( - (s) => s.groupNotificationsByTitle, - ); + const fetchOnlyUnreadNotifications = useSettingsStore((s) => s.fetchOnlyUnreadNotifications); + const groupNotificationsByProduct = useSettingsStore((s) => s.groupNotificationsByProduct); + const groupNotificationsByTitle = useSettingsStore((s) => s.groupNotificationsByTitle); // Filter store values const hasFilters = useFiltersStore((s) => s.hasActiveFilters()); @@ -66,11 +60,7 @@ const SidebarComponent: FC = () => { - + } @@ -95,10 +85,7 @@ const SidebarComponent: FC = () => { ( - + )} label={t('sidebar.notifications.label')} onClick={() => shortcuts.myNotifications.action()} @@ -131,14 +118,9 @@ const SidebarComponent: FC = () => { shortcut={[shortcuts.groupByProduct.key]} > ( - + )} label={t('sidebar.toggles.groupByProduct.label')} onClick={() => shortcuts.groupByProduct.action()} @@ -154,14 +136,9 @@ const SidebarComponent: FC = () => { shortcut={[shortcuts.groupByTitle.key]} > ( - + )} label={t('sidebar.toggles.groupByTitle.label')} onClick={() => shortcuts.groupByTitle.action()} @@ -179,10 +156,7 @@ const SidebarComponent: FC = () => { ( - + )} label={t('sidebar.filters.label')} onClick={() => shortcuts.filters.action()} @@ -215,10 +189,7 @@ const SidebarComponent: FC = () => { size="medium" /> ) : ( - + ) } isDisabled={isFetching} @@ -237,10 +208,7 @@ const SidebarComponent: FC = () => { ( - + )} label={t('sidebar.settings.label')} onClick={() => shortcuts.settings.action()} @@ -260,10 +228,7 @@ const SidebarComponent: FC = () => { ( - + )} label={t('sidebar.quit.label', { appName: APPLICATION.NAME, diff --git a/src/renderer/components/filters/FilterSection.test.tsx b/src/renderer/components/filters/FilterSection.test.tsx index 01c0a25d7..e0f00c38b 100644 --- a/src/renderer/components/filters/FilterSection.test.tsx +++ b/src/renderer/components/filters/FilterSection.test.tsx @@ -47,11 +47,7 @@ describe('renderer/components/filters/FilterSection.tsx', () => { await userEvent.click(screen.getByLabelText('Mentions')); - expect(updateFilterSpy).toHaveBeenCalledWith( - mockFilterSetting, - 'mention', - true, - ); + expect(updateFilterSpy).toHaveBeenCalledWith(mockFilterSetting, 'mention', true); }); it('should be able to toggle filter value - some filters already set', async () => { @@ -69,10 +65,6 @@ describe('renderer/components/filters/FilterSection.tsx', () => { await userEvent.click(screen.getByLabelText('Comments')); - expect(updateFilterSpy).toHaveBeenCalledWith( - mockFilterSetting, - 'comment', - true, - ); + expect(updateFilterSpy).toHaveBeenCalledWith(mockFilterSetting, 'comment', true); }); }); diff --git a/src/renderer/components/filters/FilterSection.tsx b/src/renderer/components/filters/FilterSection.tsx index 8fc887023..32afac450 100644 --- a/src/renderer/components/filters/FilterSection.tsx +++ b/src/renderer/components/filters/FilterSection.tsx @@ -33,9 +33,7 @@ const FilterSectionComponent = ({ // Memoize filter counts to avoid recalculating on every render const filterCounts = useMemo(() => { const counts = new Map(); - for (const type of Object.keys( - filter.FILTER_TYPES, - ) as FiltersState[K][number][]) { + for (const type of Object.keys(filter.FILTER_TYPES) as FiltersState[K][number][]) { counts.set(type, filter.getFilterCount(notifications, type)); } return counts; @@ -45,67 +43,56 @@ const FilterSectionComponent = ({ {title} - {(Object.keys(filter.FILTER_TYPES) as FiltersState[K][number][]).map( - (type) => { - const typeDetails = filter.getTypeDetails(type); - const typeLabel = formatProperCase(typeDetails.name); - const isChecked = filter.isFilterSet(type); - const count = filterCounts.get(type) ?? 0; + {(Object.keys(filter.FILTER_TYPES) as FiltersState[K][number][]).map((type) => { + const typeDetails = filter.getTypeDetails(type); + const typeLabel = formatProperCase(typeDetails.name); + const isChecked = filter.isFilterSet(type); + const count = filterCounts.get(type) ?? 0; - return ( - - updateFilter(filterSetting, type, !isChecked)} + return ( + + updateFilter(filterSetting, type, !isChecked)} + /> + {typeDetails.icon && ( + - {typeDetails.icon && ( - - )} - {typeDetails.logo && ( - - )} - {typeDetails.heroicon && ( - - )} - - {count} - - - ); - }, - )} + )} + {typeDetails.logo && ( + + )} + {typeDetails.heroicon && ( + + )} + + {count} + + + ); + })} ); }; // Memoize the component to prevent unnecessary re-renders -export const FilterSection = memo( - FilterSectionComponent, -) as typeof FilterSectionComponent; +export const FilterSection = memo(FilterSectionComponent) as typeof FilterSectionComponent; diff --git a/src/renderer/components/layout/Contents.tsx b/src/renderer/components/layout/Contents.tsx index 081429b4a..a64c30147 100644 --- a/src/renderer/components/layout/Contents.tsx +++ b/src/renderer/components/layout/Contents.tsx @@ -9,9 +9,5 @@ interface ContentsProps { * It provides proper padding and handles scrolling. */ export const Contents: FC = (props: ContentsProps) => { - return ( -
- {props.children} -
- ); + return
{props.children}
; }; diff --git a/src/renderer/components/layout/EmojiSplash.test.tsx b/src/renderer/components/layout/EmojiSplash.test.tsx index fb7c1111e..a744ccc5f 100644 --- a/src/renderer/components/layout/EmojiSplash.test.tsx +++ b/src/renderer/components/layout/EmojiSplash.test.tsx @@ -10,9 +10,7 @@ describe('renderer/components/layout/EmojiSplash.tsx', () => { await act(async () => { tree = await act(async () => { - return renderWithProviders( - , - ); + return renderWithProviders(); }); }); @@ -25,11 +23,7 @@ describe('renderer/components/layout/EmojiSplash.tsx', () => { await act(async () => { tree = await act(async () => { return renderWithProviders( - , + , ); }); }); diff --git a/src/renderer/components/notifications/AccountNotifications.test.tsx b/src/renderer/components/notifications/AccountNotifications.test.tsx index 6ef6087c1..6fb62f102 100644 --- a/src/renderer/components/notifications/AccountNotifications.test.tsx +++ b/src/renderer/components/notifications/AccountNotifications.test.tsx @@ -7,10 +7,7 @@ import { mockAtlassifyNotifications } from '../../__mocks__/notifications-mocks' import * as links from '../../utils/system/links'; import * as theme from '../../utils/ui/theme'; -import { - AccountNotifications, - type AccountNotificationsProps, -} from './AccountNotifications'; +import { AccountNotifications, type AccountNotificationsProps } from './AccountNotifications'; vi.mock('./ProductNotifications', () => ({ ProductNotifications: () =>
ProductNotifications
, @@ -136,15 +133,9 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { renderWithProviders(); - expect( - screen.queryByTestId('account-profile--itemInner'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('account-pull-requests'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('account-mark-as-read'), - ).not.toBeInTheDocument(); + expect(screen.queryByTestId('account-profile--itemInner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('account-pull-requests')).not.toBeInTheDocument(); + expect(screen.queryByTestId('account-mark-as-read')).not.toBeInTheDocument(); expect(screen.queryByTestId('account-toggle')).not.toBeInTheDocument(); }); }); @@ -152,9 +143,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { describe('account actions', () => { it('should open profile when clicked', async () => { - const openAccountProfileSpy = vi - .spyOn(links, 'openAccountProfile') - .mockImplementation(vi.fn()); + const openAccountProfileSpy = vi.spyOn(links, 'openAccountProfile').mockImplementation(vi.fn()); const props: AccountNotificationsProps = { account: mockAtlassianCloudAccount, @@ -171,15 +160,11 @@ describe('account actions', () => { await userEvent.click(screen.getByTestId('account-profile--itemInner')); expect(openAccountProfileSpy).toHaveBeenCalledTimes(1); - expect(openAccountProfileSpy).toHaveBeenCalledWith( - mockAtlassianCloudAccount, - ); + expect(openAccountProfileSpy).toHaveBeenCalledWith(mockAtlassianCloudAccount); }); it('should open my pull requests', async () => { - const openPullRequestsSpy = vi - .spyOn(links, 'openMyPullRequests') - .mockImplementation(vi.fn()); + const openPullRequestsSpy = vi.spyOn(links, 'openMyPullRequests').mockImplementation(vi.fn()); const props: AccountNotificationsProps = { account: mockAtlassianCloudAccount, @@ -257,9 +242,7 @@ describe('account actions', () => { renderWithProviders(); }); - expect( - screen.getAllByTestId('notification-details').length, - ).toBeGreaterThan(0); + expect(screen.getAllByTestId('notification-details').length).toBeGreaterThan(0); await userEvent.click(screen.getByTestId('account-toggle')); diff --git a/src/renderer/components/notifications/AccountNotifications.tsx b/src/renderer/components/notifications/AccountNotifications.tsx index 6aa2e7eb9..efc6115dc 100644 --- a/src/renderer/components/notifications/AccountNotifications.tsx +++ b/src/renderer/components/notifications/AccountNotifications.tsx @@ -22,20 +22,13 @@ import { Constants } from '../../constants'; import { useAppContext } from '../../hooks/useAppContext'; import { useSettingsStore } from '../../stores'; -import type { - Account, - AtlassifyError, - AtlassifyNotification, -} from '../../types'; +import type { Account, AtlassifyError, AtlassifyNotification } from '../../types'; import { groupNotificationsByProductEntries, sortNotificationsByOrder, } from '../../utils/notifications/group'; -import { - openAccountProfile, - openMyPullRequests, -} from '../../utils/system/links'; +import { openAccountProfile, openMyPullRequests } from '../../utils/system/links'; import { getChevronDetails } from '../../utils/ui/display'; import { isLightMode } from '../../utils/ui/theme'; import { AllRead } from '../AllRead'; @@ -54,18 +47,15 @@ export interface AccountNotificationsProps { export const AccountNotifications: FC = ( props: AccountNotificationsProps, ) => { - const { account, notifications, hasMoreNotifications, showAccountHeader } = - props; + const { account, notifications, hasMoreNotifications, showAccountHeader } = props; const { t } = useTranslation(); const { markNotificationsRead } = useAppContext(); - const [isAccountNotificationsVisible, setIsAccountNotificationsVisible] = - useState(true); + const [isAccountNotificationsVisible, setIsAccountNotificationsVisible] = useState(true); - const [showMarkAccountAsReadModal, setShowMarkAccountAsReadModal] = - useState(false); + const [showMarkAccountAsReadModal, setShowMarkAccountAsReadModal] = useState(false); const actionOpenMarkAccountAsReadModal = () => { setShowMarkAccountAsReadModal(true); @@ -147,10 +137,7 @@ export const AccountNotifications: FC = ( > - + = ( - + ( - - )} + icon={(iconProps) => } label={t('notifications.account.pull_requests')} onClick={(event: MouseEvent) => { event.stopPropagation(); @@ -197,10 +179,7 @@ export const AccountNotifications: FC = ( /> - + } @@ -217,9 +196,7 @@ export const AccountNotifications: FC = ( ( - - )} + icon={(iconProps) => } label={Chevron.label} shape="circle" spacing="compact" @@ -238,14 +215,12 @@ export const AccountNotifications: FC = ( {!hasAccountNotifications && !props.error && } {groupByProduct - ? groupedNotifications.map( - ([productType, productNotifications]) => ( - - ), - ) + ? groupedNotifications.map(([productType, productNotifications]) => ( + + )) : sortedNotifications.map((notification) => ( = ( {showMarkAccountAsReadModal && ( - + = ( /> - - {t('common.are_you_sure')} - + {t('common.are_you_sure')}

{t('notifications.account.mark_read_confirm.description1')}{' '} - - {t('notifications.account.mark_read_confirm.description2')} - {' '} + {t('notifications.account.mark_read_confirm.description2')}{' '} {t('notifications.account.mark_read_confirm.description3')}

diff --git a/src/renderer/components/notifications/NotificationActions.test.tsx b/src/renderer/components/notifications/NotificationActions.test.tsx index a7ab2850e..9d20f3af2 100644 --- a/src/renderer/components/notifications/NotificationActions.test.tsx +++ b/src/renderer/components/notifications/NotificationActions.test.tsx @@ -3,10 +3,7 @@ import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../__helpers__/test-utils'; -import { - NotificationActions, - type NotificationActionsProps, -} from './NotificationActions'; +import { NotificationActions, type NotificationActionsProps } from './NotificationActions'; describe('renderer/components/notifications/NotificationActions.tsx', () => { it('renders mark-as-read button when unread and not animating', () => { @@ -45,12 +42,8 @@ describe('renderer/components/notifications/NotificationActions.tsx', () => { renderWithProviders(); - expect( - screen.queryByTestId('notification-mark-as-read'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('notification-mark-as-unread'), - ).not.toBeInTheDocument(); + expect(screen.queryByTestId('notification-mark-as-read')).not.toBeInTheDocument(); + expect(screen.queryByTestId('notification-mark-as-unread')).not.toBeInTheDocument(); }); it('calls onMarkAsRead when mark-as-read is clicked', async () => { diff --git a/src/renderer/components/notifications/NotificationActions.tsx b/src/renderer/components/notifications/NotificationActions.tsx index 8818bc5ff..ef583940b 100644 --- a/src/renderer/components/notifications/NotificationActions.tsx +++ b/src/renderer/components/notifications/NotificationActions.tsx @@ -26,18 +26,10 @@ export const NotificationActions: FC = ({ {!isAnimatingExit && (isUnread ? ( - + ( - - )} + icon={() => } label={t('notifications.interactions.mark_as_read')} onClick={onMarkAsRead} shape="circle" @@ -46,10 +38,7 @@ export const NotificationActions: FC = ({ /> ) : ( - + null} diff --git a/src/renderer/components/notifications/NotificationAvatar.test.tsx b/src/renderer/components/notifications/NotificationAvatar.test.tsx index b45b040b1..488970898 100644 --- a/src/renderer/components/notifications/NotificationAvatar.test.tsx +++ b/src/renderer/components/notifications/NotificationAvatar.test.tsx @@ -2,10 +2,7 @@ import { renderWithProviders } from '../../__helpers__/test-utils'; import { mockSingleAtlassifyNotification } from '../../__mocks__/notifications-mocks'; import { PRODUCTS } from '../../utils/products'; -import { - NotificationAvatar, - type NotificationAvatarProps, -} from './NotificationAvatar'; +import { NotificationAvatar, type NotificationAvatarProps } from './NotificationAvatar'; describe('renderer/components/notifications/NotificationAvatar.tsx', () => { it('renders actor avatar with circle appearance', () => { diff --git a/src/renderer/components/notifications/NotificationContent.test.tsx b/src/renderer/components/notifications/NotificationContent.test.tsx index 503a998e6..3ca66a7e7 100644 --- a/src/renderer/components/notifications/NotificationContent.test.tsx +++ b/src/renderer/components/notifications/NotificationContent.test.tsx @@ -4,15 +4,10 @@ import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../__helpers__/test-utils'; import { mockSingleAtlassifyNotification } from '../../__mocks__/notifications-mocks'; -import { - NotificationContent, - type NotificationContentProps, -} from './NotificationContent'; +import { NotificationContent, type NotificationContentProps } from './NotificationContent'; describe('renderer/components/notifications/NotificationContent.tsx', () => { - vi.spyOn(globalThis.Date, 'now').mockImplementation(() => - new Date('2024').valueOf(), - ); + vi.spyOn(globalThis.Date, 'now').mockImplementation(() => new Date('2024').valueOf()); it('renders notification message, body and footer', () => { const props: NotificationContentProps = { diff --git a/src/renderer/components/notifications/NotificationContent.tsx b/src/renderer/components/notifications/NotificationContent.tsx index bbfec25cd..75e165415 100644 --- a/src/renderer/components/notifications/NotificationContent.tsx +++ b/src/renderer/components/notifications/NotificationContent.tsx @@ -24,24 +24,20 @@ export const NotificationContent: FC = ({ }: NotificationContentProps) => { const updatedAt = formatNotificationUpdatedAt(notification); - const avatarGroup: AvatarProps[] = - notification.notificationGroup.additionalActors.map((actor) => ({ + const avatarGroup: AvatarProps[] = notification.notificationGroup.additionalActors.map( + (actor) => ({ key: actor.displayName, name: actor.displayName, href: '#', src: actor.avatarURL, - })); + }), + ); const displayGroupSize = notification.notificationGroup.size - 1; const displayUpdateVerbiage = displayGroupSize > 1 ? 'updates' : 'update'; return ( - +
@@ -58,15 +54,11 @@ export const NotificationContent: FC = ({ as="div" hidden={!bodyText} id="notification-entity" - paddingInlineStart={ - notification.entity.iconUrl ? 'space.0' : 'space.025' - } + paddingInlineStart={notification.entity.iconUrl ? 'space.0' : 'space.025'} > {notification.entity.iconUrl ? ( = ({ - - + + = ({ {notification.notificationGroup.size > 1 && ( - {notification.notificationGroup.additionalActors.length > - 0 && ( + {notification.notificationGroup.additionalActors.length > 0 && ( // @ts-expect-error We're forcing the xsmall size for Avatar Groups )} +{displayGroupSize}{' '} - {notification.notificationGroup.additionalActors.length > - 0 + {notification.notificationGroup.additionalActors.length > 0 ? `${displayUpdateVerbiage} from ${notification.notificationGroup.additionalActors[0].displayName}` : `other ${displayUpdateVerbiage}`} - {notification.notificationGroup.additionalActors.length > - 1 && ' and others'} + {notification.notificationGroup.additionalActors.length > 1 && ' and others'} )} diff --git a/src/renderer/components/notifications/NotificationRow.test.tsx b/src/renderer/components/notifications/NotificationRow.test.tsx index 39a92afbc..91734e5f8 100644 --- a/src/renderer/components/notifications/NotificationRow.test.tsx +++ b/src/renderer/components/notifications/NotificationRow.test.tsx @@ -13,9 +13,7 @@ import { NotificationRow, type NotificationRowProps } from './NotificationRow'; describe('renderer/components/notifications/NotificationRow.tsx', () => { vi.spyOn(links, 'openNotification').mockImplementation(vi.fn()); vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); - vi.spyOn(globalThis.Date, 'now').mockImplementation(() => - new Date('2024').valueOf(), - ); + vi.spyOn(globalThis.Date, 'now').mockImplementation(() => new Date('2024').valueOf()); describe('should render notifications', () => { it('standard notification', async () => { @@ -65,9 +63,7 @@ describe('renderer/components/notifications/NotificationRow.tsx', () => { // Trigger transitionEnd to complete the animation and execute mutation await act(async () => { - notificationElement?.dispatchEvent( - new Event('transitionend', { bubbles: true }), - ); + notificationElement?.dispatchEvent(new Event('transitionend', { bubbles: true })); }); expect(links.openNotification).toHaveBeenCalledTimes(1); @@ -114,9 +110,7 @@ describe('renderer/components/notifications/NotificationRow.tsx', () => { // Trigger transitionEnd to complete the animation and execute mutation await act(async () => { - notificationElement?.dispatchEvent( - new Event('transitionend', { bubbles: true }), - ); + notificationElement?.dispatchEvent(new Event('transitionend', { bubbles: true })); }); expect(markNotificationsReadMock).toHaveBeenCalledTimes(1); diff --git a/src/renderer/components/notifications/NotificationRow.tsx b/src/renderer/components/notifications/NotificationRow.tsx index bdac5ff93..aa3217cab 100644 --- a/src/renderer/components/notifications/NotificationRow.tsx +++ b/src/renderer/components/notifications/NotificationRow.tsx @@ -28,14 +28,9 @@ export const NotificationRow: FC = ({ }: NotificationRowProps) => { const markAsReadOnOpen = useSettingsStore((s) => s.markAsReadOnOpen); - const { - markNotificationsRead, - markNotificationsUnread, - focusedNotificationId, - } = useAppContext(); - - const [shouldAnimateNotificationExit, setShouldAnimateNotificationExit] = - useState(false); + const { markNotificationsRead, markNotificationsUnread, focusedNotificationId } = useAppContext(); + + const [shouldAnimateNotificationExit, setShouldAnimateNotificationExit] = useState(false); const [pendingMarkAsRead, setPendingMarkAsRead] = useState(false); const isFocused = focusedNotificationId === notification.id; @@ -75,10 +70,7 @@ export const NotificationRow: FC = ({ } }; - const isNotificationUnread = readStateFilter.filterNotification( - notification, - 'unread', - ); + const isNotificationUnread = readStateFilter.filterNotification(notification, 'unread'); const strategy = getProductStrategy(notification); @@ -94,8 +86,7 @@ export const NotificationRow: FC = ({ className={cn( 'border-b border-atlassify-notifications hover:bg-atlassify-notifications', isFocused && 'bg-atlassify-notifications', - (isProductAnimatingExit || shouldAnimateNotificationExit) && - 'notification-exit', + (isProductAnimatingExit || shouldAnimateNotificationExit) && 'notification-exit', )} data-notification-id={notification.id} data-notification-row="true" diff --git a/src/renderer/components/notifications/ProductNotifications.test.tsx b/src/renderer/components/notifications/ProductNotifications.test.tsx index febf91458..a465dc5f3 100644 --- a/src/renderer/components/notifications/ProductNotifications.test.tsx +++ b/src/renderer/components/notifications/ProductNotifications.test.tsx @@ -6,18 +6,13 @@ import { mockAtlassifyNotifications } from '../../__mocks__/notifications-mocks' import * as comms from '../../utils/system/comms'; import * as theme from '../../utils/ui/theme'; -import { - ProductNotifications, - type ProductNotificationsProps, -} from './ProductNotifications'; +import { ProductNotifications, type ProductNotificationsProps } from './ProductNotifications'; vi.mock('./NotificationRow', () => ({ NotificationRow: () =>
NotificationRow
, })); -const openExternalLinkSpy = vi - .spyOn(comms, 'openExternalLink') - .mockImplementation(vi.fn()); +const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); describe('renderer/components/notifications/ProductNotifications.tsx', () => { it('should render itself & its children - light mode', () => { @@ -69,9 +64,7 @@ describe('renderer/components/notifications/ProductNotifications.tsx', () => { await act(async () => { renderWithProviders( - , + , ); }); diff --git a/src/renderer/components/notifications/ProductNotifications.tsx b/src/renderer/components/notifications/ProductNotifications.tsx index f6708d443..8a048d4fc 100644 --- a/src/renderer/components/notifications/ProductNotifications.tsx +++ b/src/renderer/components/notifications/ProductNotifications.tsx @@ -21,18 +21,14 @@ export interface ProductNotificationsProps { productNotifications: AtlassifyNotification[]; } -export const ProductNotifications: FC = ({ - productNotifications, -}) => { +export const ProductNotifications: FC = ({ productNotifications }) => { const { t } = useTranslation(); const { markNotificationsRead } = useAppContext(); - const [shouldAnimateProductExit, setShouldAnimateProductExit] = - useState(false); + const [shouldAnimateProductExit, setShouldAnimateProductExit] = useState(false); const [pendingMarkAsRead, setPendingMarkAsRead] = useState(false); - const [isProductNotificationsVisible, setIsProductNotificationsVisible] = - useState(true); + const [isProductNotificationsVisible, setIsProductNotificationsVisible] = useState(true); // We assume that productNotifications are all of the same product-type, as grouped within AccountNotifications const productNotification = productNotifications[0].product; @@ -65,11 +61,7 @@ export const ProductNotifications: FC = ({ setIsProductNotificationsVisible(!isProductNotificationsVisible); }; - const Chevron = getChevronDetails( - true, - isProductNotificationsVisible, - 'product', - ); + const Chevron = getChevronDetails(true, isProductNotificationsVisible, 'product'); const ChevronIcon = Chevron.icon; const boxStyles = xcss({ @@ -124,19 +116,14 @@ export const ProductNotifications: FC = ({ shouldUseNewLogoDesign size="xxsmall" /> - - {productNotification.display} - + {productNotification.display} {productNotifications.length}
- + } @@ -155,9 +142,7 @@ export const ProductNotifications: FC = ({ ( - - )} + icon={(iconProps) => } label={Chevron.label} shape="circle" spacing="compact" diff --git a/src/renderer/components/primitives/Footer.test.tsx b/src/renderer/components/primitives/Footer.test.tsx index 82533dad5..0a27623d1 100644 --- a/src/renderer/components/primitives/Footer.test.tsx +++ b/src/renderer/components/primitives/Footer.test.tsx @@ -4,9 +4,7 @@ import { Footer } from './Footer'; describe('renderer/components/primitives/Footer.tsx', () => { it('should render itself & its children - space-between', () => { - const tree = renderWithProviders( -
Test
, - ); + const tree = renderWithProviders(
Test
); expect(tree.container).toMatchSnapshot(); }); diff --git a/src/renderer/components/primitives/Footer.tsx b/src/renderer/components/primitives/Footer.tsx index 8209f3665..b654b9822 100644 --- a/src/renderer/components/primitives/Footer.tsx +++ b/src/renderer/components/primitives/Footer.tsx @@ -13,11 +13,7 @@ export const Footer: FC = (props: FooterProps) => { }); return ( - + {props.children} ); diff --git a/src/renderer/components/primitives/Header.test.tsx b/src/renderer/components/primitives/Header.test.tsx index 51fbec0ab..f908caee8 100644 --- a/src/renderer/components/primitives/Header.test.tsx +++ b/src/renderer/components/primitives/Header.test.tsx @@ -1,10 +1,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - navigateMock, - renderWithProviders, -} from '../../__helpers__/test-utils'; +import { navigateMock, renderWithProviders } from '../../__helpers__/test-utils'; import { Header } from './Header'; diff --git a/src/renderer/components/primitives/Header.tsx b/src/renderer/components/primitives/Header.tsx index 16366a4ac..d9a457e91 100644 --- a/src/renderer/components/primitives/Header.tsx +++ b/src/renderer/components/primitives/Header.tsx @@ -20,11 +20,7 @@ export const Header: FC = (props: HeaderProps) => { const { fetchNotifications } = useAppContext(); return ( - + { }); it('should update the zoom values when using the zoom buttons', async () => { - const zoomOutSpy = vi - .spyOn(zoom, 'decreaseZoom') - .mockImplementation(vi.fn()); - const zoomInSpy = vi - .spyOn(zoom, 'increaseZoom') - .mockImplementation(vi.fn()); - const zoomResetSpy = vi - .spyOn(zoom, 'resetZoomLevel') - .mockImplementation(vi.fn()); + const zoomOutSpy = vi.spyOn(zoom, 'decreaseZoom').mockImplementation(vi.fn()); + const zoomInSpy = vi.spyOn(zoom, 'increaseZoom').mockImplementation(vi.fn()); + const zoomResetSpy = vi.spyOn(zoom, 'resetZoomLevel').mockImplementation(vi.fn()); // Zoom Out await userEvent.click(screen.getByTestId('settings-zoom-out')); diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx index d3d4a04d7..fcc2f39a2 100644 --- a/src/renderer/components/settings/AppearanceSettings.tsx +++ b/src/renderer/components/settings/AppearanceSettings.tsx @@ -74,9 +74,7 @@ export const AppearanceSettings: FC = () => { {t('settings.appearance.language')}: