Compare commits

..

1 Commits

Author SHA1 Message Date
alexsparkes
05cc274b27 Refactor: welcome modal and initial background loader 2026-01-30 23:39:45 +00:00
336 changed files with 7618 additions and 26550 deletions

View File

@@ -1,5 +0,0 @@
{
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true
}
}

View File

@@ -1,8 +0,0 @@
{
"permissions": {
"allow": [
"Bash(perl -i -pe:*)",
"Bash(bun run:*)"
]
}
}

View File

@@ -1,153 +0,0 @@
# Mue Project Guidelines for GitHub Copilot
## Project Overview
Mue is a fast, open-source browser extension that provides a customizable new tab page for Chrome, Firefox, Edge, and Safari. Built with React and Vite, it emphasizes privacy, performance, and extensibility.
## Technical Stack
- **Framework**: React 19 with hooks and functional components
- **Build Tool**: Vite with SWC
- **Package Manager**: Bun >= 1.3.0 (ALWAYS use Bun, never npm or yarn)
- **Styling**: SCSS with component-scoped styles
- **Target**: Browser extensions (Manifest V3)
- **Testing**: Manual testing across Chrome, Firefox, and Safari
## Code Style & Quality
- Follow ESLint configuration in `eslint.config.js`
- Run `bun run lint:fix` before committing
- Run `bun run pretty` to format code with Prettier
- Use conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, etc.
- All commits must pass commitlint validation
## File Organization
```
src/
├── components/ # Reusable UI components (Elements, Form, Layout)
├── features/ # Feature-specific code (background, weather, etc.)
├── utils/ # Utility functions and helpers
├── contexts/ # React Context providers
├── hooks/ # Custom React hooks
├── i18n/ # Internationalization files
└── scss/ # Global styles, variables, mixins
```
## Critical Rules
### 1. Translation Files (IMPORTANT!)
- **`en_GB.json` is the base translation file**
- Never edit other locale files directly
- Workflow: Edit `en_GB.json` → Run `bun run translations`
- This ensures consistent formatting across all 30+ locales
- Translation files are in `src/i18n/locales/`
### 2. Branch Strategy
```
dev (active development)
beta (release candidates)
main (production/stable)
```
- All PRs target the `dev` branch
- Never commit directly to `beta` or `main`
- See `CONTRIBUTING.md` for full workflow
### 3. Package Manager
- **ALWAYS use Bun**, never npm or yarn
- Commands: `bun install`, `bun run <script>`
- Project requires Bun >= 1.3.0
### 4. Multi-Browser Support
- Test changes in Chrome, Firefox, and Safari when possible
- Browser-specific builds are in `build/chrome/`, `build/firefox/`, etc.
- Manifest files: `manifest/chrome.json`, `manifest/firefox.json`
### 5. State Management
- Use `useLocalStorageState` hook for persistent settings
- Use React Context for shared state (see `src/contexts/`)
- Store user data in localStorage or IndexedDB (via `customBackgroundDB.js`)
### 6. Styling
- SCSS files in `src/scss/`
- Use existing variables from `_variables.scss`
- Use mixins from `_mixins.scss` (especially for responsive design)
- Mobile styles in `_mobile.scss`
## Development Commands
```bash
bun run dev # Start dev server
bun run dev:host # Dev server accessible on network
bun run build # Build for production (all browsers)
bun run lint # Run linters
bun run lint:fix # Auto-fix linting issues
bun run pretty # Format code
bun run translations # Update translation files
```
## Common Patterns
### Component Structure
```jsx
import { useState } from 'react';
import './ComponentName.scss';
export default function ComponentName({ prop1, prop2 }) {
const [state, setState] = useState();
return <div className="component-name">{/* JSX content */}</div>;
}
```
### Using localStorage
```jsx
import { useLocalStorageState } from 'utils/useLocalStorageState';
const [value, setValue] = useLocalStorageState('settingKey', defaultValue);
```
### Using Translations
```jsx
import { useContext } from 'react';
import { TranslationContext } from 'contexts';
const { t } = useContext(TranslationContext);
const text = t('translation.key');
```
## Things to Avoid
- ❌ Don't use npm or yarn (use Bun)
- ❌ Don't edit locale files other than `en_GB.json`
- ❌ Don't commit directly to `beta` or `main` branches
- ❌ Don't introduce new dependencies without discussion
- ❌ Don't skip linting/formatting before commits
- ❌ Don't use class components (use functional components only)
## When Making Changes
1. Check which files need updates (component, styles, utils)
2. Follow existing patterns in similar components
3. Update translations in `en_GB.json` if adding new text
4. Run `bun run lint:fix` and `bun run pretty`
5. Test in at least Chrome and Firefox
6. Write conventional commit message
## Additional Resources
- See `CONTRIBUTING.md` for full contribution guidelines
- See `docs/RELEASE_PROCESS.md` for release procedures
- Check existing components in `src/components/` for patterns

View File

@@ -1,12 +1 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
target-branch: "dev"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"

View File

@@ -8,11 +8,12 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.1'
bun-version: latest
- name: Install dependencies
run: bun install
- name: Lint
run: bun run lint
continue-on-error: true
- name: Build
run: bun run build
automerge:

View File

@@ -42,21 +42,6 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Generate changelog
id: changelog
run: |
COMMITS=$(git log --pretty=format:"- %s (%h)" origin/main..${{ github.event.inputs.branch_name }})
{
echo "changelog<<EOF"
echo "### 🚨 Hotfix"
echo "${{ github.event.inputs.description }}"
echo ""
echo "### Changes"
echo "$COMMITS"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Calculate hotfix version (auto-patch bump)
id: version
run: |
@@ -116,6 +101,23 @@ jobs:
git push origin main
git push origin "v${{ steps.version.outputs.new_version }}"
- name: Generate changelog
id: changelog
run: |
# Get commits from hotfix branch
git checkout ${{ github.event.inputs.branch_name }}
COMMITS=$(git log --pretty=format:"- %s (%h)" origin/main..${{ github.event.inputs.branch_name }})
{
echo "changelog<<EOF"
echo "### 🚨 Hotfix"
echo "${{ github.event.inputs.description }}"
echo ""
echo "### Changes"
echo "$COMMITS"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Create GitHub Release
run: |
git checkout main

View File

@@ -162,25 +162,6 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Back-merge main to beta
run: |
git fetch origin beta
git checkout beta
git merge --no-ff main -m "chore: back-merge production release v${{ steps.version.outputs.version }} from main"
git push origin beta
- name: Back-merge main to dev
run: |
git fetch origin dev
git checkout dev
git merge --no-ff main -m "chore: back-merge production release v${{ steps.version.outputs.version }} from main"
git push origin dev
- name: Output success summary
run: |
echo "## 🚀 Production Release Published!" >> $GITHUB_STEP_SUMMARY
@@ -203,4 +184,4 @@ jobs:
echo "- [ ] Update [muetab.com/blog/changelog](https://muetab.com/blog/changelog)" >> $GITHUB_STEP_SUMMARY
echo "- [ ] Announce release on Discord/social media" >> $GITHUB_STEP_SUMMARY
echo "- [ ] Monitor issue tracker for bug reports" >> $GITHUB_STEP_SUMMARY
echo "- [x] Back-merged \`main\` to \`beta\` and \`dev\` (automated)" >> $GITHUB_STEP_SUMMARY
echo "- [ ] Merge \`main\` back to \`beta\` and \`dev\` to sync version" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,57 +0,0 @@
name: Submit to Browser Stores (Beta)
on:
release:
types: [prereleased]
workflow_dispatch:
inputs:
tag:
description: "Pre-release tag to re-submit (e.g. v7.6.1-beta.3)"
required: true
permissions:
contents: read
jobs:
submit-beta:
runs-on: ubuntu-latest
environment: beta
steps:
- name: Resolve tag and version
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.event.release.tag_name }}"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
- name: Download release ZIPs
run: |
mkdir -p dist
gh release download "${{ steps.tag.outputs.tag }}" \
--pattern "chrome-*.zip" \
--dir ./dist
env:
GH_TOKEN: ${{ github.token }}
- name: Verify ZIP
run: |
ls -la dist/
test -n "$(ls dist/chrome-*.zip 2>/dev/null)" || \
{ echo "chrome ZIP not found in dist/"; exit 1; }
- name: Submit to Chrome Web Store (Trusted Testers)
uses: PlasmoHQ/bpp@v3
with:
keys: ${{ secrets.SUBMIT_KEYS_BETA }}
chrome-zip: dist/chrome-${{ steps.tag.outputs.version }}.zip
- name: Summary
if: always()
run: |
echo "## Beta Store Submission: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Chrome Web Store (trustedTesters)" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,57 +1,27 @@
name: Submit to Browser Stores
name: Submit
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag:
description: "Release tag to re-submit (e.g. v7.6.1)"
description: "Release tag to submit, i.e 6.0.5"
required: true
permissions:
contents: read
jobs:
submit:
runs-on: ubuntu-latest
environment: production
steps:
- name: Resolve tag and version
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.event.release.tag_name }}"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
- name: Download release ZIPs
run: |
mkdir -p dist
gh release download "${{ steps.tag.outputs.tag }}" \
--pattern "chrome-*.zip" \
--dir ./dist
- name: Setup Chrome
uses: browser-actions/setup-chrome@latest
with:
chrome-version: latest
- name: Download Github Release Assets
uses: PlasmoHQ/download-release-asset@v1.0.0
with:
tag: ${{ github.event.inputs.tag }}
- name: Browser Plugin Publish
uses: PlasmoHQ/bpp@v2
env:
GH_TOKEN: ${{ github.token }}
- name: Verify ZIP
run: |
ls -la dist/
test -f "dist/chrome-${{ steps.tag.outputs.version }}.zip" || \
{ echo "chrome ZIP not found"; exit 1; }
- name: Submit to Chrome Web Store
uses: PlasmoHQ/bpp@v3
PUPPETEER_EXECUTABLE_PATH: /opt/hostedtoolcache/chromium/latest/x64/chrome
with:
keys: ${{ secrets.SUBMIT_KEYS }}
chrome-zip: dist/chrome-${{ steps.tag.outputs.version }}.zip
- name: Summary
if: always()
run: |
echo "## Store Submission: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- Chrome Web Store (listed)" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,18 +1,31 @@
name: Version Bump
on:
pull_request:
types:
- closed
labels:
- 'release'
workflow_dispatch:
inputs:
bump_type:
description: 'Version bump type'
required: true
type: choice
options:
- patch # 7.5.0 -> 7.5.1 (bug fixes)
- minor # 7.5.0 -> 7.6.0 (new features)
- major # 7.5.0 -> 8.0.0 (breaking changes)
pre_release:
description: 'Pre-release label (leave empty for stable release)'
required: false
type: choice
options:
- ''
- beta
- rc
- alpha
permissions:
contents: write
jobs:
bump-version:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -31,99 +44,99 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Extract bump info from PR
id: bump_info
run: |
BUMP_TYPE=$(echo "${{ github.event.pull_request.body }}" | grep -oP "bump: \K(major|minor|patch)" || echo "patch")
IS_RELEASE=$(echo "${{ github.event.pull_request.body }}" | grep -q "is_release: true" && echo "true" || echo "false")
echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT
- name: Calculate new version
id: version
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Current version: $CURRENT_VERSION"
# Remove any pre-release suffix for base version
BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/-.*$//')
IFS='.' read -r -a VERSION_PARTS <<< "$BASE_VERSION"
MAJOR="${VERSION_PARTS[0]}"
MINOR="${VERSION_PARTS[1]}"
PATCH="${VERSION_PARTS[2]}"
case "${{ steps.bump_info.outputs.bump_type }}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
# Add pre-release suffix if NOT a production release AND NOT on main branch
if [ "${{ steps.bump_info.outputs.is_release }}" != "true" ] && [ "${{ github.ref_name }}" != "main" ]; then
BETA_COUNT=$(git tag -l "v${NEW_VERSION}-beta.*" | wc -l)
BETA_NUM=$((BETA_COUNT + 1))
NEW_VERSION="${NEW_VERSION}-beta.${BETA_NUM}"
# If requesting a pre-release and current is already a pre-release, keep base version
# Otherwise, bump the version based on type
if [ -n "${{ github.event.inputs.pre_release }}" ] && [ "$IS_PRERELEASE" = "true" ]; then
# Keep existing base version for iterative betas (7.6.0-beta.1 -> 7.6.0-beta.2)
NEW_VERSION="$BASE_VERSION"
else
# Bump version based on type
case "${{ github.event.inputs.bump_type }}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
fi
# Browser extension manifests require clean semver — strip pre-release suffix
STABLE_VERSION=$(echo "$NEW_VERSION" | sed 's/-.*$//')
# Add pre-release label if specified
if [ -n "${{ github.event.inputs.pre_release }}" ]; then
# Get beta number by counting existing beta tags for this version
BETA_COUNT=$(git tag -l "v${NEW_VERSION}-${{ github.event.inputs.pre_release }}.*" | wc -l)
BETA_NUM=$((BETA_COUNT + 1))
NEW_VERSION="${NEW_VERSION}-${{ github.event.inputs.pre_release }}.${BETA_NUM}"
fi
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "stable_version=$STABLE_VERSION" >> $GITHUB_OUTPUT
echo "New version will be: $NEW_VERSION"
- name: Update package.json
run: bun x json -I -f package.json -e "this.version='${{ steps.version.outputs.new_version }}'"
run: |
bun x json -I -f package.json -e "this.version='${{ steps.version.outputs.new_version }}'"
- name: Update Chrome manifest
run: bun x json -I -f manifest/chrome.json -e "this.version='${{ steps.version.outputs.stable_version }}'"
run: |
VERSION_WITHOUT_PRERELEASE=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/-.*$//')
bun x json -I -f manifest/chrome.json -e "this.version='${VERSION_WITHOUT_PRERELEASE}'"
- name: Update Firefox manifest
run: bun x json -I -f manifest/firefox.json -e "this.version='${{ steps.version.outputs.stable_version }}'"
run: |
VERSION_WITHOUT_PRERELEASE=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/-.*$//')
bun x json -I -f manifest/firefox.json -e "this.version='${VERSION_WITHOUT_PRERELEASE}'"
- name: Update Safari manifest
run: bun x json -I -f safari/Mue\ Extension/Resources/manifest.json -e "this.version='${{ steps.version.outputs.stable_version }}'"
run: |
VERSION_WITHOUT_PRERELEASE=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/-.*$//')
bun x json -I -f safari/Mue\ Extension/Resources/manifest.json -e "this.version='${VERSION_WITHOUT_PRERELEASE}'"
- name: Update Safari Xcode project
run: sed -i "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ steps.version.outputs.stable_version }}/g" safari/Mue.xcodeproj/project.pbxproj
run: |
VERSION_WITHOUT_PRERELEASE=$(echo "${{ steps.version.outputs.new_version }}" | sed 's/-.*$//')
sed -i "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${VERSION_WITHOUT_PRERELEASE}/g" safari/Mue.xcodeproj/project.pbxproj
- name: Update constants.js
run: sed -i "s/export const VERSION = '[^']*'/export const VERSION = '${{ steps.version.outputs.new_version }}'/" src/config/constants.js
run: |
sed -i "s/export const VERSION = '[^']*'/export const VERSION = '${{ steps.version.outputs.new_version }}'/" src/config/constants.js
- name: Commit and tag
- name: Commit version bump
run: |
git add package.json manifest/chrome.json manifest/firefox.json safari/Mue\ Extension/Resources/manifest.json safari/Mue.xcodeproj/project.pbxproj src/config/constants.js
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Release v${{ steps.version.outputs.new_version }}"
- name: Push changes
run: |
git push origin ${{ github.ref_name }}
git push origin "v${{ steps.version.outputs.new_version }}"
- name: Auto back-merge (main only)
if: github.ref_name == 'main'
run: |
git fetch origin
git checkout beta
git merge --no-ff origin/main -m "chore: back-merge main into beta"
git push origin beta
git checkout dev
git merge --no-ff origin/beta -m "chore: back-merge beta into dev"
git push origin dev
- name: Summary
run: |
echo "✅ Version bumped to ${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "📦 Tag: v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "🔀 Branch: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "📦 Tag created: v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Files updated:" >> $GITHUB_STEP_SUMMARY
echo "- package.json" >> $GITHUB_STEP_SUMMARY
@@ -132,9 +145,3 @@ jobs:
echo "- safari/Mue Extension/Resources/manifest.json" >> $GITHUB_STEP_SUMMARY
echo "- safari/Mue.xcodeproj/project.pbxproj" >> $GITHUB_STEP_SUMMARY
echo "- src/config/constants.js" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.ref_name }}" = "main" ]; then
echo "### 🔄 Auto back-merge:" >> $GITHUB_STEP_SUMMARY
echo "- main → beta ✅" >> $GITHUB_STEP_SUMMARY
echo "- beta → dev ✅" >> $GITHUB_STEP_SUMMARY
fi

2
.gitignore vendored
View File

@@ -5,7 +5,6 @@ node_modules/
build/
.idea/
dist/
.claude/worktrees/
# Safari Extension Build Files
safari/Mue Extension/Resources/*.html
@@ -30,7 +29,6 @@ safari/DerivedData/
safari/build/
# Files
unused-translations.txt
package-lock.json
.stylelintcache
yarn-error.log

View File

@@ -1,2 +1 @@
#!/bin/sh
bunx --bun commitlint --edit "$1"
bunx --bun commitlint --edit $1

View File

@@ -1,7 +0,0 @@
{
"setup": [],
"teardown": [],
"run": [
"bun run dev"
]
}

121
CLAUDE.md
View File

@@ -1,121 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is Mue?
Mue is a browser extension (Chrome, Firefox, Safari) that replaces the new tab page with a customizable dashboard featuring widgets like clock, weather, quotes, greetings, quick links, and backgrounds. Built with React 19, Vite 7, and Manifest V3.
## Commands
```bash
bun install # Install dependencies (always use bun, never npm/yarn)
bun run dev # Dev server with HMR (opens browser automatically)
bun run dev:host # Dev server exposed on network
bun run build # Production build for all browsers (Chrome, Firefox, Safari)
bun run lint # Run ESLint + Stylelint
bun run lint:fix # Auto-fix lint issues
bun run pretty # Format with Prettier
bun run translations # Sync all locale files from en_GB.json base
bun run translations:percentages # Update translation completion stats
bun run translations:unused # Find unused translation keys
```
Build outputs: `build/chrome/`, `build/firefox/`, `safari/Mue Extension/Resources/`. Vite's `prepareBuilds` plugin (in `vite.config.mjs`) copies dist + manifests + icons into each browser folder and creates versioned zips.
E2E tests use Cypress:
```bash
bun run test:e2e # Open Cypress UI
bun run test:e2e:headless # Run headlessly (CI)
```
Tests live in `cypress/e2e/`.
## Architecture
### Bootstrap Flow
1. `src/index.jsx` - Initializes i18n from localStorage language, sets up Sentry, runs data migrations, exposes global `window.t()`, renders `<RouterProvider router={router} />`
2. `src/router/` - Hash-based router (`createHashRouter`) required for extension compatibility (no physical file routing). `ModalRouter.jsx` and `RouterBridge.jsx` handle modal deep-linking.
3. `src/App.jsx` - `useAppSetup()` checks first-run state, calls `loadSettings()` (which applies theme, fonts, custom CSS to the DOM), listens for EventBus `'refresh'` events. Renders: `<TranslationProvider>``<Background>` + `<CustomWidgets>` + `<Widgets>` + `<Modals>`
### State Management (no Redux/Zustand)
All persistent state lives in **localStorage**. Components read from localStorage on mount and re-read when they receive EventBus refresh events. The only React Context is `TranslationContext` for i18n.
- `localStorage.getItem('key') === 'true'` is the standard boolean check pattern
- Settings changes write to localStorage, then emit `EventBus.emit('refresh', 'category')` to notify widgets
- `src/utils/settings/load.js` applies localStorage settings to the DOM (theme classes, injected style elements for custom fonts/CSS)
### EventBus (`src/utils/eventbus.js`)
Static class wrapping DOM CustomEvents. Primary communication mechanism between settings UI and widgets.
Key events:
- `'refresh'` with payload: `'quote'`, `'greeting'`, `'background'`, `'widgets'`, `'clock'`, `'other'` - triggers widget reload
- `'languageChange'` with `{language: 'en_GB'}` - switches locale
- `'modal'` with `'openMainModal'` - opens settings modal
Pattern: register in `useEffect`, clean up with `EventBus.off()` on unmount.
### Feature Organization (`src/features/`)
Each feature (background, time, quote, greeting, weather, search, quicklinks, message, navbar, stats, marketplace, welcome, helpers, modals) follows this structure:
```
feature/
├── index.jsx # Main component
├── options/index.jsx # Settings panel UI
├── hooks/ # Feature-specific hooks (useQuoteState, useQuoteLoader, etc.)
├── components/ # Subcomponents
├── api/ # Data fetching/processing
└── scss/ # Styles
```
The `misc` feature is special - it contains the modal system (`modals/Modals.jsx`), widget layout (`CustomWidgets.jsx`, `views/Widgets.jsx`), and the settings view (`views/Settings.jsx`).
### Modal & Settings System
`Modals.jsx` orchestrates four modals (main, welcome, update, apps). The main modal has three tabs: Settings, Discover (marketplace), Library. Deep-linking via URL hash: `#settings/appearance/fonts`, `#discover/quote_packs`.
### Global Variables (`src/config/variables.js`)
Singleton object holding `language` (i18n instance), `languagecode`, `stats`, and `constants`. Mutated during initialization. `variables.getMessage()` is dynamically set by TranslationContext.
### Hooks
- `useFrequencyInterval()` - configurable update intervals with visibility-aware pause/resume
- `useCachedFetch()` - fetch with localStorage caching and TTL
- `useT()` / `useTranslation()` - access translation function from context
### Path Aliases (configured in `vite.config.mjs`)
Use these instead of relative imports: `@/`, `components/`, `contexts/`, `hooks/`, `assets/`, `config/`, `features/`, `lib/`, `scss/`, `translations/`, `utils/`, `i18n/`
## Code Rules
### Do not add comments
Keep code self-explanatory. Use descriptive names instead of comments.
### Do not use emojis in code strings
No emojis in console logs, placeholders, or code strings. Exception: user-facing toast notifications may include emojis.
### All user-visible strings must use i18n
Never hardcode user-facing text. Use `const t = useT(); t('widgets.greeting.morning')` or `getMessage('key')`. Console logs don't need i18n.
### Translation workflow
Edit `src/i18n/locales/en_GB.json` first (it's the base file), then run `bun run translations` to sync all other locales.
### Naming conventions
Concise names without redundant prefixes. `const [open, setOpen] = useState(false)` not `const [isOpen, setIsOpen]`. Exception: `is` prefix is fine for boolean-returning functions (`isValid()`), `has` prefix for boolean properties.
### Branch strategy
Three branches: `dev` (all PRs target here) → `beta``main`. Hotfix branches (`hotfix/*`) branch from `main`.
### Conventional commits
Enforced by commitlint: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `style:`, `perf:`. Scopes encouraged: `feat(weather): add hourly forecast`.

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.1.42
FROM oven/bun:latest
WORKDIR /app
@@ -8,4 +8,4 @@ RUN bun install
EXPOSE 5173
CMD ["bun", "run", "dev:host"]
CMD ["bun", "run", "dev:host"]

View File

@@ -1,45 +0,0 @@
# Mue Engineering Standards (GEMINI.md)
This file contains project-specific instructions and guidelines for Mue.
## Stack & Commands
- **Package Manager:** Always use `bun`. Never use `npm` or `yarn`.
- **Framework:** React 19, Vite 7, Manifest V3.
- **Core Commands:**
- `bun install`: Install dependencies.
- `bun run dev`: Start dev server.
- `bun run build`: Production build for Chrome, Firefox, Safari.
- `bun run lint`: Run ESLint and Stylelint.
- `bun run lint:fix`: Auto-fix lint issues.
- `bun run pretty`: Format with Prettier.
- `bun run translations`: Sync locale files from `en_GB.json`.
- `bun run translations:percentages`: Update translation stats.
- `bun run translations:unused`: Find unused translation keys.
- `bun run test:e2e:headless`: Run Cypress tests headlessly.
## Architecture & Patterns
- **State Management:** Use **localStorage** for persistent state.
- **Communication:** Use `src/utils/eventbus.js` for inter-widget/UI communication.
- Register in `useEffect`, cleanup with `EventBus.off()`.
- **I18n:** All user-visible strings MUST use `src/i18n/locales/en_GB.json` as the base.
- Use `useT()` hook or `getMessage()` helper.
- **Routing:** Hash-based router (`createHashRouter`) in `src/router/`.
- **Path Aliases:** Use `@/`, `components/`, `features/`, etc. (see `vite.config.mjs`).
## Coding Rules
- **No Comments:** Keep code self-explanatory through descriptive naming.
- **No Emojis:** Do not use emojis in code strings or logs (except user-facing toasts).
- **Naming:** Concise names. Use `const [open, setOpen] = useState(false)`, not `isOpen`.
- **Commits:** Conventional commits (`feat:`, `fix:`, etc.) are enforced.
- **Translations:** Edit `en_GB.json` first, then run `bun run translations`.
## Directory Structure
- `src/features/`: Feature-based organization (background, time, quote, etc.).
- `src/components/`: Shared UI elements and layouts.
- `src/utils/`: Shared utilities and helpers.
- `manifest/`: Extension manifest templates.
- `safari/`: Safari-specific project files.

361
bun.lock
View File

@@ -17,14 +17,12 @@
"blurhash": "^2.0.5",
"fast-blurhash": "^1.1.4",
"image-conversion": "^2.1.1",
"maplibre-gl": "^5.24.0",
"react": "^19.2.3",
"react-best-gradient-color-picker": "^3.0.14",
"react-clock": "6.0.0",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-modal": "3.16.3",
"react-router": "^7.13.0",
"react-toastify": "11.0.5",
"use-debounce": "^10.1.0",
},
@@ -35,7 +33,6 @@
"@eslint/js": "^9.39.2",
"@vitejs/plugin-react-swc": "^4.2.2",
"adm-zip": "0.5.16",
"cypress": "^15.10.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
@@ -135,10 +132,6 @@
"@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="],
"@cypress/request": ["@cypress/request@3.0.10", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "~6.14.1", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ=="],
"@cypress/xvfb": ["@cypress/xvfb@1.2.4", "", { "dependencies": { "debug": "^3.1.0", "lodash.once": "^4.1.1" } }, "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
@@ -257,26 +250,6 @@
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
"@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.1.0", "", {}, "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg=="],
"@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.1", "", {}, "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="],
"@mapbox/vector-tile": ["@mapbox/vector-tile@2.0.4", "", { "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^4.0.1" } }, "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg=="],
"@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="],
"@maplibre/geojson-vt": ["@maplibre/geojson-vt@6.1.0", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ=="],
"@maplibre/maplibre-gl-style-spec": ["@maplibre/maplibre-gl-style-spec@24.8.3", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs", "gl-style-format": "dist/gl-style-format.mjs" } }, "sha512-e5u6GZXA0t6HnBh63YLefSLy62W1QloF4qVLAo1ae88oH/8+PNK98i7/aVf/XWc4wD9D2lTgW2LaApYLPtM1sA=="],
"@maplibre/mlt": ["@maplibre/mlt@1.1.9", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0" } }, "sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g=="],
"@maplibre/vt-pbf": ["@maplibre/vt-pbf@4.3.0", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0", "@mapbox/vector-tile": "^2.0.4", "@maplibre/geojson-vt": "^5.0.4", "@types/geojson": "^7946.0.16", "@types/supercluster": "^7.1.3", "pbf": "^4.0.1", "supercluster": "^8.0.1" } }, "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -409,22 +382,10 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
"@types/sinonjs__fake-timers": ["@types/sinonjs__fake-timers@8.1.1", "", {}, "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g=="],
"@types/sizzle": ["@types/sizzle@2.3.10", "", {}, "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww=="],
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
"@types/tmp": ["@types/tmp@0.2.6", "", {}, "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.2", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.47", "@swc/core": "^1.13.5" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA=="],
"@wojtekmaj/date-utils": ["@wojtekmaj/date-utils@2.0.2", "", {}, "sha512-Do66mSlSNifFFuo3l9gNKfRMSFi26CRuQMsDJuuKO/ekrDWuTTtE4ZQxoFCUOG+NgxnpSeBq/k5TY8ZseEzLpA=="],
@@ -437,20 +398,12 @@
"adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"arch": ["arch@2.2.0", "", {}, "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
@@ -471,26 +424,14 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -499,16 +440,8 @@
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
"blob-util": ["blob-util@2.0.2", "", {}, "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ=="],
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"blurhash": ["blurhash@2.0.5", "", {}, "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -517,14 +450,8 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"cacheable": ["cacheable@2.3.2", "", { "dependencies": { "@cacheable/memory": "^2.0.7", "@cacheable/utils": "^2.3.3", "hookified": "^1.15.0", "keyv": "^5.5.5", "qified": "^0.6.0" } }, "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA=="],
"cachedir": ["cachedir@2.4.0", "", {}, "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -535,22 +462,10 @@
"caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-table3": ["cli-table3@0.6.1", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "colors": "1.4.0" } }, "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA=="],
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -561,16 +476,6 @@
"colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -583,10 +488,6 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
"cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.2.0", "", { "dependencies": { "jiti": "^2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ=="],
@@ -601,22 +502,16 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cypress": ["cypress@15.10.0", "", { "dependencies": { "@cypress/request": "^3.0.10", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "@types/tmp": "^0.2.3", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", "figures": "^3.2.0", "fs-extra": "^9.1.0", "hasha": "5.2.2", "is-installed-globally": "~0.4.0", "listr2": "^3.8.3", "lodash": "^4.17.23", "log-symbols": "^4.0.0", "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "supports-color": "^8.1.1", "systeminformation": "^5.27.14", "tmp": "~0.2.4", "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" } }, "sha512-OtUh7OMrfEjKoXydlAD1CfG2BvKxIqgWGY4/RMjrqQ3BKGBo5JFKoYNH+Tpcj4xKxWH4XK0Xri+9y8WkxhYbqQ=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
"dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="],
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -625,8 +520,6 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
@@ -635,18 +528,10 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.279", "", {}, "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
@@ -697,20 +582,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter2": ["eventemitter2@6.4.7", "", {}, "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg=="],
"execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="],
"executable": ["executable@4.1.1", "", { "dependencies": { "pify": "^2.2.0" } }, "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg=="],
"exenv": ["exenv@1.2.2", "", {}, "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
"fast-blurhash": ["fast-blurhash@1.1.4", "", {}, "sha512-xeH121M027hgWHHhHWYYjUmMKl8vCH3PPkXk439ixsP8Bvb/r3UFqg12oMSToD/aSAw8EE6XiTdfZ6M5jaLfzg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -727,12 +600,8 @@
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -745,12 +614,6 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -771,24 +634,16 @@
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"get-user-locale": ["get-user-locale@3.0.0", "", { "dependencies": { "memoize": "^10.0.0" } }, "sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g=="],
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
"git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="],
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="],
"global-dirs": ["global-dirs@3.0.1", "", { "dependencies": { "ini": "2.0.0" } }, "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA=="],
"global-modules": ["global-modules@2.0.0", "", { "dependencies": { "global-prefix": "^3.0.0" } }, "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A=="],
"global-prefix": ["global-prefix@3.0.0", "", { "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } }, "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg=="],
@@ -803,11 +658,9 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
@@ -817,8 +670,6 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasha": ["hasha@5.2.2", "", { "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" } }, "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ=="],
"hashery": ["hashery@1.4.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@@ -833,14 +684,8 @@
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
"http-signature": ["http-signature@1.4.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", "sshpk": "^1.18.0" } }, "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg=="],
"human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="],
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"image-conversion": ["image-conversion@2.1.1", "", {}, "sha512-hnMOmP7q2jxA+52FZ+wHNhg3fdFRlgfngsQH2JQHEQkafY7tj/8F15e6Rv/RxDegc872jvyaRHwMbkTZK1Cjbg=="],
@@ -853,9 +698,7 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
@@ -887,8 +730,6 @@
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-installed-globally": ["is-installed-globally@0.4.0", "", { "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" } }, "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
@@ -899,7 +740,7 @@
"is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
@@ -909,8 +750,6 @@
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
@@ -919,10 +758,6 @@
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
@@ -933,8 +768,6 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
@@ -943,36 +776,22 @@
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
"jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="],
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
@@ -987,12 +806,8 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"listr2": ["listr2@3.14.0", "", { "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", "log-update": "^4.0.0", "p-map": "^4.0.0", "rfdc": "^1.3.0", "rxjs": "^7.5.1", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
@@ -1003,8 +818,6 @@
"lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="],
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
"lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="],
@@ -1017,16 +830,10 @@
"lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="],
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"maplibre-gl": ["maplibre-gl@5.24.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.1.0", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/geojson-vt": "^6.1.0", "@maplibre/maplibre-gl-style-spec": "^24.8.1", "@maplibre/mlt": "^1.1.8", "@maplibre/vt-pbf": "^4.3.0", "@types/geojson": "^7946.0.16", "earcut": "^3.0.2", "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" } }, "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mathml-tag-names": ["mathml-tag-names@4.0.0", "", {}, "sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ=="],
@@ -1037,18 +844,10 @@
"meow": ["meow@14.0.0", "", {}, "sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
@@ -1057,8 +856,6 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"murmurhash-js": ["murmurhash-js@1.0.0", "", {}, "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -1069,8 +866,6 @@
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -1085,22 +880,14 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"ospath": ["ospath@1.2.2", "", {}, "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
@@ -1111,18 +898,10 @@
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
@@ -1139,36 +918,20 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.1", "", {}, "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ=="],
"proxy-from-env": ["proxy-from-env@1.0.0", "", {}, "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qified": ["qified@0.6.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA=="],
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
"rd": ["rd@2.0.1", "", { "dependencies": { "@types/node": "^10.3.6" } }, "sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
@@ -1187,8 +950,6 @@
"react-modal": ["react-modal@3.16.3", "", { "dependencies": { "exenv": "^1.2.0", "prop-types": "^15.7.2", "react-lifecycles-compat": "^3.0.0", "warning": "^4.0.3" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" } }, "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw=="],
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
"react-toastify": ["react-toastify@11.0.5", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -1197,8 +958,6 @@
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"request-progress": ["request-progress@3.0.0", "", { "dependencies": { "throttleit": "^1.0.0" } }, "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
@@ -1207,40 +966,24 @@
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sass": ["sass@1.97.3", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -1259,7 +1002,7 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
@@ -1269,8 +1012,6 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
@@ -1287,9 +1028,7 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
@@ -1305,9 +1044,7 @@
"stylelint-scss": ["stylelint-scss@7.0.0", "", { "dependencies": { "css-tree": "^3.0.1", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mdn-data": "^2.25.0", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "stylelint": "^16.8.2 || ^17.0.0" } }, "sha512-H88kCC+6Vtzj76NsC8rv6x/LW8slBzIbyeSjsKVlS+4qaEJoDrcJR4L+8JdrR2ORdTscrBzYWiiT2jq6leYR1Q=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-hyperlinks": ["supports-hyperlinks@4.4.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="],
@@ -1315,16 +1052,12 @@
"svg-tags": ["svg-tags@1.0.0", "", {}, "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA=="],
"systeminformation": ["systeminformation@5.30.7", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-33B/cftpaWdpvH+Ho9U1b08ss8GQuLxrWHelbJT1yw4M48Taj8W3ezcPuaLoIHZz5V6tVHuQPr5BprEfnBLBMw=="],
"table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="],
"text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="],
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
"throttleit": ["throttleit@1.0.1", "", {}, "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ=="],
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"tinycolor2": ["tinycolor2@1.4.2", "", {}, "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="],
@@ -1333,30 +1066,12 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="],
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
@@ -1373,10 +1088,6 @@
"unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -1387,10 +1098,6 @@
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vite-plugin-progress": ["vite-plugin-progress@0.0.7", "", { "dependencies": { "picocolors": "^1.0.0", "progress": "^2.0.3", "rd": "^2.0.1" }, "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-zyvKdcc/X+6hnw3J1HVV1TKrlFKC4Rh8GnDnWG/2qhRXjqytTcM++xZ+SAPnoDsSyWl8O93ymK0wZRgHAoglEQ=="],
@@ -1411,8 +1118,6 @@
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -1423,8 +1128,6 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@@ -1447,67 +1150,43 @@
"@commitlint/types/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@cypress/xvfb/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@keyv/bigmap/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"@maplibre/vt-pbf/@maplibre/geojson-vt": ["@maplibre/geojson-vt@5.0.4", "", {}, "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ=="],
"@types/conventional-commits-parser/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"@types/yauzl/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"cacheable/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"conventional-commits-parser/meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="],
"css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"git-raw-commits/meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="],
"global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
"global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
"globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"globby/is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"log-update/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"stylelint/file-entry-cache": ["file-entry-cache@11.1.2", "", { "dependencies": { "flat-cache": "^6.1.20" } }, "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log=="],
"stylelint/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"supports-hyperlinks/has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
"supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"supports-hyperlinks/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
@@ -1515,9 +1194,11 @@
"table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1529,15 +1210,9 @@
"@commitlint/top-level/find-up/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
"cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"log-update/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"stylelint/file-entry-cache/flat-cache": ["flat-cache@6.1.20", "", { "dependencies": { "cacheable": "^2.3.2", "flatted": "^3.3.3", "hookified": "^1.15.0" } }, "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ=="],
@@ -1545,13 +1220,19 @@
"table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
"log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],

View File

@@ -1,12 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:5173",
viewportWidth: 1920,
viewportHeight: 1080,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@@ -1,131 +0,0 @@
describe('Basic Features', () => {
beforeEach(() => {
// Bypass onboarding and enable features
cy.visit('/', {
onBeforeLoad: (win) => {
win.localStorage.clear();
win.localStorage.setItem('firstRun', 'true');
win.localStorage.setItem('stats', 'true');
win.localStorage.setItem('navbarHover', 'false');
win.localStorage.setItem('notesEnabled', 'true');
win.localStorage.setItem('searchBar', 'true');
win.localStorage.setItem('voiceSearch', 'false');
win.localStorage.setItem('language', 'en_GB');
win.localStorage.setItem('theme', 'dark');
win.localStorage.setItem('widgetStyle', 'new');
win.localStorage.setItem('showWelcome', 'false');
win.localStorage.setItem('view', 'true'); // Enable Maximise button
win.localStorage.setItem('background', 'true'); // Enable Background for Maximise button
win.localStorage.setItem('photoInformation', 'true'); // Enable Photo Information
win.localStorage.setItem('backgroundType', 'api'); // Required for backgroundLoader
win.localStorage.setItem('quoteType', 'custom');
win.localStorage.setItem('customQuote', 'Test Quote');
win.localStorage.setItem('customQuoteAuthor', 'Test Author');
// Seed other widgets
win.localStorage.setItem('greeting', 'true');
win.localStorage.setItem('time', 'true');
win.localStorage.setItem('date', 'true');
// Seed Quick Links with data so it renders with height
win.localStorage.setItem('quicklinks', JSON.stringify([
{ name: "Test Link", url: "https://example.com", key: "1" }
]));
win.localStorage.setItem('quicklinksenabled', 'true');
win.localStorage.setItem('message', 'true');
win.localStorage.setItem('messages', '["Test Message"]');
win.localStorage.setItem('order', JSON.stringify(["greeting", "time", "quicklinks", "quote", "date", "message"]));
}
});
// Mock Background API
cy.intercept('GET', '**/images/random*', { fixture: 'background.json' }).as('getBackground');
});
it('should perform search', () => {
const searchText = 'Cypress Test Search';
cy.get('#searchtext').should('exist');
cy.get('#searchtext').type(searchText);
cy.get('#searchtext').should('have.value', searchText);
});
it('should use Notes feature', () => {
cy.get('.notes .navbarButton').click();
const noteText = 'This is a test note';
cy.get('.notesContainer textarea').should('be.visible');
cy.get('.notesContainer textarea').clear().type(noteText);
cy.reload();
cy.get('.notes .navbarButton').click();
cy.get('.notesContainer textarea').should('have.value', noteText);
});
it('should maximise and unmaximise widgets', () => {
cy.get('#widgets').should('not.have.css', 'display', 'none');
cy.get('button[aria-label="Maximise"]').click();
cy.get('#widgets').should('have.css', 'display', 'none');
cy.get('button[aria-label="Maximise"]').click();
cy.get('#widgets').should('not.have.css', 'display', 'none');
});
it('should display photo information and background actions', () => {
cy.wait('@getBackground');
cy.get('.photoInformation', { timeout: 10000 }).should('exist').and('be.visible');
cy.get('.photoInformation').trigger('mouseover');
// Check primary content
cy.get('.photoInformation .primary-content').should('exist');
// Check Action Buttons
cy.get('.photoInformation .buttons').should('exist');
cy.get('.photoInformation .buttons svg').should('have.length.at.least', 3);
// Test Favourite Interaction
cy.get('.photoInformation .buttons > .tooltip').eq(1).click({ force: true });
});
it('should display quote link', () => {
// Reload with offlineMode to ensure we get a guaranteed offline quote
cy.visit('/', {
onBeforeLoad: (win) => {
win.localStorage.clear();
win.localStorage.setItem('firstRun', 'true');
win.localStorage.setItem('showWelcome', 'false');
win.localStorage.setItem('widgetStyle', 'new');
win.localStorage.setItem('offlineMode', 'true');
win.localStorage.setItem('order', JSON.stringify(["quote"]));
}
});
cy.get('.quotediv').should('exist');
cy.get('.quotediv .quote').should('not.be.empty');
cy.get('.quotediv a').should('exist').and('have.attr', 'href').and('include', 'wikipedia');
});
it('should have a refresh button', () => {
cy.get('button[aria-label="Refresh"]').should('exist').and('be.visible');
});
it('should display other dashboard widgets', () => {
// Verify Greeting
cy.get('.greeting').should('exist').and('be.visible');
// Verify Time (Clock)
// Checks for either generic clock class or clock container
cy.get('.clock-container').should('exist').and('be.visible');
// Verify Date
cy.get('.date').should('exist').and('be.visible');
// Verify Quick Links
// Checks for the container
cy.get('.quicklinkscontainer').should('exist').and('be.visible');
// Ensure it has content (the link we seeded)
cy.get('.quicklinkscontainer a').should('have.length.at.least', 1);
// Verify Message
cy.get('.message').should('exist').and('be.visible').and('contain', 'Test Message');
});
});

View File

@@ -1,48 +0,0 @@
describe('Welcome Modal Flow', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('should complete the full onboarding flow', () => {
// Step 1: Intro
cy.contains('Welcome to Mue Tab', { timeout: 10000 }).should('be.visible');
cy.contains('Next').click();
// Step 2: Language
cy.contains('Choose your language').should('be.visible');
cy.contains('Next').click();
// Step 3: Import Settings
cy.get('.upload').should('be.visible');
cy.contains('Next').click();
// Step 4: Theme
cy.contains('Select a theme').should('be.visible');
cy.contains('Dark').click();
cy.get('body').should('have.class', 'dark');
cy.contains('Light').click();
cy.get('body').should('not.have.class', 'dark');
cy.contains('Auto').click();
cy.contains('Dark').click();
cy.contains('Next').click();
// Step 5: Style
cy.contains('Choose a style').should('be.visible');
cy.contains('Modern').click();
cy.contains('Next').click();
// Step 6: Privacy
cy.contains('Privacy Options').should('be.visible');
cy.contains('Next').click();
// Step 7: Final
cy.contains('Final step').should('be.visible');
cy.contains('Finish').click();
// Verify Dashboard
cy.get('.greeting').should('be.visible');
});
});

View File

@@ -1,18 +0,0 @@
{
"file": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e",
"location": {
"name": "Loch Lomond, United Kingdom",
"latitude": 56.2466465,
"longitude": -4.7208886
},
"photographer": "Adam Jang",
"photographer_page": "https://unsplash.com/@adamjang",
"camera": "Canon, EOS 70D",
"views": 1500000,
"downloads": 10000,
"likes": 500,
"category": "nature",
"colour": "#262626",
"blur_hash": "L25#t70000?b00?b?bof00D%?bof",
"pun": "test_unique_id"
}

View File

@@ -1,9 +0,0 @@
[
{
"quote": "The only way to do great work is to love what you do.",
"author": "Steve Jobs",
"category": "inspirational",
"language": "en",
"authorlink": "https://en.wikipedia.org/wiki/Steve_Jobs"
}
]

View File

@@ -1,24 +0,0 @@
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@@ -1,19 +0,0 @@
// This example e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -5,6 +5,7 @@ import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-config-prettier';
export default [
// Ignore patterns
{
ignores: [
'**/node_modules/**',
@@ -17,6 +18,7 @@ export default [
],
},
// Base config for all JS/JSX files
{
files: ['**/*.{js,jsx,mjs}'],
languageOptions: {
@@ -24,6 +26,7 @@ export default [
sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } },
globals: {
// Browser globals
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
@@ -45,6 +48,7 @@ export default [
AbortController: 'readonly',
btoa: 'readonly',
atob: 'readonly',
// Node globals for scripts
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
@@ -60,17 +64,21 @@ export default [
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-uses-react': 'off',
// React specific rules
'react/prop-types': 'off', // Using PropTypes is optional
'react/react-in-jsx-scope': 'off', // Not needed with React 17+
'react/jsx-uses-react': 'off', // Not needed with React 17+
// General rules
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'no-console': ['warn', { allow: ['warn', 'error'] }],
// Modern JS
'prefer-const': 'warn',
'no-var': 'error',
},
},
// Prettier config (must be last to override other formatting rules)
prettier,
];

View File

@@ -12,7 +12,6 @@
"@/*": ["./*"],
"i18n/*": ["./i18n/*"],
"components/*": ["./components/*"],
"hooks/*": ["./hooks/*"],
"assets/*": ["./assets/*"],
"config/*": ["./config/*"],
"features/*": ["./features/*"],

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -1,8 +0,0 @@
{
"name": {
"message": "Mue"
},
"description": {
"message": "Fast, open and free-to-use new tab page for modern browsers."
}
}

View File

@@ -4,9 +4,9 @@
"default_locale": "en",
"name": "__MSG_name__",
"description": "__MSG_description__",
"version": "7.6.1",
"version": "7.6.0",
"homepage_url": "https://muetab.com",
"permissions": ["search", "bookmarks"],
"permissions": ["search"],
"action": {
"default_icon": "icons/128x128.png"
},

View File

@@ -2,9 +2,8 @@
"manifest_version": 3,
"name": "Mue",
"description": "Fast, open and free-to-use new tab page for modern browsers.",
"version": "7.6.1",
"version": "7.6.0",
"homepage_url": "https://muetab.com",
"permissions": ["bookmarks"],
"action": {
"default_icon": "icons/128x128.png"
},

View File

@@ -29,14 +29,12 @@
"blurhash": "^2.0.5",
"fast-blurhash": "^1.1.4",
"image-conversion": "^2.1.1",
"maplibre-gl": "^5.24.0",
"react": "^19.2.3",
"react-best-gradient-color-picker": "^3.0.14",
"react-clock": "6.0.0",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-modal": "3.16.3",
"react-router": "^7.13.0",
"react-toastify": "11.0.5",
"use-debounce": "^10.1.0"
},
@@ -47,7 +45,6 @@
"@eslint/js": "^9.39.2",
"@vitejs/plugin-react-swc": "^4.2.2",
"adm-zip": "0.5.16",
"cypress": "^15.10.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
@@ -65,11 +62,8 @@
"scripts": {
"dev": "vite",
"dev:host": "vite --host",
"test:e2e": "cypress open",
"test:e2e:headless": "cypress run",
"translations": "cd scripts && node updatetranslations.cjs",
"translations:percentages": "node scripts/updateTranslationPercentages.cjs",
"translations:unused": "node scripts/findUnusedTranslations.cjs",
"build": "vite build",
"pretty": "prettier --write \"./**/*.{js,jsx,json,scss,css}\"",
"lint": "eslint \"./src/**/*.{js,jsx}\" && stylelint \"./src/**/*.{scss,css}\"",
@@ -77,4 +71,4 @@
"postinstall": "husky",
"prepare": "husky"
}
}
}

View File

@@ -27,12 +27,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
message = request?.userInfo?["message"]
}
os_log(
.default,
"Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)",
String(describing: message),
profile?.uuidString ?? "none"
)
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
let response = NSExtensionItem()
if #available(iOS 15.0, macOS 11.0, *) {

View File

@@ -1,11 +1,11 @@
{
"colors": [
"colors" : [
{
"idiom": "universal"
"idiom" : "universal"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,68 +1,68 @@
{
"images": [
"images" : [
{
"filename": "icon_16x16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename": "icon_16x16@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename": "icon_32x32@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename": "icon_128x128.png",
"idiom": "mac",
"scale": "1x",
"size": "128x128"
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename": "icon_128x128@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename": "icon_256x256@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename": "icon_512x512@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,6 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,20 +1,20 @@
{
"images": [
"images" : [
{
"idiom": "universal",
"scale": "1x"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom": "universal",
"scale": "3x"
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,25 +1,22 @@
function show(enabled, useSettingsInsteadOfPreferences) {
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('state-on')[0].innerText =
'Extension is enabled and ready to use!';
document.getElementsByClassName('state-off')[0].innerText =
'Extension is disabled. Enable it in Safari Settings.';
document.getElementsByClassName('state-unknown')[0].innerText =
'Enable Mue in Safari Settings to get started.';
document.getElementsByClassName('open-preferences')[0].innerText = 'Open Safari Settings';
}
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('state-on')[0].innerText = "Extension is enabled and ready to use!";
document.getElementsByClassName('state-off')[0].innerText = "Extension is disabled. Enable it in Safari Settings.";
document.getElementsByClassName('state-unknown')[0].innerText = "Enable Mue in Safari Settings to get started.";
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Settings";
}
if (typeof enabled === 'boolean') {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
document.body.classList.remove(`state-on`);
document.body.classList.remove(`state-off`);
}
if (typeof enabled === "boolean") {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
document.body.classList.remove(`state-on`);
document.body.classList.remove(`state-off`);
}
}
function openPreferences() {
webkit.messageHandlers.controller.postMessage('open-preferences');
webkit.messageHandlers.controller.postMessage("open-preferences");
}
document.querySelector('button.open-preferences').addEventListener('click', openPreferences);
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);

View File

@@ -1,133 +1,124 @@
* {
-webkit-user-select: none;
-webkit-user-drag: none;
cursor: default;
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-user-select: none;
-webkit-user-drag: none;
cursor: default;
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--spacing: 20px;
--text-color: #fff;
--shadow: 0 4px 20px rgb(0 0 0 / 30%);
--background-color: #0a0a0a;
--spacing: 20px;
--text-color: #ffffff;
--shadow: 0 4px 20px rgb(0 0 0 / 30%);
--background-color: #0A0A0A;
}
html {
height: 100%;
height: 100%;
}
body {
height: 100%;
margin: 0;
overflow: hidden;
font-family:
'Lexend Deca',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
sans-serif;
height: 100%;
margin: 0;
overflow: hidden;
font-family: 'Lexend Deca', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--background-color);
z-index: -1;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--background-color);
z-index: -1;
}
.content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: calc(var(--spacing) * 0.5);
height: 100%;
text-align: center;
color: var(--text-color);
padding: var(--spacing);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: calc(var(--spacing) * 0.5);
height: 100%;
text-align: center;
color: var(--text-color);
padding: var(--spacing);
}
img {
filter: drop-shadow(var(--shadow));
margin-bottom: 5px;
filter: drop-shadow(var(--shadow));
margin-bottom: 5px;
}
.title {
font-size: 3em;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
letter-spacing: -0.5px;
font-size: 3em;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
letter-spacing: -0.5px;
}
.subtitle {
font-size: 1.2em;
font-weight: 400;
margin: 0;
opacity: 0.95;
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
font-size: 1.2em;
font-weight: 400;
margin: 0;
opacity: 0.95;
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
}
.status-container {
margin: 10px 0;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
margin: 10px 0;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.status-container p {
font-size: 1.1em;
margin: 0;
padding: 12px 24px;
background: #fff;
color: #000;
border-radius: 12px;
box-shadow: var(--shadow);
font-size: 1.1em;
margin: 0;
padding: 12px 24px;
background: #fff;
color: #000;
border-radius: 12px;
box-shadow: var(--shadow);
}
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
display: none;
display: none;
}
body.state-on :is(.state-off, .state-unknown) {
display: none;
display: none;
}
body.state-off :is(.state-on, .state-unknown) {
display: none;
display: none;
}
button {
font-size: 1.1em;
font-weight: 600;
padding: 12px 28px;
background: #fff;
color: #000;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: var(--shadow);
margin-top: 5px;
font-size: 1.1em;
font-weight: 600;
padding: 12px 28px;
background: #fff;
color: #000;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: var(--shadow);
margin-top: 5px;
}
button:hover {
background: #f0f0f0;
transform: translateY(-2px);
box-shadow: 0 6px 25px rgb(0 0 0 / 40%);
background: #f0f0f0;
transform: translateY(-2px);
box-shadow: 0 6px 25px rgb(0 0 0 / 40%);
}
button:active {
transform: translateY(0);
box-shadow: var(--shadow);
transform: translateY(0);
box-shadow: var(--shadow);
}

View File

@@ -22,15 +22,11 @@ class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHan
self.webView.configuration.userContentController.add(self, name: "controller")
let mainUrl = Bundle.main.url(forResource: "Main", withExtension: "html")!
let resourceUrl = Bundle.main.resourceURL!
self.webView.loadFileURL(mainUrl, allowingReadAccessTo: resourceUrl)
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
SFSafariExtensionManager.getStateOfSafariExtension(
withIdentifier: extensionBundleIdentifier
) { (state, error) in
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
guard let state = state, error == nil else {
// Insert code to inform the user that something went wrong.
return
@@ -47,11 +43,11 @@ class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHan
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageBody = message.body as? String, messageBody == "open-preferences" else {
return
if (message.body as! String != "open-preferences") {
return;
}
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { _ in
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
DispatchQueue.main.async {
NSApplication.shared.terminate(nil)
}

View File

@@ -1,224 +0,0 @@
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const LOCALE_FILE = path.join(__dirname, '../src/i18n/locales/en_GB.json');
const ACHIEVEMENTS_FILE = path.join(__dirname, '../src/i18n/locales/achievements/en_GB.json');
const SEARCH_DIR = path.join(__dirname, '../src');
const FILE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.json'];
/**
* Flatten nested JSON object into dot-notation keys
* @param {Object} obj - The object to flatten
* @param {String} prefix - The prefix for nested keys
* @returns {Array} Array of flattened keys
*/
function flattenKeys(obj, prefix = '') {
const keys = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
keys.push(...flattenKeys(obj[key], fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
function getAllFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
if (!file.startsWith('.') && file !== 'node_modules' && file !== 'dist' && file !== 'build') {
getAllFiles(filePath, fileList);
}
} else {
const ext = path.extname(file);
if (FILE_EXTENSIONS.includes(ext)) {
fileList.push(filePath);
}
}
});
return fileList;
}
/**
* Search files for usage of a translation key
* Handles both direct usage and dynamic template literal construction
* @param {String} key - The translation key to search for
* @param {Array} files - Array of file paths to search
* @param {Map} fileContentsCache - Cache of file contents
* @returns {Boolean} True if the key is found in any file
*/
function isKeyUsed(key, files, fileContentsCache) {
const keySegments = key.split('.');
const patterns = [];
const escapedFullKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
patterns.push(new RegExp(`['"\`]${escapedFullKey}['"\`]`, 'g'));
if (keySegments.length >= 2) {
const lastTwo = keySegments.slice(-2).map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\.');
patterns.push(new RegExp(`['"\`]${lastTwo}['"\`]`, 'g'));
}
if (keySegments.length >= 3) {
const lastThree = keySegments.slice(-3).map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\.');
patterns.push(new RegExp(`['"\`]${lastThree}['"\`]`, 'g'));
}
if (keySegments.length >= 3) {
const finalSegment = keySegments[keySegments.length - 1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
patterns.push(new RegExp(`\\.${finalSegment}['"\`]`, 'g'));
}
for (const file of files) {
let content = fileContentsCache.get(file);
if (content === undefined) {
try {
content = fs.readFileSync(file, 'utf-8');
fileContentsCache.set(file, content);
} catch {
fileContentsCache.set(file, '');
continue;
}
}
for (const pattern of patterns) {
if (pattern.test(content)) {
return true;
}
}
}
return false;
}
function main() {
console.log('Finding unused translation keys...\n');
let translationKeys = [];
try {
const localeContent = fs.readFileSync(LOCALE_FILE, 'utf-8');
const localeData = JSON.parse(localeContent);
translationKeys = flattenKeys(localeData);
console.log(`Found ${translationKeys.length} translation keys in en_GB.json`);
} catch (error) {
console.error(`Error reading locale file: ${error.message}`);
process.exit(1);
}
try {
if (fs.existsSync(ACHIEVEMENTS_FILE)) {
const achievementsContent = fs.readFileSync(ACHIEVEMENTS_FILE, 'utf-8');
const achievementsData = JSON.parse(achievementsContent);
const achievementKeys = flattenKeys(achievementsData, 'achievements');
translationKeys.push(...achievementKeys);
console.log(`Found ${achievementKeys.length} achievement keys in achievements/en_GB.json`);
}
} catch (error) {
console.warn(`Warning: Could not read achievements file: ${error.message}`);
}
console.log(`\nTotal keys to check: ${translationKeys.length}`);
console.log('Scanning source files...');
const files = getAllFiles(SEARCH_DIR);
console.log(`Found ${files.length} files to search\n`);
const fileContentsCache = new Map();
const unusedKeys = [];
const usedKeys = [];
console.log('Searching for key usage (including template literals)...');
let processed = 0;
const totalKeys = translationKeys.length;
const startTime = Date.now();
for (const key of translationKeys) {
processed++;
if (processed % 10 === 0 || processed === totalKeys) {
const percent = Math.round(processed / totalKeys * 100);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
process.stdout.write(`\r Progress: ${processed}/${totalKeys} (${percent}%) - ${elapsed}s elapsed`);
}
if (isKeyUsed(key, files, fileContentsCache)) {
usedKeys.push(key);
} else {
unusedKeys.push(key);
}
}
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\r Progress: ${totalKeys}/${totalKeys} (100%) - ${totalTime}s total \n`);
console.log('\n' + '='.repeat(70));
console.log('RESULTS');
console.log('='.repeat(70) + '\n');
console.log(`Used keys: ${usedKeys.length}`);
console.log(`Unused keys: ${unusedKeys.length}`);
console.log(`Usage rate: ${((usedKeys.length / totalKeys) * 100).toFixed(2)}%\n`);
if (unusedKeys.length > 0) {
console.log('Unused translation keys:\n');
const grouped = {};
unusedKeys.forEach(key => {
const topLevel = key.split('.')[0];
if (!grouped[topLevel]) {
grouped[topLevel] = [];
}
grouped[topLevel].push(key);
});
Object.keys(grouped).sort().forEach(category => {
console.log(`\n ${category}:`);
grouped[category].sort().forEach(key => {
console.log(` - ${key}`);
});
});
const outputFile = path.join(__dirname, '../unused-translations.txt');
const outputContent = [
'# Unused Translation Keys',
`# Generated: ${new Date().toISOString()}`,
`# Total unused: ${unusedKeys.length}`,
`# Note: This script checks for full keys and partial keys (last 2-3 segments)`,
`# to catch dynamic template literal usage like \`\${PREFIX}.key\``,
'',
...unusedKeys.sort()
].join('\n');
fs.writeFileSync(outputFile, outputContent, 'utf-8');
console.log(`\nFull list saved to: ${path.relative(process.cwd(), outputFile)}`);
} else {
console.log('No unused translation keys found!');
}
console.log('\n' + '='.repeat(70) + '\n');
if (unusedKeys.length > 0) {
console.log('Tip: You can safely remove these keys from your translation files to reduce bundle size.');
console.log('Note: Some keys might be used dynamically - review before removing!');
}
}
main();

View File

@@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path');
const https = require('https');
// Language code mappings between Weblate and Mue
const CODE_MAPPINGS = {
de: 'de_DE',
id: 'id_ID',
@@ -68,7 +69,7 @@ async function updateTranslationPercentages() {
fs.writeFileSync(outputPath, JSON.stringify(percentages, null, 2));
fs.appendFileSync(outputPath, '\n');
console.log(`Translation percentages updated successfully!`);
console.log(`Translation percentages updated successfully!`);
console.log(` Total languages: ${Object.keys(percentages).length}`);
console.log(` Output: ${outputPath}`);

View File

@@ -16,12 +16,13 @@ const compareAndRemoveKeys = (json1, json2) => {
const localesDir = path.join(__dirname, '../src/i18n/locales');
const achievementsDir = path.join(localesDir, 'achievements');
const manifestLocalesDir = path.join(__dirname, '../manifest/_locales');
// Check if the locales directory exists, if not, create it
if (!fs.existsSync(localesDir)) {
fs.mkdirSync(localesDir, { recursive: true });
}
// Check if the achievements directory exists, if not, create it
if (!fs.existsSync(achievementsDir)) {
fs.mkdirSync(achievementsDir, { recursive: true });
}
@@ -72,6 +73,7 @@ fs.readdirSync(achievementsDir).forEach((file) => {
const locales = fs.readdirSync(localesDir);
locales.forEach((locale) => {
if (!fs.existsSync(path.join(achievementsDir, locale))) {
// ignore directories
if (fs.lstatSync(path.join(localesDir, locale)).isDirectory()) {
return;
}
@@ -81,30 +83,3 @@ fs.readdirSync(achievementsDir).forEach((file) => {
}
});
});
console.log('Syncing manifest/_locales with src/i18n/locales...');
const enManifestPath = path.join(manifestLocalesDir, 'en', 'messages.json');
if (!fs.existsSync(enManifestPath)) {
console.error(`English manifest file does not exist at '${enManifestPath}'`);
} else {
const enManifest = fs.readFileSync(enManifestPath, 'utf8');
const localeFiles = fs.readdirSync(localesDir).filter((file) => {
return !fs.lstatSync(path.join(localesDir, file)).isDirectory() && file.endsWith('.json');
});
localeFiles.forEach((localeFile) => {
const localeCode = localeFile.replace('.json', '');
const manifestLocalePath = path.join(manifestLocalesDir, localeCode);
const manifestMessagesPath = path.join(manifestLocalePath, 'messages.json');
if (!fs.existsSync(manifestLocalePath)) {
console.log(`Creating missing locale: ${localeCode}`);
fs.mkdirSync(manifestLocalePath, { recursive: true });
fs.writeFileSync(manifestMessagesPath, enManifest);
}
});
console.log('Manifest locales sync complete!');
}

View File

@@ -1,21 +1,15 @@
import { useEffect, useState } from 'react';
import { Outlet } from 'react-router';
import { ToastContainer } from 'react-toastify';
import Background from 'features/background/Background';
import Widgets from 'features/misc/views/Widgets';
import Modals from 'features/misc/modals/Modals';
import CustomWidgets from 'features/misc/CustomWidgets';
import { loadSettings, moveSettings } from 'utils/settings';
import EventBus from 'utils/eventbus';
import variables from 'config/variables';
import { TranslationProvider } from 'contexts/TranslationContext';
import { registerAllHandlers } from 'utils/marketplace/registerHandlers';
import { installDefaultPacks } from 'utils/marketplace/installDefaultPacks';
const useAppSetup = () => {
useEffect(() => {
registerAllHandlers();
const firstRun = localStorage.getItem('firstRun');
const stats = localStorage.getItem('stats');
@@ -26,10 +20,8 @@ const useAppSetup = () => {
loadSettings();
installDefaultPacks();
const refreshHandler = (data) => {
if (data === 'other' || data === 'greeting' || data === 'clock' || data === 'quote') {
if (data === 'other') {
loadSettings(true);
}
};
@@ -63,15 +55,11 @@ const App = () => {
useAppSetup();
const languagecode = variables.languagecode || localStorage.getItem('language') || 'en_GB';
const languagecode = localStorage.getItem('language') || 'en_GB';
return (
<TranslationProvider
initialLanguage={languagecode}
initialTranslations={variables.language?.messages || {}}
>
<TranslationProvider initialLanguage={languagecode}>
{showBackground && <Background />}
<CustomWidgets />
<ToastContainer
position="top-center"
autoClose={toastDisplayTime}
@@ -83,7 +71,6 @@ const App = () => {
<Widgets />
<Modals />
</div>
<Outlet />
</TranslationProvider>
);
};

View File

@@ -1,7 +1,6 @@
import React, { PureComponent } from 'react';
import { captureException } from '@sentry/react';
import variables from 'config/variables';
class ErrorBoundary extends PureComponent {
constructor(props) {
@@ -27,35 +26,21 @@ class ErrorBoundary extends PureComponent {
render() {
if (this.state.error) {
const title = variables.getMessage
? variables.getMessage('error_boundary.title')
: 'An error occurred';
const message = variables.getMessage
? variables.getMessage('error_boundary.message')
: 'Something went wrong. Please try refreshing the page.';
const reportButton = variables.getMessage
? variables.getMessage('error_boundary.report_button')
: 'Report Error';
const sentSuccessfully = variables.getMessage
? variables.getMessage('error_boundary.sent_successfully')
: 'Report sent successfully';
const supportDiscord = variables.getMessage
? variables.getMessage('error_boundary.support_discord')
: 'Get Support on Discord';
return (
<div className="criticalError">
<div className="criticalError-message">
<h1>{title}</h1>
<p>{message}</p>
<h1>A critical error has occurred</h1>
<p>
The new tab page could not be loaded. Please uninstall the extension and try again.
</p>
<div className="criticalError-actions">
{this.state.showReport ? (
<button onClick={() => this.reportError()}>{reportButton}</button>
<button onClick={() => this.reportError()}>Report Issue</button>
) : (
<p>{sentSuccessfully}</p>
<p>Sent Successfully</p>
)}
<a href="https://discord.gg/zv8C9F8" target="_blank" rel="noreferrer">
{supportDiscord}
Support Discord
</a>
</div>
</div>

View File

@@ -1,229 +1,77 @@
import { useT } from 'contexts';
import variables from 'config/variables';
import { useState, memo, useEffect } from 'react';
import { useState, memo } from 'react';
import { MdAddLink, MdClose } from 'react-icons/md';
import { Tooltip } from 'components/Elements';
import { Button } from 'components/Elements';
import { Dropdown, Text } from 'components/Form/Settings';
import { IconService } from 'utils/quicklinks';
import './AddModal.scss';
function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, editLink }) {
const t = useT();
const [name, setName] = useState(edit ? editData.name : '');
const [url, setUrl] = useState(edit ? editData.url : '');
const [icon, setIcon] = useState(edit ? editData.icon : '');
const [iconType, setIconType] = useState(edit && editData.iconType ? editData.iconType : 'auto');
const [iconData, setIconData] = useState(edit && editData.iconData ? editData.iconData : null);
const [iconPreview, setIconPreview] = useState(null);
const [uploadError, setUploadError] = useState('');
const [suggestedName, setSuggestedName] = useState('');
const [resetKey, setResetKey] = useState(Date.now());
useEffect(() => {
if (!edit) {
localStorage.removeItem('quicklink_modal_name');
localStorage.removeItem('quicklink_modal_url');
localStorage.removeItem('quicklink_modal_iconType');
localStorage.removeItem('quicklink_modal_icon_url');
localStorage.removeItem('quicklink_modal_emoji');
setName('');
setUrl('');
setIcon('');
setIconType('auto');
setIconData(null);
setIconPreview(null);
setUploadError('');
setSuggestedName('');
setResetKey(Date.now());
}
}, [edit]);
useEffect(() => {
if (name || !url) {
setSuggestedName('');
return;
}
try {
let urlToTest = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
urlToTest = 'https://' + url;
}
const domain = new URL(urlToTest).hostname;
if (domain) {
const parts = domain.split('.');
let name = parts[0];
if (parts.length > 2 && parts[parts.length - 2] === 'co') {
name = parts[parts.length - 3];
}
setSuggestedName(name);
}
} catch (e) {
setSuggestedName('');
}
}, [url, name]);
const handleIconUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const dataUrl = await IconService.uploadCustomIcon(file);
setIconData(dataUrl);
setIconPreview(dataUrl);
setUploadError('');
} catch (e) {
setUploadError(e.message);
}
};
const handleSubmit = () => {
const finalName = name || suggestedName || '';
if (edit) {
editLink(editData, finalName, url, icon, iconType, iconData);
} else {
addLink(finalName, url, icon, iconType, iconData);
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
return (
<div className="addLinkModal" onKeyDown={handleKeyDown}>
<div className="addLinkModal">
<div className="shareHeader">
<span className="title">
{edit ? t('widgets.quicklinks.edit') : t('widgets.quicklinks.new')}
{edit
? variables.getMessage('widgets.quicklinks.edit')
: variables.getMessage('widgets.quicklinks.new')}
</span>
<Tooltip title={t('modals.welcome.buttons.close')}>
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')}>
<div className="close" onClick={() => closeModal()}>
<MdClose />
</div>
</Tooltip>
</div>
<div className="quicklinkModalTextbox">
<div className="addLinkModal-row">
<div className="addLinkModal-field">
<div className="addLinkModal-labelRow">
<label className="addLinkModal-label">{t('widgets.quicklinks.name')}</label>
{suggestedName && !name && (
<span className="addLinkModal-suggestedText">
{t('widgets.quicklinks.suggested', { name: suggestedName })}
</span>
)}
</div>
<Text
key={`name-${resetKey}`}
name="quicklink_modal_name"
noSetting={true}
onChange={(value) => setName(value)}
placeholder={suggestedName || t('widgets.quicklinks.name_placeholder')}
/>
</div>
<div className="addLinkModal-field">
<label className="addLinkModal-label">
{t('widgets.quicklinks.url')}
<span className="addLinkModal-required">*</span>
</label>
<Text
key={`url-${resetKey}`}
name="quicklink_modal_url"
noSetting={true}
onChange={(value) => {
let finalValue = value;
if (value && !value.startsWith('http://') && !value.startsWith('https://')) {
finalValue = 'https://' + value;
}
setUrl(finalValue);
}}
placeholder={t('widgets.quicklinks.url_placeholder')}
/>
</div>
</div>
<div className="addLinkModal-dropdownWrapper">
<Dropdown
label={t('widgets.quicklinks.icon_type_label')}
name="quicklink_modal_iconType"
noSetting={true}
onChange={(value) => setIconType(value)}
items={[
{ value: 'auto', text: t('widgets.quicklinks.icon_type_auto') },
{ value: 'custom_url', text: t('widgets.quicklinks.icon_type_custom_url') },
{ value: 'custom_upload', text: t('widgets.quicklinks.icon_type_upload') },
{ value: 'emoji', text: t('widgets.quicklinks.icon_type_emoji') },
{ value: 'letter', text: t('widgets.quicklinks.icon_type_letter') },
]}
<div className="text-field" style={{ gridColumn: 'span 2' }}>
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.name')}
value={name}
onChange={(e) => setName(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
/>
</div>
<div className="text-field">
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.url')}
value={url}
onChange={(e) => setUrl(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
/>
</div>
<div className="text-field">
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.icon')}
value={icon}
onChange={(e) => setIcon(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
/>
</div>
{iconType === 'custom_url' && (
<div className="text-field" style={{ gridColumn: 'span 2' }}>
<Text
key={`icon-url-${resetKey}`}
name="quicklink_modal_icon_url"
noSetting={true}
onChange={(value) => setIcon(value)}
placeholder={t('widgets.quicklinks.icon_url_placeholder')}
/>
</div>
)}
{iconType === 'custom_upload' && (
<div className="text-field" style={{ gridColumn: 'span 2' }}>
<input
type="file"
accept="image/*"
onChange={handleIconUpload}
style={{ padding: '10px 0' }}
/>
{iconPreview && (
<img
src={iconPreview}
alt={t('common.alt_text.preview')}
style={{ width: '40px', height: '40px', marginTop: '8px', borderRadius: '4px' }}
/>
)}
{uploadError && (
<span className="dropdown-error" style={{ display: 'block', marginTop: '4px' }}>
{uploadError}
</span>
)}
</div>
)}
{iconType === 'emoji' && (
<div className="text-field" style={{ gridColumn: 'span 2' }}>
<Text
key={`emoji-${resetKey}`}
name="quicklink_modal_emoji"
noSetting={true}
onChange={(value) => setIcon(value)}
placeholder=""
/>
</div>
)}
</div>
<div className="addFooter">
<span className="dropdown-error">
{iconError} {urlError}
</span>
<Button
type="settings"
onClick={handleSubmit}
icon={<MdAddLink />}
label={
edit ? t('modals.main.settings.sections.quicklinks.edit') : t('widgets.quicklinks.add')
}
/>
{edit ? (
<Button
type="settings"
onClick={() => editLink(editData, name, url, icon)}
icon={<MdAddLink />}
label={variables.getMessage('modals.main.settings.sections.quicklinks.edit')}
/>
) : (
<Button
type="settings"
onClick={() => addLink(name, url, icon)}
icon={<MdAddLink />}
label={variables.getMessage('widgets.quicklinks.add')}
/>
)}
</div>
</div>
);

View File

@@ -1,81 +0,0 @@
@use 'scss/variables' as *;
@use 'scss/mixins' as *;
.addLinkModal {
width: 600px;
max-width: 90vw;
.addLinkModal-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
grid-column: span 2;
}
.addLinkModal-field {
display: flex;
flex-direction: column;
gap: 8px;
margin-right: 35px;
.text-field-container {
width: 100%;
.text-field {
width: 100%;
.text-field-input {
width: 100%;
}
}
}
}
.addLinkModal-labelRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.addLinkModal-label {
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 4px;
@include themed {
color: t($subColor);
}
}
.addLinkModal-suggestedText {
font-size: 11px;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
white-space: nowrap;
@include themed {
color: t($link);
opacity: 0.8;
}
}
.addLinkModal-required {
@include themed {
color: t($link);
}
}
.addLinkModal-dropdownWrapper {
grid-column: span 2;
.dropdown {
width: 100%;
}
}
}

View File

@@ -3,20 +3,7 @@ import Tooltip from 'components/Elements/Tooltip/Tooltip';
const Button = forwardRef(
(
{
icon,
label,
type,
iconPlacement,
onClick,
active,
disabled,
tooltipTitle,
tooltipKey,
href,
style,
badge,
},
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href, style, badge },
ref,
) => {
let className;
@@ -64,15 +51,9 @@ const Button = forwardRef(
);
const linkButton = (
<a
className={className}
onClick={onClick}
ref={ref}
disabled={disabled}
href={href}
style={style}
target="_blank"
rel="noopener noreferrer"
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href} style={style}
target="_blank"
rel="noopener noreferrer"
>
{icon}
{label}

View File

@@ -1,18 +1,17 @@
import { Suspense, lazy, useState, memo, useEffect, useRef, useMemo } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router';
import { useT } from 'contexts';
import { Suspense, lazy, useState, memo, useEffect } from 'react';
import variables from 'config/variables';
import './scss/index.scss';
import ModalLoader from './components/ModalLoader';
import ModalTopBar from './components/ModalTopBar';
import { TAB_TYPES } from './constants/tabConfig';
import { updateHash, parseDeepLink } from 'utils/deepLinking';
import { useRouterBridge } from '../../../router/RouterBridge';
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
const Library = lazy(() => import('../../../features/misc/views/Library'));
const Discover = lazy(() => import('../../../features/misc/views/Discover'));
// Map tab types to their corresponding components
const TAB_COMPONENTS = {
[TAB_TYPES.SETTINGS]: Settings,
[TAB_TYPES.LIBRARY]: Library,
@@ -20,224 +19,185 @@ const TAB_COMPONENTS = {
};
function MainModal({ modalClose, deepLinkData }) {
const t = useT();
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
const { deepLinkData: routerDeepLinkData } = useRouterBridge();
// Use router-based deepLinkData if available, fallback to prop
// Memoize to prevent infinite loops in useEffect
const effectiveDeepLinkData = useMemo(
() => routerDeepLinkData || deepLinkData,
[location.pathname, deepLinkData],
);
// Derive currentTab from router location instead of state
const currentTab = effectiveDeepLinkData?.tab || TAB_TYPES.SETTINGS;
// Initialize with deep link tab if provided, otherwise default to settings
const initialTab = deepLinkData?.tab || TAB_TYPES.SETTINGS;
const [currentTab, setCurrentTab] = useState(initialTab);
const [currentSection, setCurrentSection] = useState('');
const [currentSectionName, setCurrentSectionName] = useState('');
const [currentSubSection, setCurrentSubSection] = useState(
effectiveDeepLinkData?.subSection || null,
);
const [currentSubSection, setCurrentSubSection] = useState(deepLinkData?.subSection || null);
const [productView, setProductView] = useState(null);
const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false);
const [navigationTrigger, setNavigationTrigger] = useState(null);
const [iframeBreadcrumbs, setIframeBreadcrumbs] = useState([]);
const historyRef = useRef([]);
const historyIndexRef = useRef(-1);
const [canGoBack, setCanGoBack] = useState(false);
const [canGoForward, setCanGoForward] = useState(false);
const updateNavButtons = () => {
setCanGoBack(historyIndexRef.current > 0);
setCanGoForward(historyIndexRef.current < historyRef.current.length - 1);
};
// Clear product view when changing tabs
useEffect(() => {
setProductView(null);
}, [currentTab]);
// Handle deep link updates (when modal opens via EventBus with new deep link)
useEffect(() => {
if (effectiveDeepLinkData) {
if (effectiveDeepLinkData.tab === TAB_TYPES.SETTINGS && effectiveDeepLinkData.section) {
if (deepLinkData) {
// Update tab if different
if (deepLinkData.tab && deepLinkData.tab !== currentTab) {
setCurrentTab(deepLinkData.tab);
}
// Handle settings section navigation with subsection
if (deepLinkData.tab === TAB_TYPES.SETTINGS && deepLinkData.section) {
setNavigationTrigger({
type: 'settings-section',
data: effectiveDeepLinkData.section,
data: deepLinkData.section,
timestamp: Date.now(),
});
if (effectiveDeepLinkData.subSection) {
setCurrentSubSection(effectiveDeepLinkData.subSection);
if (historyIndexRef.current >= 0) {
historyRef.current[historyIndexRef.current] = {
...historyRef.current[historyIndexRef.current],
subSection: effectiveDeepLinkData.subSection,
};
}
// Set sub-section if present
if (deepLinkData.subSection) {
setCurrentSubSection(deepLinkData.subSection);
}
}
}
}, [effectiveDeepLinkData]);
}, [deepLinkData]);
// Clear hash when modal closes
useEffect(() => {
return () => {
// When modal unmounts, clear the hash
if (window.location.hash) {
window.history.replaceState(null, null, window.location.pathname);
}
};
}, []);
// React to router location changes
useEffect(() => {
if (effectiveDeepLinkData) {
if (effectiveDeepLinkData.tab === TAB_TYPES.SETTINGS && effectiveDeepLinkData.section) {
setNavigationTrigger({
type: 'settings-section',
data: effectiveDeepLinkData.section,
timestamp: Date.now(),
});
setCurrentSubSection(effectiveDeepLinkData.subSection || null);
return;
}
// Listen for browser back/forward navigation via popstate
const handlePopState = () => {
const linkData = window.location.hash ? parseDeepLink(window.location.hash) : null;
if (
effectiveDeepLinkData.itemId &&
effectiveDeepLinkData.collection &&
effectiveDeepLinkData.fromCollection
) {
setNavigationTrigger({
type: 'collection',
data: effectiveDeepLinkData.collection,
timestamp: Date.now(),
});
setTimeout(() => {
if (linkData) {
// Update tab if different
if (linkData.tab && linkData.tab !== currentTab) {
setCurrentTab(linkData.tab);
}
// Handle settings section navigation
if (linkData.tab === TAB_TYPES.SETTINGS && linkData.section) {
setNavigationTrigger({
type: 'settings-section',
data: linkData.section,
timestamp: Date.now(),
});
// Set sub-section if present in hash
setCurrentSubSection(linkData.subSection || null);
return;
}
// Handle product and collection navigation
if (linkData.itemId && linkData.collection && linkData.fromCollection) {
// Product viewed from within a collection
// First set collection state, then navigate to product
setNavigationTrigger({
type: 'collection',
data: linkData.collection,
timestamp: Date.now(),
});
// Small delay to ensure collection state is set before navigating to product
setTimeout(() => {
setNavigationTrigger({
type: 'product',
data: {
id: linkData.itemId,
type: linkData.category,
},
timestamp: Date.now(),
});
}, 100);
} else if (linkData.itemId) {
// Product navigation (standalone)
setNavigationTrigger({
type: 'product',
data: {
id: effectiveDeepLinkData.itemId,
type: effectiveDeepLinkData.category,
id: linkData.itemId,
type: linkData.category,
},
timestamp: Date.now(),
});
}, 100);
} else if (effectiveDeepLinkData.itemId) {
setNavigationTrigger({
type: 'product',
data: {
id: effectiveDeepLinkData.itemId,
type: effectiveDeepLinkData.category,
},
timestamp: Date.now(),
});
} else if (effectiveDeepLinkData.collection) {
setNavigationTrigger({
type: 'collection',
data: effectiveDeepLinkData.collection,
timestamp: Date.now(),
});
} else {
setProductView(null);
setNavigationTrigger({
type: 'main',
data: { clearCollection: true },
timestamp: Date.now(),
});
} else if (linkData.collection) {
// Collection page navigation
setNavigationTrigger({
type: 'collection',
data: linkData.collection,
timestamp: Date.now(),
});
} else {
// Back to main view (clear collection state)
setProductView(null);
setNavigationTrigger({
type: 'main',
data: { clearCollection: true },
timestamp: Date.now(),
});
}
}
}
}, [effectiveDeepLinkData, currentTab]);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [currentTab]);
const handleChangeTab = (newTab) => {
historyRef.current = [];
historyIndexRef.current = -1;
updateNavButtons();
setCurrentTab(newTab);
// Update URL hash when tab changes
if (newTab === TAB_TYPES.DISCOVER) {
const section = effectiveDeepLinkData?.category || effectiveDeepLinkData?.section || 'all';
const itemId = effectiveDeepLinkData?.itemId ? `/${effectiveDeepLinkData.itemId}` : '';
navigate(`/${newTab}/${section}${itemId}`);
updateHash(`#${newTab}/all`);
} else if (newTab === TAB_TYPES.LIBRARY) {
navigate(`/${newTab}/added`);
updateHash(`#${newTab}/added`);
} else {
navigate(`/${newTab}`);
updateHash(`#${newTab}`);
}
};
const handleSectionChange = (section, sectionName) => {
setCurrentSection(section);
setCurrentSectionName(sectionName);
// Only reset subsection if we're actually changing to a different section
// Don't reset on initial section set (when currentSectionName is empty)
if (currentSectionName !== '' && currentSectionName !== sectionName) {
setCurrentSubSection(null);
}
const entry = {
section,
sectionName,
subSection:
currentSectionName === '' || currentSectionName === sectionName ? currentSubSection : null,
};
const current = historyRef.current[historyIndexRef.current];
if (
!current ||
current.sectionName !== sectionName ||
current.subSection !== entry.subSection
) {
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1).concat(entry);
historyIndexRef.current = historyRef.current.length - 1;
updateNavButtons();
}
// Clear sub-section when changing sections
setCurrentSubSection(null);
// Update URL hash when section changes
if (currentTab === TAB_TYPES.DISCOVER) {
// Don't navigate away if we're viewing a specific item
if (effectiveDeepLinkData?.itemId) {
return;
}
// For Discover tab, update with the section type
const sectionMap = {
[t('modals.main.marketplace.all')]: 'all',
[t('modals.main.marketplace.photo_packs')]: 'photo_packs',
[t('modals.main.marketplace.quote_packs')]: 'quote_packs',
[t('modals.main.marketplace.preset_settings')]: 'preset_settings',
[t('modals.main.marketplace.collections')]: 'collections',
[variables.getMessage('modals.main.marketplace.all')]: 'all',
[variables.getMessage('modals.main.marketplace.photo_packs')]: 'photo_packs',
[variables.getMessage('modals.main.marketplace.quote_packs')]: 'quote_packs',
[variables.getMessage('modals.main.marketplace.preset_settings')]: 'preset_settings',
[variables.getMessage('modals.main.marketplace.collections')]: 'collections',
};
const sectionKey = sectionMap[section];
if (sectionKey) {
navigate(`/${currentTab}/${sectionKey}`);
updateHash(`#${currentTab}/${sectionKey}`);
}
} else if (currentTab === TAB_TYPES.SETTINGS && sectionName) {
// Include subsection in hash if it exists and we're not changing sections
const path =
currentSubSection && (currentSectionName === '' || currentSectionName === sectionName)
? `/${currentTab}/${sectionName}/${currentSubSection}`
: `/${currentTab}/${sectionName}`;
navigate(path, { replace: true });
// For Settings tab, update with the section name
updateHash(`#${currentTab}/${sectionName}`, false);
}
};
const handleSubSectionChange = (subSection, sectionName) => {
setCurrentSubSection(subSection);
const effectiveSectionName = sectionName || currentSectionName;
const entry = { section: currentSection, sectionName: effectiveSectionName, subSection };
const current = historyRef.current[historyIndexRef.current];
if (
!current ||
current.sectionName !== effectiveSectionName ||
current.subSection !== subSection
) {
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1).concat(entry);
historyIndexRef.current = historyRef.current.length - 1;
updateNavButtons();
}
// Update URL hash when sub-section changes
if (currentTab === TAB_TYPES.SETTINGS && sectionName) {
if (subSection) {
navigate(`/${currentTab}/${sectionName}/${subSection}`);
updateHash(`#${currentTab}/${sectionName}/${subSection}`);
} else {
navigate(`/${currentTab}/${sectionName}`);
// Going back to section, remove sub-section from hash
updateHash(`#${currentTab}/${sectionName}`);
}
}
};
const handleProductView = (product) => {
setProductView(product);
// URL hash is already updated by child components (Browse.jsx)
// Browser history automatically tracks hash changes
};
const handleResetDiscoverToAll = () => {
@@ -245,39 +205,21 @@ function MainModal({ modalClose, deepLinkData }) {
setTimeout(() => setResetDiscoverToAll(false), 100);
};
const restoreHistoryEntry = (entry) => {
setCurrentSubSection(entry.subSection);
if (entry.sectionName !== currentSectionName) {
setCurrentSection(entry.section);
setCurrentSectionName(entry.sectionName);
setNavigationTrigger({
type: 'settings-section',
data: entry.sectionName,
timestamp: Date.now(),
});
}
if (currentTab === TAB_TYPES.SETTINGS) {
const hash = entry.subSection
? `#${currentTab}/${entry.sectionName}/${entry.subSection}`
: `#${currentTab}/${entry.sectionName}`;
window.history.replaceState(null, null, hash);
}
};
const handleBack = () => {
if (historyIndexRef.current <= 0) return;
historyIndexRef.current -= 1;
updateNavButtons();
restoreHistoryEntry(historyRef.current[historyIndexRef.current]);
// Clear iframe breadcrumbs when navigating back
setIframeBreadcrumbs([]);
window.history.back();
};
const handleForward = () => {
if (historyIndexRef.current >= historyRef.current.length - 1) return;
historyIndexRef.current += 1;
updateNavButtons();
restoreHistoryEntry(historyRef.current[historyIndexRef.current]);
window.history.forward();
};
// Browser manages history state, so we always show buttons enabled
// Browser will handle whether there's actually history to go back/forward
const canGoBack = true;
const canGoForward = true;
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
return (
@@ -302,7 +244,7 @@ function MainModal({ modalClose, deepLinkData }) {
<TabComponent
key={currentTab}
changeTab={handleChangeTab}
deepLinkData={effectiveDeepLinkData}
deepLinkData={deepLinkData}
currentTab={currentTab}
onSectionChange={handleSectionChange}
onSubSectionChange={handleSubSectionChange}

View File

@@ -1,20 +1,28 @@
import { memo } from 'react';
import { memo, useState, useEffect } from 'react';
import { useT } from 'contexts/TranslationContext';
import { Tooltip } from 'components/Elements';
import { getIconComponent, DIVIDER_LABELS } from '../constants/tabConfig';
function Tab({ label, currentTab, onClick, navbarTab, isCollapsed }) {
const t = useT();
const isExperimental = localStorage.getItem('experimental') !== 'false';
const [isExperimental, setIsExperimental] = useState(true);
useEffect(() => {
setIsExperimental(localStorage.getItem('experimental') !== 'false');
}, []);
// Get the icon component for this label (label is already translated)
const IconComponent = getIconComponent(label, { getMessage: t });
// Determine if this label should have a divider after it
const hasDivider = DIVIDER_LABELS.some((key) => t(key) === label);
// Build className
const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item';
const activeClass = navbarTab ? 'navbar-item-active' : 'tab-list-active';
const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`;
// Hide experimental tab if experimental mode is disabled
const isExperimentalTab = label === t('modals.main.settings.sections.experimental.title');
if (isExperimentalTab && !isExperimental) {
return <hr />;

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useLayoutEffect, useRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useT } from 'contexts/TranslationContext';
import variables from 'config/variables';
import Tab from './Tab';
@@ -6,8 +6,6 @@ import ReminderInfo from '../components/ReminderInfo';
import SidebarToggle from '../components/SidebarToggle';
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
import { TAB_TYPES } from '../constants/tabConfig';
import { SearchInput } from 'components/Form/Settings';
import EventBus from 'utils/eventbus';
const Tabs = ({
children,
@@ -21,6 +19,7 @@ const Tabs = ({
}) => {
const t = useT();
// Find initial section from deep link if available
const getInitialSection = () => {
if (deepLinkData?.section && sections) {
const section = sections.find((s) => s.name === deepLinkData.section);
@@ -31,15 +30,6 @@ const Tabs = ({
};
}
}
if (deepLinkData?.category && sections) {
const section = sections.find((s) => s.name === deepLinkData.category);
if (section) {
return {
label: t(section.label),
name: section.name,
};
}
}
return {
label: children[0]?.props.label,
name: children[0]?.props.name,
@@ -47,80 +37,73 @@ const Tabs = ({
};
const initial = getInitialSection();
const [currentTab, setCurrentTab] = useState(initial.label);
const [currentName, setCurrentName] = useState(initial.name);
const [showReminder, setShowReminder] = useState(localStorage.getItem('showReminder') === 'true');
const [sidebarCollapsed, setSidebarCollapsed] = useState(
localStorage.getItem('sidebarCollapsed') === 'true',
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(
localStorage.getItem('sidebarCollapsed') === 'true'
);
const [searchQuery, setSearchQuery] = useState('');
const contentRef = useRef(null);
const currentTab = (() => {
if (sections && currentName) {
const section = sections.find((s) => s.name === currentName);
if (section) {
return t(section.label);
}
}
const child = children.find((c) => c.props.name === currentName);
return child?.props.label || children[0]?.props.label;
})();
const handleTabClick = (tab, name) => {
if (name !== currentName) {
variables.stats.postEvent('tab', `Opened ${name}`);
}
setCurrentTab(tab);
setCurrentName(name);
// Scroll content to top when changing tabs
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
// Notify parent of section change with both label and name
if (onSectionChange) {
onSectionChange(tab, name);
}
};
// Notify parent of initial section on mount
useEffect(() => {
if (onSectionChange && currentTab) {
onSectionChange(currentTab, currentName);
}
}, []);
// React to deep link changes (e.g., when navigating to a suggested pack from settings)
// Update labels when language changes
useEffect(() => {
if (deepLinkData && sections) {
const targetSection = deepLinkData.section || deepLinkData.category;
if (targetSection) {
const section = sections.find((s) => s.name === targetSection);
if (section && section.name !== currentName) {
setCurrentName(section.name);
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
}
if (sections && currentName) {
const section = sections.find((s) => s.name === currentName);
if (section) {
const newLabel = t(section.label);
setCurrentTab(newLabel);
}
}
}, [deepLinkData, sections, currentName]);
}, [t, sections, currentName]);
// useLayoutEffect is appropriate here for synchronous state updates before paint
useLayoutEffect(() => {
// Handle navigation trigger for settings sections (popstate)
useEffect(() => {
if (navigationTrigger?.type === 'settings-section' && sections) {
const section = sections.find((s) => s.name === navigationTrigger.data);
if (section) {
const label = t(section.label);
setCurrentTab(label);
setCurrentName(section.name);
// Scroll content to top when navigating via browser history
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
}
}
}, [navigationTrigger, sections]);
}, [navigationTrigger, sections, t]);
// useLayoutEffect is appropriate here for synchronous state updates before paint
useLayoutEffect(() => {
// Reset to first tab when requested
useEffect(() => {
if (resetToFirst) {
setCurrentTab(children[0]?.props.label);
setCurrentName(children[0]?.props.name);
// Scroll content to top when resetting to first tab
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
@@ -130,40 +113,27 @@ const Tabs = ({
}
}, [resetToFirst]);
useEffect(() => {
const handleShowReminder = () => {
localStorage.setItem('showReminder', 'true');
setShowReminder(true);
};
EventBus.on('showReminder', handleShowReminder);
return () => EventBus.off('showReminder', handleShowReminder);
}, []);
const handleHideReminder = () => {
localStorage.setItem('showReminder', 'false');
setShowReminder(false);
};
const handleToggleSidebar = () => {
const newState = !sidebarCollapsed;
setSidebarCollapsed(newState);
const newState = !isSidebarCollapsed;
setIsSidebarCollapsed(newState);
localStorage.setItem('sidebarCollapsed', newState.toString());
};
// Show sidebar for Settings and Discover tabs
const showSidebar = activeTab === TAB_TYPES.SETTINGS || activeTab === TAB_TYPES.DISCOVER;
const filteredChildren = children.filter((tab) => {
if (!searchQuery.trim()) return true;
return tab.props.label.toLowerCase().includes(searchQuery.toLowerCase());
});
// Keyboard shortcut for sidebar toggle (Ctrl/Cmd + B)
useEffect(() => {
const handleKeyPress = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
e.preventDefault();
if (showSidebar) {
setSidebarCollapsed((prev) => !prev);
setIsSidebarCollapsed((prev) => !prev);
}
}
};
@@ -175,46 +145,35 @@ const Tabs = ({
return (
<div style={{ display: 'flex', width: '100%', height: '100%', overflow: 'hidden' }}>
{showSidebar ? (
<div className={`modalSidebar ${sidebarCollapsed ? 'collapsed' : 'expanded'}`}>
<div className={`modalSidebar ${isSidebarCollapsed ? 'collapsed' : 'expanded'}`}>
<div className="sidebarHeader">
<SidebarToggle isCollapsed={sidebarCollapsed} onToggle={handleToggleSidebar} />
{!sidebarCollapsed && activeTab === TAB_TYPES.SETTINGS && (
<SearchInput
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('widgets.search')}
fullWidth
/>
)}
<SidebarToggle isCollapsed={isSidebarCollapsed} onToggle={handleToggleSidebar} />
</div>
{filteredChildren.map((tab, index) => (
{children.map((tab, index) => (
<Tab
key={index}
currentTab={currentTab}
label={tab.props.label}
onClick={(nextTab) => handleTabClick(nextTab, tab.props.name)}
navbarTab={navbar}
isCollapsed={sidebarCollapsed}
isCollapsed={isSidebarCollapsed}
/>
))}
{searchQuery.trim() && filteredChildren.length === 0 && (
<div className="sidebarEmptyState">{t('widgets.weather.not_found')}</div>
{!isSidebarCollapsed && (
<ReminderInfo isVisible={showReminder} onHide={handleHideReminder} />
)}
</div>
) : null}
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 }}>
<ReminderInfo isVisible={showReminder} onHide={handleHideReminder} />
<div className="modalTabContent" ref={contentRef}>
{children.map((tab, index) => {
if (tab.props.label !== currentTab) {
return null;
}
<div className="modalTabContent" ref={contentRef}>
{children.map((tab, index) => {
if (tab.props.label !== currentTab) {
return null;
}
return (
<ErrorBoundary key={`error-boundary-${index}`}>{tab.props.children}</ErrorBoundary>
);
})}
</div>
return (
<ErrorBoundary key={`error-boundary-${index}`}>{tab.props.children}</ErrorBoundary>
);
})}
</div>
</div>
);

View File

@@ -1,4 +1,4 @@
import { useT } from 'contexts';
import variables from 'config/variables';
import SidebarSkeleton from './SidebarSkeleton';
const ModalLoader = ({ currentTab }) => (
@@ -11,7 +11,7 @@ const ModalLoader = ({ currentTab }) => (
<div className="emptyMessage">
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{t('modals.main.loading')}</span>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>
</div>
</div>

View File

@@ -1,33 +1,22 @@
import { useT } from 'contexts/TranslationContext';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md';
import { Tooltip, Button } from 'components/Elements';
import { NAVBAR_BUTTONS, TAB_TYPES } from '../constants/tabConfig';
import mueAboutIcon from 'assets/icons/mue_about.png';
// Map marketplace types to translation keys
const MARKETPLACE_TYPE_TO_KEY = {
photo_packs: 'modals.main.marketplace.photo_packs',
'photo packs': 'modals.main.marketplace.photo_packs',
photos: 'modals.main.marketplace.photo_packs',
quote_packs: 'modals.main.marketplace.quote_packs',
'quote packs': 'modals.main.marketplace.quote_packs',
quotes: 'modals.main.marketplace.quote_packs',
preset_settings: 'modals.main.marketplace.preset_settings',
'preset settings': 'modals.main.marketplace.preset_settings',
settings: 'modals.main.marketplace.preset_settings',
collections: 'modals.main.marketplace.collections',
all: 'modals.main.marketplace.all',
};
const BREADCRUMB_LABEL_TO_CATEGORY = {
'photo packs': 'photo_packs',
'quote packs': 'quote_packs',
'preset settings': 'preset_settings',
collections: 'collections',
marketplace: 'all',
};
function ModalTopBar({
currentTab,
currentSection,
@@ -36,7 +25,6 @@ function ModalTopBar({
productView,
iframeBreadcrumbs,
onTabChange,
onSectionChange,
onSubSectionChange,
onClose,
onBack,
@@ -45,8 +33,8 @@ function ModalTopBar({
canGoForward,
}) {
const t = useT();
const navigate = useNavigate();
// Track installed addons count for badge
const [installedCount, setInstalledCount] = useState(() => {
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
@@ -66,8 +54,10 @@ function ModalTopBar({
}
};
// Listen for storage events (changes from other tabs)
window.addEventListener('storage', updateCount);
// Listen for custom event for same-tab updates
window.addEventListener('installedAddonsChanged', updateCount);
return () => {
@@ -76,17 +66,20 @@ function ModalTopBar({
};
}, []);
// Get the current tab label
const currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab);
const currentTabLabel = currentTabButton ? t(currentTabButton.messageKey) : '';
// Utility function to get translated sub-section label
const getSubSectionLabel = (subSection, sectionName) => {
if (!subSection || !sectionName) {
return subSection;
}
if (!subSection || !sectionName) return subSection;
// Use the same translation pattern as the section components
const translationKey = `modals.main.settings.sections.${sectionName}.${subSection}.title`;
const translated = t(translationKey);
// If translation key is returned as-is or empty, it means translation doesn't exist
// Fall back to capitalized sub-section name
if (!translated || translated === translationKey) {
return subSection.charAt(0).toUpperCase() + subSection.slice(1);
}
@@ -94,78 +87,53 @@ function ModalTopBar({
return translated;
};
// Determine breadcrumb path with click handlers
const breadcrumbPath = [];
if (currentTabLabel) {
breadcrumbPath.push({
label: currentTabLabel,
onClick:
(iframeBreadcrumbs && iframeBreadcrumbs.length > 0) || productView
? () => {
navigate('/discover/all');
}
: null,
onClick: productView ? productView.onBackToAll : null, // Clickable if viewing a product
});
// Check if we have iframe breadcrumbs (from Discover iframe)
// If so, only use the last item (the item name) and keep our section
if (iframeBreadcrumbs && iframeBreadcrumbs.length > 0) {
const relevantCrumbs = iframeBreadcrumbs.slice(1);
relevantCrumbs.forEach((crumb, index) => {
const isLast = index === relevantCrumbs.length - 1;
const lowerLabel = crumb.label.toLowerCase();
const translationKey = MARKETPLACE_TYPE_TO_KEY[lowerLabel];
const displayLabel = translationKey ? t(translationKey) : crumb.label;
const categoryKey = BREADCRUMB_LABEL_TO_CATEGORY[lowerLabel];
// Get the last breadcrumb item (the item name)
const lastCrumb = iframeBreadcrumbs[iframeBreadcrumbs.length - 1];
// Add current section if available and different from the last crumb
if (currentSection && currentSection !== lastCrumb.label) {
breadcrumbPath.push({
label: displayLabel,
onClick:
crumb.clickable && !isLast && crumb.href
? () => {
const href = crumb.href;
if (href.includes('type=')) {
const urlParams = new URLSearchParams(href.split('?')[1]);
const typeParam = urlParams.get('type');
if (typeParam) {
navigate(`/discover/${typeParam}`);
}
} else if (href.includes('/collections')) {
navigate('/discover/collections');
} else if (href.includes('/collection/')) {
const collectionId = href.split('/collection/')[1]?.split('?')[0];
if (collectionId) {
navigate(`/discover/collection/${collectionId}`);
}
} else if (categoryKey) {
navigate(`/discover/${categoryKey}`);
} else if (href === '/marketplace' || href === '/marketplace/') {
navigate('/discover/all');
} else {
const stepsBack = relevantCrumbs.length - index - 1;
for (let i = 0; i < stepsBack; i++) {
window.history.back();
}
}
}
: null,
label: currentSection,
onClick: () => onBack(), // Clickable to go back
});
}
// Add the item name from iframe
breadcrumbPath.push({
label: lastCrumb.label,
onClick: null, // Current item - not clickable
});
} else if (productView) {
// If viewing a collection page itself (not a product within it)
if (productView.isCollection) {
// Show: Discover > Collection Name
breadcrumbPath.push({
label: productView.collectionTitle || productView.name,
onClick: null,
onClick: null, // Current page - not clickable
});
} else {
// Viewing a product
// Show: Discover > Collection/Category > Product
if (productView.fromCollection && productView.collectionTitle) {
// If from a collection, show collection name
breadcrumbPath.push({
label: productView.collectionTitle,
onClick: productView.onBack || null,
});
} else {
// Otherwise show category
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
if (categoryKey) {
breadcrumbPath.push({
@@ -174,23 +142,24 @@ function ModalTopBar({
});
}
}
// Add product name as final breadcrumb
breadcrumbPath.push({
label: productView.name,
onClick: null,
onClick: null, // Current item - not clickable
});
}
} else if (currentSection) {
// Show: Tab > Section or Tab > Section > Sub-Section
breadcrumbPath.push({
label: currentSection,
onClick: currentSubSection
? () => onSubSectionChange(null, currentSectionName)
: null,
onClick: currentSubSection ? () => onSubSectionChange(null) : null, // Clickable if sub-section is active
});
// Add sub-section if present
if (currentSubSection) {
breadcrumbPath.push({
label: getSubSectionLabel(currentSubSection, currentSectionName),
onClick: null,
onClick: null, // Current sub-section - not clickable
});
}
}
@@ -200,30 +169,35 @@ function ModalTopBar({
<div className="modalTopBar">
<div className="topBarLeft">
<div className="navigationButtons">
<Tooltip title={t('common.navigation.back')} key="backTooltip">
<Tooltip title="Back" key="backTooltip">
<button
className="navButton"
onClick={onBack}
disabled={!canGoBack}
aria-label={t('common.navigation.navigate_back')}
aria-label="Navigate back"
>
<MdArrowBack />
</button>
</Tooltip>
<Tooltip title={t('common.navigation.forward')} key="forwardTooltip">
<Tooltip title="Forward" key="forwardTooltip">
<button
className="navButton"
onClick={onForward}
disabled={!canGoForward}
aria-label={t('common.navigation.navigate_forward')}
aria-label="Navigate forward"
>
<MdArrowForward />
</button>
</Tooltip>
</div>
<img src={mueAboutIcon} alt="Mue" className="topBarLogo" draggable={false} />
<img
src={mueAboutIcon}
alt="Mue"
className="topBarLogo"
draggable={false}
/>
{breadcrumbPath.length > 0 && (
<nav className="breadcrumbs" aria-label={t('common.navigation.breadcrumb_navigation')}>
<nav className="breadcrumbs" aria-label="Breadcrumb navigation">
{breadcrumbPath.map((item, index) => {
const isLast = index === breadcrumbPath.length - 1;
const isClickable = item.onClick !== null;
@@ -242,7 +216,7 @@ function ModalTopBar({
}}
role="button"
tabIndex={0}
aria-label={t('common.navigation.navigate_to', { item: item.label })}
aria-label={`Navigate to ${item.label}`}
>
{item.label}
</span>
@@ -254,9 +228,7 @@ function ModalTopBar({
{item.label}
</span>
)}
{!isLast && (
<MdChevronRight className="breadcrumb-separator" aria-hidden="true" />
)}
{!isLast && <MdChevronRight className="breadcrumb-separator" aria-hidden="true" />}
</span>
);
})}
@@ -266,8 +238,8 @@ function ModalTopBar({
<div className="topBarRight">
<div className="topBarNavigation">
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => {
const badgeValue =
tab === TAB_TYPES.LIBRARY && installedCount > 0 ? installedCount : undefined;
// Show badge for Library tab when there are installed addons
const badgeValue = tab === TAB_TYPES.LIBRARY && installedCount > 0 ? installedCount : undefined;
return (
<Button

View File

@@ -1,23 +1,26 @@
import { useT } from 'contexts';
import variables from 'config/variables';
import { MdRefresh, MdClose } from 'react-icons/md';
const ReminderInfo = ({ isVisible, onHide }) => {
const t = useT();
if (!isVisible) {
return null;
}
return (
<div className="reminder-info">
<span className="title">{t('modals.main.settings.reminder.title')}</span>
<span className="subtitle">{t('modals.main.settings.reminder.message')}</span>
<div className="shareHeader">
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
<span className="closeModal" onClick={onHide}>
<MdClose />
</span>
</div>
<span className="subtitle">
{variables.getMessage('modals.main.settings.reminder.message')}
</span>
<button onClick={() => window.location.reload()}>
<MdRefresh />
{t('modals.main.error_boundary.refresh')}
{variables.getMessage('modals.main.error_boundary.refresh')}
</button>
<span className="closeModal" onClick={onHide}>
<MdClose />
</span>
</div>
);
};

View File

@@ -1,41 +1,34 @@
import { TAB_TYPES } from '../constants/tabConfig';
// Tab-specific configurations with exact divider positions
const TAB_CONFIGS = {
[TAB_TYPES.SETTINGS]: {
itemCount: 16,
dividerPositions: [10, 12],
textWidths: [80, 100, 70, 90, 85, 75, 80, 95, 90, 75, 85, 90, 85, 80, 70, 95],
showSearch: true,
itemCount: 16, // Excluding experimental
dividerPositions: [10, 12], // After Weather, Language
textWidths: [80, 100, 70, 90, 85, 75, 80, 95, 90, 75, 85, 90, 85, 80, 70, 95], // Fixed widths in pixels
},
[TAB_TYPES.DISCOVER]: {
itemCount: 5,
dividerPositions: [0],
textWidths: [60, 95, 95, 110, 90],
showSearch: false,
dividerPositions: [0], // After "All"
textWidths: [60, 95, 95, 110, 90], // Fixed widths
},
[TAB_TYPES.LIBRARY]: {
itemCount: 0,
itemCount: 0, // Library doesn't show sidebar
dividerPositions: [],
textWidths: [],
showSearch: false,
},
};
const SidebarSkeleton = ({ currentTab = TAB_TYPES.SETTINGS }) => {
const config = TAB_CONFIGS[currentTab] || TAB_CONFIGS[TAB_TYPES.SETTINGS];
// Library tab doesn't show sidebar
if (config.itemCount === 0) {
return null;
}
return (
<div className="sidebarSkeleton">
{/* Header with toggle button and optional search */}
<div className="skeletonHeader">
<div className="skeletonToggle pulse" />
{config.showSearch && <div className="skeletonSearch pulse" />}
</div>
{Array.from({ length: config.itemCount }).map((_, index) => {
const hasDivider = config.dividerPositions.includes(index);
const textWidth = config.textWidths[index] || 80;

View File

@@ -1,4 +1,4 @@
import { FiSidebar } from 'react-icons/fi';
import { FiSidebar } from "react-icons/fi";
import { Tooltip } from 'components/Elements';
import { useT } from 'contexts/TranslationContext';

View File

@@ -1,5 +1,7 @@
import variables from 'config/variables';
import {
MdSettings,
MdWidgets,
MdShoppingBasket,
MdTune,
MdBookmarks,
MdExplore,
@@ -26,12 +28,14 @@ import {
MdCollectionsBookmark,
} from 'react-icons/md';
// Tab type constants
export const TAB_TYPES = {
SETTINGS: 'settings',
LIBRARY: 'library',
DISCOVER: 'discover',
};
// Icon component mapping - using component references instead of elements
export const ICON_COMPONENTS = {
SETTINGS: MdTune,
LIBRARY: MdBookmarks,
@@ -59,8 +63,9 @@ export const ICON_COMPONENTS = {
COLLECTIONS: MdCollectionsBookmark,
};
// Message keys for icon mapping
export const MESSAGE_KEYS = {
OVERVIEW: 'modals.main.settings.sections.order.title',
OVERVIEW: 'modals.main.marketplace.product.overview',
SETTINGS: 'modals.main.navbar.settings',
LIBRARY: 'modals.main.navbar.library',
DISCOVER: 'modals.main.navbar.discover',
@@ -90,6 +95,8 @@ export const MESSAGE_KEYS = {
COLLECTIONS: 'modals.main.marketplace.collections',
};
// Helper to get icon component by translated label
// This function builds a map at runtime using variables.getMessage
export const getIconComponent = (label, variables) => {
const iconMap = {
[variables.getMessage(MESSAGE_KEYS.OVERVIEW)]: ICON_COMPONENTS.OVERVIEW,
@@ -125,6 +132,7 @@ export const getIconComponent = (label, variables) => {
return iconMap[label];
};
// Navbar configuration
export const NAVBAR_BUTTONS = [
{
tab: TAB_TYPES.SETTINGS,
@@ -143,6 +151,7 @@ export const NAVBAR_BUTTONS = [
},
];
// Labels that should have dividers after them
export const DIVIDER_LABELS = [
'modals.main.settings.sections.weather.title',
'modals.main.settings.sections.language.title',

View File

@@ -9,8 +9,7 @@
@use 'modules/scrollbars' as *;
@use 'settings/main' as *;
@use 'marketplace/main' as *;
/* Fixed: Added sass:map module */
// Fixed: Added sass:map module
.Overlay {
position: fixed;
@@ -20,16 +19,6 @@
height: 100vh;
display: grid;
place-items: center;
opacity: 0;
transition: opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
&.ReactModal__Overlay--after-open {
opacity: 1;
}
&.ReactModal__Overlay--before-close {
opacity: 0;
}
}
.Modal {
@@ -38,15 +27,14 @@
}
box-shadow: 0 0 20px rgb(0 0 0 / 30%);
opacity: 0;
opacity: 1;
z-index: -2;
transition-timing-function: ease-in;
border-radius: map.get($modal, 'border-radius');
-webkit-user-select: none;
user-select: none;
overflow: hidden;
transform: scale(0.95);
transition: opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transform: scale(0);
transition: all 0.3s cubic-bezier(0.47, 1.64, 0.41, 0.8);
&:focus {
outline: 0;
@@ -60,7 +48,7 @@
.closePositioning {
position: absolute;
top: 3rem;
inset-inline-end: 3rem;
right: 3rem;
}
.closeModal {
@@ -102,13 +90,14 @@
.ReactModal__Content--before-close {
opacity: 0;
transform: scale(0.95);
transform: scale(0);
}
#modal {
height: 80vh;
width: clamp(60vw, 1400px, 90vw);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
overflow: hidden;
border-radius: 12px;
@@ -324,51 +313,27 @@ h5 {
.reminder-info {
display: flex;
flex-flow: row;
align-items: center;
gap: 12px;
padding: 10px 16px;
flex-shrink: 0;
flex-flow: column;
position: absolute;
bottom: 0;
padding: 15px;
gap: 15px;
@include themed {
background-color: t($modal-secondaryColour);
border-bottom: 1px solid t($modal-sidebarActive);
}
.title {
font-weight: 600;
white-space: nowrap;
}
.subtitle {
opacity: 0.7;
flex: 1;
border-radius: t($borderRadius);
border: 1px solid t($modal-sidebarActive);
}
button {
@include basicIconButton(8px 14px, 0.875rem, modal);
@include basicIconButton(5px, 5px, modal);
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
justify-content: center;
gap: 15px;
svg {
margin: 0 !important;
font-size: 1rem;
}
}
.closeModal {
cursor: pointer;
display: flex;
align-items: center;
flex-shrink: 0;
opacity: 0.6;
&:hover {
opacity: 1;
}
}

View File

@@ -1,10 +1,11 @@
// this file is too long
@use 'modules/lightbox' as *;
@use 'scss/variables' as *;
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 280px));
grid-gap: 1.5rem;
margin-top: 15px;
margin-bottom: 30px;
@@ -23,7 +24,7 @@
background-size: cover;
padding: 1.5rem;
will-change: transform;
transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0); // Force GPU acceleration
@include themed {
background-color: t($modal-secondaryColour);
@@ -69,7 +70,6 @@
display: flex;
flex-flow: column;
align-items: flex-start;
width: 100%;
.card-title {
font-size: 18px;
@@ -114,17 +114,6 @@
}
}
&.item-disabled {
opacity: 0.5;
.card-title,
.card-subtitle {
@include themed {
color: t($subColor);
}
}
}
.item-uninstall-btn {
display: flex;
align-items: center;
@@ -133,7 +122,7 @@
height: 28px;
border-radius: 50%;
border: none;
background-color: rgb(0 0 0 / 50%);
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: background-color 0.2s ease;
@@ -143,7 +132,7 @@
}
&:hover {
background-color: rgb(220 50 50 / 90%);
background-color: rgba(220, 50, 50, 0.9);
}
}
@@ -157,8 +146,8 @@
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid rgb(255 255 255 / 40%);
box-shadow: 0 2px 6px rgb(0 0 0 / 30%);
border: 2px solid rgba(255, 255, 255, 0.4);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
z-index: 2;
pointer-events: none;
will-change: transform;
@@ -176,7 +165,7 @@
width: 28px;
height: 28px;
border-radius: 50%;
background-color: rgb(100 100 100 / 90%);
background-color: rgba(100, 100, 100, 0.9);
cursor: help;
svg {
@@ -199,13 +188,6 @@
}
}
.item-card-actions {
display: flex;
gap: 8px;
margin-top: 1.5rem;
width: 100%;
}
.itemPage {
display: flex;
flex-flow: row;
@@ -229,7 +211,7 @@
table {
table-layout: fixed;
width: 100%;
overflow-wrap: break-word !important;
word-wrap: break-word !important;
font-size: 16px;
border-collapse: collapse;
}
@@ -241,7 +223,7 @@
width: 28px;
height: 28px;
border-radius: 50%;
background-color: rgb(100 100 100 / 90%);
background-color: rgba(100, 100, 100, 0.9);
cursor: help;
svg {
@@ -274,7 +256,7 @@
.emptyMessage {
display: grid;
place-items: center;
gap: 5px;
grid-gap: 5px;
padding: 50px;
.title,
@@ -305,7 +287,7 @@
flex-flow: column;
text-align: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
user-select: none;
img {
@@ -348,6 +330,7 @@ p.author {
cursor: pointer;
}
.filter {
display: flex;
flex-flow: row;
@@ -395,7 +378,7 @@ p.author {
margin-bottom: 15px;
.tooltip {
margin-inline-end: 25px;
margin-right: 25px;
}
.mainTitle {
@@ -427,7 +410,7 @@ p.author {
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
user-select: none;
@include themed {
@@ -437,7 +420,7 @@ p.author {
&:hover {
@include themed {
background-color: rgb(255 255 255 / 15%);
background-color: rgba(255, 255, 255, 0.15);
}
}
@@ -455,7 +438,7 @@ p.author {
}
&:focus-visible {
outline: 2px solid rgb(255 255 255 / 50%);
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
}
@@ -476,7 +459,7 @@ p.author {
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
-webkit-user-select: none;
user-select: none;
@include themed {
@@ -490,7 +473,7 @@ p.author {
&:hover {
@include themed {
background-color: rgb(255 255 255 / 15%);
background-color: rgba(255, 255, 255, 0.15);
color: t($color);
}
}
@@ -509,7 +492,7 @@ p.author {
}
&:focus-visible {
outline: 2px solid rgb(255 255 255 / 50%);
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
}
@@ -552,7 +535,7 @@ p.author {
.card-chips {
margin-top: 0 !important;
margin-inline-start: auto;
margin-left: auto;
}
}
}

View File

@@ -1,5 +1,6 @@
@use 'scss/variables' as *;
// Default button behavior for all modal buttons
.btn-default {
@include modal-button(standard);
@@ -48,9 +49,7 @@
padding: 10px 20px;
border-radius: 12px !important;
transition:
background 0.2s ease,
transform 0.1s ease;
transition: background 0.2s ease, transform 0.1s ease;
position: relative;
@include themed {
@@ -71,15 +70,15 @@
&:hover {
@include themed {
background: rgb(0 0 0 / 4%) !important;
background: rgba(0, 0, 0, 0.04) !important;
}
.light & {
background: rgb(0 0 0 / 4%) !important;
background: rgba(0, 0, 0, 0.04) !important;
}
.dark & {
background: rgb(255 255 255 / 6%) !important;
background: rgba(255, 255, 255, 0.06) !important;
}
}
@@ -101,7 +100,7 @@
}
.btn-badge {
margin-inline-start: 3px;
margin-left: 3px;
padding: 5px 7px;
border-radius: 6px;
font-size: 0.75em !important;
@@ -118,30 +117,30 @@
}
.light & {
background-color: rgb(0 0 0 / 8%);
color: rgb(0 0 0 / 80%);
background-color: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.8);
}
.dark & {
background-color: rgb(255 255 255 / 12%);
color: rgb(255 255 255 / 90%);
background-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.9);
}
}
&.btn-navigation-active .btn-badge {
@include themed {
background-color: rgb(0 0 0 / 8%);
color: rgb(0 0 0 / 80%);
background-color: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.8);
}
.light & {
background-color: rgb(0 0 0 / 10%);
color: rgb(0 0 0 / 85%);
background-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.85);
}
.dark & {
background-color: rgb(255 255 255 / 15%);
color: rgb(255 255 255 / 100%);
background-color: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 1);
}
}
}
@@ -155,17 +154,17 @@
.btn-navigation-active {
@include themed {
background: rgb(0 0 0 / 6%) !important;
background: rgba(0, 0, 0, 0.06) !important;
box-shadow: none !important;
}
.light & {
background: rgb(0 0 0 / 6%) !important;
background: rgba(0, 0, 0, 0.06) !important;
border: none !important;
}
.dark & {
background: rgb(255 255 255 / 10%) !important;
background: rgba(255, 255, 255, 0.1) !important;
border: none !important;
}
}
@@ -184,11 +183,7 @@
flex-flow: row;
justify-content: center;
gap: 20px;
transition:
background 0.2s ease,
transform 0.1s ease,
border 0.2s ease,
box-shadow 0.2s ease;
transition: background 0.2s ease, transform 0.1s ease, border 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
&:hover {

View File

@@ -1,8 +1,7 @@
@use 'scss/variables' as *;
.modalTabContent {
flex: 1;
min-width: 0;
width: 100% !important;
/* button {
@include modal-button(standard);
@@ -11,14 +10,14 @@
padding: 1rem 2rem 5rem;
display: flex;
flex-direction: column;
width: 100%;
// height: 100%;
overflow-y: auto;
overflow-x: hidden;
margin-bottom: 0;
padding-bottom: 2rem;
@include themed {
background: t($modal-background);
margin: 0;
border-radius: t($borderRadius);
}
@@ -82,7 +81,7 @@
.resetDataButtonsLayout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
grid-gap: 20px;
:nth-child(1) {
grid-column: span 2;
@@ -251,13 +250,7 @@ table {
}
.messageAction {
[dir='ltr'] & {
float: right;
}
[dir='rtl'] & {
float: left;
}
float: right;
}
}
@@ -281,7 +274,7 @@ table {
hr {
height: 100%;
margin-inline-end: 5px;
margin-right: 5px;
@include themed {
border-color: t($modal-secondaryColour);
@@ -296,7 +289,8 @@ table {
transition: 0.5s;
cursor: pointer;
border: 0;
padding-inline: 10px 5px;
padding-left: 10px;
padding-right: 5px;
input[type='tel'],
input[type='number'] {

View File

@@ -6,45 +6,21 @@
margin: 0;
padding: 0.75rem 0.5rem;
background: t($modal-sidebar);
border-start-start-radius: 12px;
border-end-start-radius: 12px;
border-start-end-radius: 0;
border-end-end-radius: 0;
overflow: hidden auto;
border-radius: 12px 0 0 12px;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
min-width: 250px;
flex-shrink: 0;
transition: min-width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
// Container for toggle button positioning
.sidebarHeader {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
justify-content: flex-end;
margin-bottom: 0.5rem;
.search-input-container {
flex: 1;
width: auto;
.search-input-field {
height: 38px;
font-size: 14px;
padding-inline: 42px 14px;
padding-block: 0;
border-radius: 10px;
&::placeholder {
opacity: 0.6;
}
}
.search-input-icon {
inset-inline-start: 16px;
font-size: 18px;
}
}
height: 32px;
align-items: center;
}
svg {
@@ -76,14 +52,12 @@
gap: 12px;
margin: 0.15rem 0.25rem;
padding: 0.65rem 0.75rem;
transition:
background 0.2s ease,
transform 0.1s ease;
transition: background 0.2s ease, transform 0.1s ease;
outline: none;
border: none;
background: none;
width: calc(100% - 0.5rem);
text-align: start;
text-align: left;
position: relative;
&:last-child {
@@ -110,22 +84,10 @@
span {
white-space: nowrap;
max-width: 200px;
overflow: hidden;
transition:
opacity 0.25s ease,
max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
transition: opacity 0.25s ease;
}
}
.sidebarEmptyState {
padding: 2rem 1rem;
text-align: center;
font-size: 14px;
color: t($color);
opacity: 0.6;
}
.tab-list-active {
background: t($modal-sidebarActive);
position: relative;
@@ -134,26 +96,21 @@
color: t($color);
}
// Active indicator line
&::before {
content: '';
position: absolute;
inset-inline-start: 0;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
background: t($color);
[dir='ltr'] & {
border-radius: 0 2px 2px 0;
}
[dir='rtl'] & {
border-radius: 2px 0 0 2px;
}
border-radius: 0 2px 2px 0;
}
}
// Collapsed state
&.collapsed {
min-width: 64px;
padding: 0.75rem 0.25rem;
@@ -169,13 +126,14 @@
span {
opacity: 0;
max-width: 0;
width: 0;
overflow: hidden;
margin: 0;
}
}
.tab-list-active::before {
inset-inline-start: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 3px;
@@ -240,39 +198,10 @@
margin-right: 0 !important;
}
// Sidebar skeleton loader
.sidebarSkeleton {
padding: 0.5rem 0.2rem;
.skeletonHeader {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
.skeletonToggle {
width: 32px;
height: 32px;
border-radius: 8px;
flex-shrink: 0;
@include themed {
background: t($modal-sidebarActive);
}
}
.skeletonSearch {
flex: 1;
height: 38px;
border-radius: 10px;
@include themed {
background: t($modal-sidebarActive);
}
}
}
.skeletonItem {
display: flex;
align-items: center;
@@ -284,7 +213,8 @@
width: 17px;
height: 17px;
border-radius: 6px;
margin-inline: 20px;
margin-left: 20px;
margin-right: 20px;
flex-shrink: 0;
@include themed {

View File

@@ -7,11 +7,10 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
padding: 1.5rem 1.5rem;
// width: 100%;
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
@include themed {
@@ -75,7 +74,6 @@
.breadcrumb-segment {
display: flex;
align-items: center;
// gap: 0.5rem;
}
@@ -83,7 +81,6 @@
@include themed {
color: t($subColor);
}
white-space: nowrap;
transition: opacity 0.2s;
}
@@ -101,7 +98,6 @@
@include themed {
color: t($subColor);
}
opacity: 0.5;
font-size: 1.2rem;
margin: 0 0.25rem;
@@ -111,7 +107,6 @@
@include themed {
color: t($color);
}
font-weight: 600;
}
}

View File

@@ -6,6 +6,48 @@
@use 'modules/tabs/stats' as *;
input {
/* colour picker */
&[type='color'] {
border-radius: 100%;
height: 30px;
width: 30px;
border: none;
outline: none;
appearance: none;
vertical-align: middle;
background: none;
@include themed {
border: t($modal-sidebarActive) 1px solid;
}
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
border-radius: 100%;
}
}
/* firefox fixes for colour picker (using "," didn't work) */
&[type='color']::-moz-color-swatch {
border-radius: 100%;
height: 30px;
width: 30px;
border: none;
outline: none;
appearance: none;
vertical-align: middle;
background: none;
&::-moz-color-swatch {
border: none;
border-radius: 100%;
}
}
/* date picker */
&[type='date'] {
width: 260px;
@@ -53,7 +95,7 @@ h4 {
position: sticky;
top: -20px;
z-index: 90;
padding: 25px 0 15px;
padding: 25px 0 15px 0;
display: flex;
flex-flow: row;
justify-content: space-between;
@@ -169,7 +211,7 @@ h4 {
background: none;
border: none;
padding: 0;
margin-inline-start: 5px;
margin-left: 5px;
cursor: pointer;
text-decoration: underline;
font-size: 14px;
@@ -188,7 +230,7 @@ h4 {
background: none;
border: none;
padding: 0;
margin-inline-start: 5px;
margin-left: 5px;
cursor: pointer;
text-decoration: underline;
font-size: 14px;
@@ -231,7 +273,7 @@ h4 {
transition: 0.4s ease-in-out;
}
/* Warning banner (used in Search settings and potentially others) */
// Warning banner (used in Search settings and potentially others)
.itemWarning {
padding: 10px 20px;
display: flex;

View File

@@ -18,7 +18,7 @@ legend {
}
.MuiFormControlLabel-labelPlacementStart {
margin-inline-start: 0 !important;
margin-left: 0 !important;
}
.MuiSwitch-colorPrimary.Mui-checked + .MuiSwitch-track {
@@ -152,10 +152,11 @@ legend,
.settingsRow {
.MuiFormControlLabel-root {
flex-direction: row-reverse;
margin-inline: 0;
margin-right: 0;
display: flex;
justify-content: space-between;
width: 100%;
margin-left: 0;
}
.MuiFormControlLabel-root {

View File

@@ -45,7 +45,6 @@
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);

View File

@@ -48,7 +48,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
padding: 20px;
gap: 20px;
grid-gap: 20px;
@include themed {
div {
@@ -95,16 +95,17 @@
}
}
// Enhanced custom images grid
.images-grid {
display: grid;
padding: 1px;
// Show all checkboxes when in selection mode (any image selected)
&.selection-mode {
.image-checkbox {
opacity: 1;
}
}
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
@@ -125,7 +126,6 @@
&:hover {
transform: translateY(-4px);
// box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
.image-nav-buttons {
@@ -156,11 +156,11 @@
appearance: none;
border: 2px solid #fff;
border-radius: 4px;
background: rgb(0 0 0 / 60%);
backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.6);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
transition: all 0.2s;
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
&:checked {
@@ -187,6 +187,7 @@
}
}
// Keep checkbox visible when checked
&:has(input:checked) {
opacity: 1;
}
@@ -243,8 +244,8 @@
height: 36px;
border-radius: 50%;
border: none;
background: rgb(0 0 0 / 60%);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.6);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: #fff;
display: flex;
@@ -258,7 +259,7 @@
}
&:hover:not(:disabled) {
background: rgb(0 0 0 / 80%);
background: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
@@ -313,7 +314,7 @@
height: 32px;
border-radius: 50%;
border: none;
background: rgb(255 71 87 / 90%);
background: rgba(255, 71, 87, 0.9);
color: #fff;
display: flex;
align-items: center;
@@ -322,7 +323,7 @@
opacity: 0;
transition: all 0.2s;
z-index: 11;
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
svg {
font-size: 20px;
@@ -334,6 +335,7 @@
}
}
// Show delete button when card is hovered or has checkbox visible
&:hover .delete-button,
.image-checkbox:has(input:checked) ~ * .delete-button {
opacity: 1;
@@ -342,6 +344,7 @@
}
}
// Storage quota display
.storage-quota {
padding: 15px 20px;
margin-top: 10px;
@@ -410,6 +413,7 @@
}
}
// Folder tagging modal styles
.taggingModalContent {
padding: 20px;
@@ -447,7 +451,7 @@
&:focus {
border-color: #ff5c25;
box-shadow: 0 0 0 3px rgb(255 92 37 / 10%);
box-shadow: 0 0 0 3px rgba(255, 92, 37, 0.1);
}
}
}
@@ -501,7 +505,7 @@
.overviewGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
grid-gap: 30px;
}
.tabPreview {

View File

@@ -16,7 +16,7 @@
.statGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
grid-gap: 10px;
div {
display: flex;
@@ -43,7 +43,7 @@
.achievementsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
grid-gap: 10px;
}
.achievement {
@@ -134,6 +134,7 @@
button {
margin-bottom: 15px;
flex-flow: row !important;
padding-inline: 20px;
padding-left: 20px;
padding-right: 20px;
}
}

View File

@@ -1,12 +1,10 @@
import { memo } from 'react';
import { useT } from 'contexts';
import variables from 'config/variables';
import { MdClose, MdRestartAlt } from 'react-icons/md';
import { setDefaultSettings } from 'utils/settings';
import { Tooltip, Button } from 'components/Elements';
function ResetModal({ modalClose }) {
const t = useT();
const reset = () => {
variables.stats.postEvent('setting', 'Reset');
setDefaultSettings('reset');
@@ -17,32 +15,34 @@ function ResetModal({ modalClose }) {
<div className="smallModal">
<div className="shareHeader">
<span className="title">
{t('modals.main.settings.sections.advanced.reset_modal.title')}
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.title')}
</span>
<Tooltip title={t('modals.main.settings.sections.advanced.reset_modal.cancel')}>
<Tooltip
title={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
>
<div className="close" onClick={modalClose}>
<MdClose />
</div>
</Tooltip>
</div>
<span className="title">
{t('modals.main.settings.sections.advanced.reset_modal.question')}
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.question')}
</span>
<span className="subtitle">
{t('modals.main.settings.sections.advanced.reset_modal.information')}
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.information')}
</span>
<div className="resetFooter">
<Button
type="secondary"
onClick={modalClose}
icon={<MdClose />}
label={t('modals.main.settings.sections.advanced.reset_modal.cancel')}
label={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
/>
<Button
type="settings"
onClick={() => reset()}
icon={<MdRestartAlt />}
label={t('modals.main.settings.buttons.reset')}
label={variables.getMessage('modals.main.settings.buttons.reset')}
/>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { memo } from 'react';
import { useT } from 'contexts';
import variables from 'config/variables';
import { MdClose, MdEmail, MdContentCopy } from 'react-icons/md';
import { FaFacebookF } from 'react-icons/fa';
import { AiFillWechat } from 'react-icons/ai';
@@ -12,34 +12,37 @@ import { Button } from 'components/Elements';
import './sharemodal.scss';
function ShareModal({ modalClose, data }) {
const t = useT();
if (data.startsWith('https://cdn.')) {
data = {
url: data,
name: t('modals.share.item_type.image'),
name: 'this image',
};
} else if (data.startsWith('"')) {
data = {
url: data,
name: t('modals.share.item_type.quote'),
name: 'this quote',
};
} else {
data = {
url: data,
name: t('modals.share.item_type.marketplace_item'),
name: 'this marketplace item',
};
}
const copyLink = () => {
navigator.clipboard.writeText(data.url);
toast(data.startsWith('"') ? t('toasts.quote') : t('toasts.link_copied'));
toast(
data.startsWith('"')
? variables.getMessage('toasts.quote')
: variables.getMessage('toasts.link_copied'),
);
};
return (
<div className="smallModal">
<div className="shareHeader">
<span className="title">{t('widgets.quote.share')}</span>
<Tooltip title={t('modals.welcome.buttons.close')}>
<span className="title">{variables.getMessage('widgets.quote.share')}</span>
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')}>
<div className="close" onClick={modalClose}>
<MdClose />
</div>
@@ -50,13 +53,13 @@ function ShareModal({ modalClose, data }) {
onClick={() =>
window
.open(
`https://x.com/intent/tweet?text=${t('modals.share.twitter_message', { name: data.name })}: ${data.url}`,
`https://x.com/intent/tweet?text=Check out ${data.name} on @getmue: ${data.url}`,
'_blank',
)
.focus()
}
icon={<SiX />}
tooltipTitle={t('modals.share.social.twitter')}
tooltipTitle="X (Twitter)"
type="icon"
/>
<Button
@@ -66,20 +69,23 @@ function ShareModal({ modalClose, data }) {
.focus()
}
icon={<FaFacebookF />}
tooltipTitle={t('modals.share.social.facebook')}
tooltipTitle="Facebook"
type="icon"
/>
<Button
onClick={() =>
window
.open(
`mailto:email@example.com?subject=${encodeURIComponent(t('modals.share.email_subject'))}&body=${encodeURIComponent(t('modals.share.email_body', { name: data.name, url: data.url }))}`,
'mailto:email@example.com?subject=Check%20out%20this%20%on%20%Mue!&body=' +
data.name +
'on Mue: ' +
data.url,
'_blank',
)
.focus()
}
icon={<MdEmail />}
tooltipTitle={t('modals.share.social.email')}
tooltipTitle="Email"
type="icon"
/>
<Button
@@ -92,7 +98,7 @@ function ShareModal({ modalClose, data }) {
.focus()
}
icon={<AiFillWechat />}
tooltipTitle={t('modals.share.social.wechat')}
tooltipTitle="WeChat"
type="icon"
/>
<Button
@@ -102,7 +108,7 @@ function ShareModal({ modalClose, data }) {
.focus()
}
icon={<SiTencentqq />}
tooltipTitle={t('modals.share.social.qq')}
tooltipTitle="Tencent QQ"
type="icon"
/>
</div>
@@ -111,7 +117,7 @@ function ShareModal({ modalClose, data }) {
<Button
onClick={() => copyLink()}
icon={<MdContentCopy />}
tooltipTitle={t('modals.share.copy_link')}
tooltipTitle={variables.getMessage('modals.share.copy_link')}
type="icon"
/>
</div>
@@ -121,4 +127,4 @@ function ShareModal({ modalClose, data }) {
const MemoizedSharemodal = memo(ShareModal);
export { MemoizedSharemodal as default, MemoizedSharemodal as ShareModal };
export { MemoizedSharemodal as default, MemoizedSharemodal as ShareModal };

View File

@@ -1,83 +0,0 @@
import { useState, useEffect } from 'react';
import { IconService } from 'utils/quicklinks';
import './smarticon.scss';
export const SmartIcon = ({ item, size = 32, fallbackChain, className = '' }) => {
const [currentIconIndex, setCurrentIconIndex] = useState(0);
const [iconUrls, setIconUrls] = useState([]);
const [hasError, setHasError] = useState(false);
const [showPlaceholder, setShowPlaceholder] = useState(false);
useEffect(() => {
const urls = IconService.getIconUrl(item, fallbackChain);
setIconUrls(Array.isArray(urls) ? urls : [urls]);
setCurrentIconIndex(0);
setHasError(false);
setShowPlaceholder(false);
}, [item, fallbackChain]);
const handleImageError = () => {
if (currentIconIndex < iconUrls.length - 1) {
setCurrentIconIndex((prev) => prev + 1);
setHasError(false);
} else {
setHasError(true);
setShowPlaceholder(true);
}
};
if (item.iconType === 'emoji' && item.icon) {
return (
<div className={`smart-icon emoji ${className}`} style={{ fontSize: size }}>
{item.icon}
</div>
);
}
if (item.iconType === 'letter' || showPlaceholder) {
const avatar = IconService.generateLetterAvatar(item.name);
return (
<div
className={`smart-icon letter-avatar ${className}`}
style={{
width: size,
height: size,
backgroundColor: avatar.backgroundColor,
fontSize: size * 0.5,
}}
>
{avatar.letter}
</div>
);
}
const currentUrl = iconUrls[currentIconIndex];
if (!currentUrl) {
const avatar = IconService.generateLetterAvatar(item.name);
return (
<div
className={`smart-icon letter-avatar ${className}`}
style={{
width: size,
height: size,
backgroundColor: avatar.backgroundColor,
fontSize: size * 0.5,
}}
>
{avatar.letter}
</div>
);
}
return (
<img
src={currentUrl}
alt={item.name}
className={`smart-icon ${className}`}
style={{ width: size, height: size }}
onError={handleImageError}
draggable={false}
loading="lazy"
/>
);
};

View File

@@ -1 +0,0 @@
export { SmartIcon } from './SmartIcon';

View File

@@ -1,26 +0,0 @@
.smart-icon {
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
flex-shrink: 0;
&.emoji {
line-height: 1;
}
&.letter-avatar {
border-radius: 50%;
color: white;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: center;
}
img {
object-fit: contain;
}
}

View File

@@ -1,12 +1,12 @@
import { useState, memo, useRef, useId } from 'react';
import { useState, memo, useRef } from 'react';
import { useFloating, flip, offset, shift } from '@floating-ui/react-dom';
import './tooltip.scss';
function Tooltip({ children, title, style, placement, subtitle }) {
const [showTooltip, setShowTooltip] = useState(false);
const [closing, setClosing] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [reference, setReference] = useState(null);
const tooltipId = useId();
const tooltipId = useRef(`tooltip-${Math.random()}`);
const closeTimeout = useRef(null);
const {
@@ -23,23 +23,23 @@ function Tooltip({ children, title, style, placement, subtitle }) {
},
});
const { setFloating } = refs;
const handleMouseEnter = () => {
// Clear any pending close timeout if mouse re-enters during exit
if (closeTimeout.current) {
clearTimeout(closeTimeout.current);
closeTimeout.current = null;
}
setClosing(false);
setIsClosing(false);
setShowTooltip(true);
};
const handleMouseLeave = () => {
setClosing(true);
setIsClosing(true);
// Wait for exit animation to complete before unmounting
closeTimeout.current = setTimeout(() => {
setShowTooltip(false);
setClosing(false);
}, 200);
setIsClosing(false);
}, 200); // Match exit animation duration
};
const handleFocus = () => {
@@ -47,25 +47,22 @@ function Tooltip({ children, title, style, placement, subtitle }) {
clearTimeout(closeTimeout.current);
closeTimeout.current = null;
}
setClosing(false);
setIsClosing(false);
setShowTooltip(true);
};
const handleBlur = () => {
setClosing(true);
setIsClosing(true);
closeTimeout.current = setTimeout(() => {
setShowTooltip(false);
setClosing(false);
setIsClosing(false);
}, 200);
};
// Determine the data-status attribute value
const getStatus = () => {
if (!showTooltip && !closing) {
return 'initial';
}
if (closing) {
return 'close';
}
if (!showTooltip && !isClosing) return 'initial';
if (isClosing) return 'close';
return 'open';
};
@@ -79,13 +76,13 @@ function Tooltip({ children, title, style, placement, subtitle }) {
onFocus={handleFocus}
onBlur={handleBlur}
ref={setReference}
aria-describedby={tooltipId}
aria-describedby={tooltipId.current}
>
{children}
</div>
{(showTooltip || closing) && (
{(showTooltip || isClosing) && (
<span
ref={setFloating}
ref={refs.setFloating}
style={{
position: strategy,
top: y ?? '',

View File

@@ -5,54 +5,6 @@
display: grid;
}
@keyframes floatingFromTop {
0% {
transform: translateY(5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes floatingFromBottom {
0% {
transform: translateY(-5px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes floatingFromLeft {
0% {
transform: translateX(5px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes floatingFromRight {
0% {
transform: translateX(-5px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes floating {
0% {
transform: translate(0, -5px);
@@ -85,36 +37,21 @@
opacity 0.2s ease-out,
transform 0.2s ease-out;
/* Initial state (not yet shown) */
// Initial state (not yet shown)
&[data-status='initial'] {
opacity: 0;
}
/* Open state - entrance animation */
// Open state - entrance animation
&[data-status='open'] {
opacity: 1;
transform: translate(0, 0);
animation-name: floating;
animation-duration: 0.3s;
animation-timing-function: ease-in;
&[data-placement^='top'] {
animation-name: floatingFromTop;
}
&[data-placement^='bottom'] {
animation-name: floatingFromBottom;
}
&[data-placement^='left'] {
animation-name: floatingFromLeft;
}
&[data-placement^='right'] {
animation-name: floatingFromRight;
}
}
/* Closing state - exit animation */
// Closing state - exit animation
&[data-status='close'] {
opacity: 0;

View File

@@ -18,28 +18,27 @@ const Checkbox = memo((props) => {
props.onChange(value);
}
variables.stats.postEvent('setting', `${props.name} ${value ? 'enabled' : 'disabled'}`);
variables.stats.postEvent(
'setting',
`${props.name} ${value ? 'enabled' : 'disabled'}`,
);
if (props.element) {
if (!document.querySelector(props.element)) {
localStorage.setItem('showReminder', 'true');
EventBus.emit('showReminder');
return;
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
}
}
EventBus.emit('refresh', props.category);
}, [checked, props]);
const handleKeyDown = useCallback(
(e) => {
if ((e.key === ' ' || e.key === 'Enter') && !props.disabled) {
e.preventDefault();
handleChange();
}
},
[handleChange, props.disabled],
);
const handleKeyDown = useCallback((e) => {
if ((e.key === ' ' || e.key === 'Enter') && !props.disabled) {
e.preventDefault();
handleChange();
}
}, [handleChange, props.disabled]);
return (
<div className={`checkbox-wrapper ${props.disabled ? 'disabled' : ''}`}>
@@ -54,7 +53,9 @@ const Checkbox = memo((props) => {
aria-label={props.text}
onKeyDown={handleKeyDown}
/>
<div className={`checkbox-box ${checked ? 'checked' : ''}`}>{checked && <MdCheck />}</div>
<div className={`checkbox-box ${checked ? 'checked' : ''}`}>
{checked && <MdCheck />}
</div>
</div>
);
});

View File

@@ -41,7 +41,6 @@
flex: 1;
transition: color 0.2s ease;
pointer-events: none;
font-size: 1rem;
@include themed {
color: t($color);
@@ -75,7 +74,7 @@
transform: scale(0.95);
@include themed {
box-shadow: 0 0 0 4px rgb(255 92 37 / 10%);
box-shadow: 0 0 0 4px rgba(255, 92, 37, 0.1);
}
}
@@ -91,7 +90,7 @@
}
svg {
font-size: 1.125rem;
font-size: 18px;
color: white;
}
}

View File

@@ -4,51 +4,30 @@ import { MdExpandMore, MdClose, MdCheck } from 'react-icons/md';
import './ChipSelect.scss';
function ChipSelect({ label, options, onChange, name }) {
const storageKey = name || 'apiCategories';
let start = (localStorage.getItem(storageKey) || '').split(',');
function ChipSelect({ label, options, onChange }) {
let start = (localStorage.getItem('apiCategories') || '').split(',');
if (start[0] === '') {
start = [];
}
const [optionsSelected, setOptionsSelected] = useState(start);
const [open, setOpen] = useState(false);
const [closing, setClosing] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 });
const containerRef = useRef(null);
const controlRef = useRef(null);
const menuRef = useRef(null);
const closeDropdown = useCallback(() => {
setClosing(true);
setIsClosing(true);
setTimeout(() => {
setOpen(false);
setClosing(false);
}, 200);
setIsOpen(false);
setIsClosing(false);
}, 200); // Match animation duration
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
// Ignore clicks on color inputs to prevent closing when native color picker opens
if (event.target.type === 'color') {
return;
}
// Also ignore clicks on color input labels and wrappers
const target = event.target;
if (target.tagName === 'LABEL' && target.htmlFor) {
const associatedInput = document.getElementById(target.htmlFor) || document.querySelector(`input[name="${target.htmlFor}"]`);
if (associatedInput && associatedInput.type === 'color') {
return;
}
}
// Check if clicking within a color input wrapper
const colorInputWrapper = target.closest('.colour-picker');
if (colorInputWrapper && colorInputWrapper.querySelector('input[type="color"]')) {
return;
}
if (
containerRef.current &&
!containerRef.current.contains(event.target) &&
@@ -69,11 +48,14 @@ function ChipSelect({ label, options, onChange, name }) {
const gap = 4;
const viewportHeight = window.innerHeight;
// Estimate menu height
const estimatedMenuHeight = Math.min(options.length * 44, 250);
// Calculate if dropdown would overflow bottom of viewport
const spaceBelow = viewportHeight - rect.bottom - gap;
const spaceAbove = rect.top - gap;
// If not enough space below but more space above, flip to top
const shouldFlipUp = spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow;
return {
@@ -90,7 +72,7 @@ function ChipSelect({ label, options, onChange, name }) {
const openDropdown = useCallback(() => {
const position = calculatePosition();
setMenuPosition(position);
setOpen(true);
setIsOpen(true);
}, [calculatePosition]);
const handleToggle = (optionName) => {
@@ -102,8 +84,7 @@ function ChipSelect({ label, options, onChange, name }) {
}
setOptionsSelected(newSelected);
const storageKey = name || 'apiCategories';
localStorage.setItem(storageKey, newSelected.join(','));
localStorage.setItem('apiCategories', newSelected.join(','));
if (onChange) {
onChange(newSelected);
@@ -122,7 +103,7 @@ function ChipSelect({ label, options, onChange, name }) {
ref={controlRef}
className="chipSelect-control"
onClick={() => {
if (open) {
if (isOpen) {
closeDropdown();
} else {
openDropdown();
@@ -149,13 +130,13 @@ function ChipSelect({ label, options, onChange, name }) {
</div>
)}
</div>
<MdExpandMore className={`chipSelect-arrow ${open ? 'open' : ''}`} />
<MdExpandMore className={`chipSelect-arrow ${isOpen ? 'open' : ''}`} />
</div>
{(open || closing) &&
{(isOpen || isClosing) &&
createPortal(
<div
ref={menuRef}
className={`chipSelect-dropdown ${closing ? 'closing' : ''} ${menuPosition.flipped ? 'flipped' : ''}`}
className={`chipSelect-dropdown ${isClosing ? 'closing' : ''} ${menuPosition.flipped ? 'flipped' : ''}`}
style={{
position: 'fixed',
top: `${menuPosition.top}px`,

View File

@@ -58,7 +58,7 @@
flex-flow: column;
.chipSelect-label {
font-size: 0.75rem;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -113,7 +113,7 @@
align-items: center;
gap: 6px;
padding: 6px 10px;
font-size: 0.8125rem;
font-size: 13px;
text-transform: capitalize;
transition: all 0.15s ease;
@@ -147,14 +147,14 @@
}
svg {
font-size: 0.75rem;
font-size: 12px;
}
}
}
.chipSelect-arrow {
flex-shrink: 0;
font-size: 1.5rem;
font-size: 24px;
transition: transform 0.2s ease;
@include themed {
@@ -254,7 +254,7 @@
}
svg {
font-size: 0.875rem;
font-size: 14px;
}
}

View File

@@ -1,98 +0,0 @@
import { useT } from 'contexts';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { MdRefresh } from 'react-icons/md';
import { toast } from 'react-toastify';
import EventBus from 'utils/eventbus';
import './ColourPicker.scss';
const ColourPicker = memo(
({ name, label, category, defaultValue = '#ffffff', value: controlledValue, onChange }) => {
const t = useT();
const inputRef = useRef(null);
const debounceTimerRef = useRef(null);
const [internalValue, setInternalValue] = useState(
() => localStorage.getItem(name) || defaultValue,
);
const value = controlledValue !== undefined ? controlledValue : internalValue;
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, []);
const handleChange = useCallback(
(e) => {
const colour = e.target.value;
if (controlledValue === undefined) {
setInternalValue(colour);
localStorage.setItem(name, colour);
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
EventBus.emit('refresh', category);
}, 500);
}
if (onChange) {
onChange(e);
}
},
[name, category, onChange, controlledValue],
);
const resetColour = useCallback(
(e) => {
e?.stopPropagation();
const resetEvent = { target: { value: defaultValue } };
handleChange(resetEvent);
toast(t('toasts.reset'));
},
[handleChange, defaultValue, t],
);
const openPicker = useCallback((e) => {
e.stopPropagation();
inputRef.current?.click();
}, []);
return (
<div className="colour-picker">
{label && (
<div className="colour-picker-header">
<label className="colour-picker-label">{label}</label>
<span className="colour-picker-reset" onClick={resetColour}>
<MdRefresh />
{t('modals.main.settings.buttons.reset')}
</span>
</div>
)}
<div className="colour-picker-control" onClick={openPicker}>
<div className="colour-picker-swatch" style={{ backgroundColor: value }} />
<span className="colour-picker-value">{value}</span>
<input
ref={inputRef}
type="color"
name={name}
value={value}
onChange={handleChange}
className="colour-picker-input"
/>
</div>
</div>
);
},
);
ColourPicker.displayName = 'ColourPicker';
export { ColourPicker as default, ColourPicker };

View File

@@ -1,101 +0,0 @@
@use 'scss/variables' as *;
.colour-picker {
display: flex;
flex-direction: column;
width: 300px;
gap: 8px;
.colour-picker-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.colour-picker-label {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@include themed {
color: t($subColor);
}
}
.colour-picker-reset {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@include themed {
color: t($link);
}
&:hover {
opacity: 0.8;
}
svg {
font-size: 0.75rem;
}
}
.colour-picker-control {
position: relative;
display: flex;
align-items: center;
gap: 12px;
height: 56px;
padding: 0 16px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover {
border-color: t($color);
}
}
}
.colour-picker-swatch {
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
transition: box-shadow 0.2s ease;
@include themed {
box-shadow: 0 0 0 1px t($modal-sidebarActive);
}
}
.colour-picker-value {
flex: 1;
font-size: 1rem;
font-family: inherit;
}
.colour-picker-input {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: -1;
}
}

View File

@@ -1 +0,0 @@
export * from './ColourPicker';

View File

@@ -5,8 +5,8 @@ import { MdExpandMore, MdChevronLeft, MdChevronRight } from 'react-icons/md';
import './DatePicker.scss';
const DatePicker = memo((props) => {
const [open, setOpen] = useState(false);
const [closing, setClosing] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 });
const [viewDate, setViewDate] = useState(props.value || new Date());
const containerRef = useRef(null);
@@ -14,35 +14,15 @@ const DatePicker = memo((props) => {
const menuRef = useRef(null);
const closeDropdown = useCallback(() => {
setClosing(true);
setIsClosing(true);
setTimeout(() => {
setOpen(false);
setClosing(false);
setIsOpen(false);
setIsClosing(false);
}, 200);
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
// Ignore clicks on color inputs to prevent closing when native color picker opens
if (event.target.type === 'color') {
return;
}
// Also ignore clicks on color input labels and wrappers
const target = event.target;
if (target.tagName === 'LABEL' && target.htmlFor) {
const associatedInput = document.getElementById(target.htmlFor) || document.querySelector(`input[name="${target.htmlFor}"]`);
if (associatedInput && associatedInput.type === 'color') {
return;
}
}
// Check if clicking within a color input wrapper
const colorInputWrapper = target.closest('.colour-picker');
if (colorInputWrapper && colorInputWrapper.querySelector('input[type="color"]')) {
return;
}
if (
containerRef.current &&
!containerRef.current.contains(event.target) &&
@@ -81,7 +61,7 @@ const DatePicker = memo((props) => {
const openDropdown = useCallback(() => {
const position = calculatePosition();
setMenuPosition(position);
setOpen(true);
setIsOpen(true);
}, [calculatePosition]);
const formatDate = (date) => {
@@ -117,18 +97,8 @@ const DatePicker = memo((props) => {
};
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const renderCalendar = () => {
@@ -138,10 +108,12 @@ const DatePicker = memo((props) => {
const today = new Date();
const selectedDate = props.value;
// Empty cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="calendar-day empty" />);
}
// Days of the month
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
const isToday = date.toDateString() === today.toDateString();
@@ -154,7 +126,7 @@ const DatePicker = memo((props) => {
onClick={() => handleDateSelect(day)}
>
{day}
</div>,
</div>
);
}
@@ -167,7 +139,7 @@ const DatePicker = memo((props) => {
ref={controlRef}
className="datepicker-control"
onClick={() => {
if (open) {
if (isOpen) {
closeDropdown();
} else {
openDropdown();
@@ -175,13 +147,13 @@ const DatePicker = memo((props) => {
}}
>
<span className="datepicker-value">{formatDate(props.value)}</span>
<MdExpandMore className={`datepicker-arrow ${open ? 'open' : ''}`} />
<MdExpandMore className={`datepicker-arrow ${isOpen ? 'open' : ''}`} />
</div>
{(open || closing) &&
{(isOpen || isClosing) &&
createPortal(
<div
ref={menuRef}
className={`datepicker-menu ${closing ? 'closing' : ''} ${menuPosition.flipped ? 'flipped' : ''}`}
className={`datepicker-menu ${isClosing ? 'closing' : ''} ${menuPosition.flipped ? 'flipped' : ''}`}
style={{
position: 'fixed',
top: `${menuPosition.top}px`,
@@ -194,8 +166,7 @@ const DatePicker = memo((props) => {
<MdChevronLeft />
</button>
<span className="calendar-month">
{monthNames[viewDate.getMonth()]}
{props.hideYear ? '' : ` ${viewDate.getFullYear()}`}
{monthNames[viewDate.getMonth()]}{props.hideYear ? '' : ` ${viewDate.getFullYear()}`}
</span>
<button onClick={handleNextMonth} className="calendar-nav">
<MdChevronRight />
@@ -210,7 +181,9 @@ const DatePicker = memo((props) => {
<div>Fr</div>
<div>Sa</div>
</div>
<div className="calendar-grid">{renderCalendar()}</div>
<div className="calendar-grid">
{renderCalendar()}
</div>
</div>,
document.body,
)}

View File

@@ -97,7 +97,7 @@
.datepicker-arrow {
flex-shrink: 0;
font-size: 1.5rem;
font-size: 24px;
transition: all 0.2s ease;
cursor: pointer;
padding: 4px;
@@ -121,9 +121,7 @@
.datepicker-menu {
z-index: 9999;
@include animation(datepickerSlideIn 0.2s ease-out);
will-change: transform, opacity;
padding: 16px;
min-width: 280px;
@@ -133,7 +131,7 @@
background: t($modal-background);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.flipped {
@@ -177,13 +175,13 @@
}
svg {
font-size: 1.25rem;
font-size: 20px;
}
}
.calendar-month {
font-weight: 600;
font-size: 0.875rem;
font-size: 14px;
flex: 1;
text-align: center;
@@ -201,7 +199,7 @@
div {
text-align: center;
font-size: 0.75rem;
font-size: 12px;
font-weight: 500;
padding: 8px 4px;
@@ -223,7 +221,7 @@
justify-content: center;
cursor: pointer;
border-radius: 50%;
font-size: 0.875rem;
font-size: 14px;
transition: all 0.15s ease;
min-width: 32px;
min-height: 32px;

Some files were not shown because too many files have changed in this diff Show More