mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-02 19:36:12 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			fix/mkdocs
			...
			fix/resolv
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c0a55fec60 | 
@@ -1,6 +1,6 @@
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*.{js,ts,.tsx}]
 | 
			
		||||
[*.{js,ts}]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
indent_size = 4
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							@@ -2,5 +2,3 @@
 | 
			
		||||
 | 
			
		||||
github: [eliandoran]
 | 
			
		||||
custom: ["https://paypal.me/eliandoran"]
 | 
			
		||||
liberapay: ElianDoran
 | 
			
		||||
buy_me_a_coffee: eliandoran
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -86,7 +86,7 @@ runs:
 | 
			
		||||
      APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
 | 
			
		||||
      WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
 | 
			
		||||
      TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
 | 
			
		||||
    run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
 | 
			
		||||
    run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
 | 
			
		||||
 | 
			
		||||
  # Add DMG signing step
 | 
			
		||||
  - name: Sign DMG
 | 
			
		||||
@@ -162,25 +162,3 @@ runs:
 | 
			
		||||
        echo "Found ZIP: $zip_file"
 | 
			
		||||
        echo "Note: ZIP files are not code signed, but their contents should be"
 | 
			
		||||
      fi
 | 
			
		||||
 | 
			
		||||
  - name: Sign the RPM
 | 
			
		||||
    if: inputs.os == 'linux'
 | 
			
		||||
    shell: ${{ inputs.shell }}
 | 
			
		||||
    run: |
 | 
			
		||||
      echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
 | 
			
		||||
 | 
			
		||||
      # Import the key into RPM for verification
 | 
			
		||||
      gpg --export -a > pubkey
 | 
			
		||||
      rpm --import pubkey
 | 
			
		||||
      rm pubkey
 | 
			
		||||
 | 
			
		||||
      # Sign the RPM
 | 
			
		||||
      rpm_file=$(find ./apps/desktop/upload -name "*.rpm" -print -quit)
 | 
			
		||||
      rpmsign --define "_gpg_name Trilium Notes Signing Key <triliumnotes@outlook.com>" --addsign "$rpm_file"
 | 
			
		||||
      rpm -Kv "$rpm_file"
 | 
			
		||||
 | 
			
		||||
      # Validate code signing
 | 
			
		||||
      if ! rpm -K "$rpm_file" | grep -q "digests signatures OK"; then
 | 
			
		||||
        echo .rpm file not signed
 | 
			
		||||
        exit 1
 | 
			
		||||
      fi
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,7 +10,7 @@ runs:
 | 
			
		||||
  steps:
 | 
			
		||||
  - uses: pnpm/action-setup@v4
 | 
			
		||||
  - name: Set up node & dependencies
 | 
			
		||||
    uses: actions/setup-node@v5
 | 
			
		||||
    uses: actions/setup-node@v4
 | 
			
		||||
    with:
 | 
			
		||||
      node-version: 22
 | 
			
		||||
      cache: "pnpm"
 | 
			
		||||
@@ -23,7 +23,7 @@ runs:
 | 
			
		||||
    shell: bash
 | 
			
		||||
    run: |
 | 
			
		||||
      pnpm run chore:update-build-info
 | 
			
		||||
      pnpm run --filter server package
 | 
			
		||||
      pnpm nx --project=server package
 | 
			
		||||
  - name: Prepare artifacts
 | 
			
		||||
    shell: bash
 | 
			
		||||
    run: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/actions/report-size/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/report-size/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -44,7 +44,7 @@ runs:
 | 
			
		||||
  steps:
 | 
			
		||||
    # Checkout branch to compare to [required]
 | 
			
		||||
    - name: Checkout base branch
 | 
			
		||||
      uses: actions/checkout@v5
 | 
			
		||||
      uses: actions/checkout@v4
 | 
			
		||||
      with:
 | 
			
		||||
        ref: ${{ inputs.branch }}
 | 
			
		||||
        path: br-base
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/checks.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/checks.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,18 +0,0 @@
 | 
			
		||||
name: Checks
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
  pull_request_target:
 | 
			
		||||
    types: [synchronize]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  main:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check if PRs have conflicts
 | 
			
		||||
        uses: eps1lon/actions-label-merge-conflict@v3
 | 
			
		||||
        if: github.repository == ${{ vars.REPO_MAIN }}
 | 
			
		||||
        with:
 | 
			
		||||
          dirtyLabel: "merge-conflicts"
 | 
			
		||||
          repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							@@ -57,7 +57,7 @@ jobs:
 | 
			
		||||
        # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout repository
 | 
			
		||||
      uses: actions/checkout@v5
 | 
			
		||||
      uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
    # Add any setup steps before running the `github/codeql-action/init` action.
 | 
			
		||||
    # This includes steps like installing compilers or runtimes (`actions/setup-node`
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										190
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										190
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,190 +0,0 @@
 | 
			
		||||
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
 | 
			
		||||
# This workflow builds and deploys your MkDocs site when changes are pushed to main
 | 
			
		||||
name: Deploy MkDocs Documentation
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  # Trigger on push to main branch
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
      - master  # Also support master branch
 | 
			
		||||
    # Only run when docs files change
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
      - 'validate-docs-links.ts'
 | 
			
		||||
  
 | 
			
		||||
  # Allow manual triggering from Actions tab
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  
 | 
			
		||||
  # Run on pull requests for preview deployments
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
      - master
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
      - 'validate-docs-links.ts'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-and-deploy:
 | 
			
		||||
    name: Build and Deploy MkDocs
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
    
 | 
			
		||||
    # Required permissions for deployment
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      deployments: write
 | 
			
		||||
      pull-requests: write # For PR preview comments
 | 
			
		||||
      id-token: write # For OIDC authentication (if needed)
 | 
			
		||||
    
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
 | 
			
		||||
      
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v6
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.13'
 | 
			
		||||
          cache: 'pip'
 | 
			
		||||
          cache-dependency-path: 'requirements-docs.txt'
 | 
			
		||||
      
 | 
			
		||||
      - name: Install MkDocs and Dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          pip install --upgrade pip
 | 
			
		||||
          pip install -r requirements-docs.txt
 | 
			
		||||
        env:
 | 
			
		||||
          PIP_DISABLE_PIP_VERSION_CHECK: 1
 | 
			
		||||
      
 | 
			
		||||
      # Setup pnpm before fixing docs structure
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      
 | 
			
		||||
      # Setup Node.js with pnpm
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '22'
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      
 | 
			
		||||
      # Install Node.js dependencies for the TypeScript script
 | 
			
		||||
      - name: Install Dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          pnpm install --frozen-lockfile
 | 
			
		||||
      
 | 
			
		||||
      - name: Fix Documentation Structure
 | 
			
		||||
        run: |
 | 
			
		||||
          # Fix duplicate navigation entries by moving overview pages to index.md
 | 
			
		||||
          pnpm run chore:fix-mkdocs-structure
 | 
			
		||||
      
 | 
			
		||||
      - name: Build MkDocs Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Build with strict mode but allow expected warnings
 | 
			
		||||
          mkdocs build --verbose || {
 | 
			
		||||
            EXIT_CODE=$?
 | 
			
		||||
            # Check if the only issue is expected warnings
 | 
			
		||||
            if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
 | 
			
		||||
               [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
 | 
			
		||||
              echo "✅ Build succeeded with expected warnings"
 | 
			
		||||
              mkdocs build --verbose
 | 
			
		||||
            else
 | 
			
		||||
              echo "❌ Build failed with unexpected errors"
 | 
			
		||||
              exit $EXIT_CODE
 | 
			
		||||
            fi
 | 
			
		||||
          }
 | 
			
		||||
      
 | 
			
		||||
      - name: Validate Built Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Basic validation that important files exist
 | 
			
		||||
          test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
 | 
			
		||||
          test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
 | 
			
		||||
          test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
 | 
			
		||||
          echo "✅ Site validation passed"
 | 
			
		||||
      
 | 
			
		||||
      - name: Validate Documentation Links
 | 
			
		||||
        run: |
 | 
			
		||||
          # Run the TypeScript link validation script
 | 
			
		||||
          pnpm tsx validate-docs-links.ts
 | 
			
		||||
      
 | 
			
		||||
      # Install wrangler globally to avoid workspace issues
 | 
			
		||||
      - name: Install Wrangler
 | 
			
		||||
        run: |
 | 
			
		||||
          npm install -g wrangler
 | 
			
		||||
      
 | 
			
		||||
      # Deploy using Wrangler (use pre-installed wrangler)
 | 
			
		||||
      - name: Deploy to Cloudflare Pages
 | 
			
		||||
        id: deploy
 | 
			
		||||
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
 | 
			
		||||
        uses: cloudflare/wrangler-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
 | 
			
		||||
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
 | 
			
		||||
          command: pages deploy site --project-name=trilium-docs --branch=${{ github.ref_name }}
 | 
			
		||||
          wranglerVersion: ''  # Use pre-installed version
 | 
			
		||||
      
 | 
			
		||||
      # Deploy preview for PRs
 | 
			
		||||
      - name: Deploy Preview to Cloudflare Pages
 | 
			
		||||
        id: preview-deployment
 | 
			
		||||
        if: github.event_name == 'pull_request'
 | 
			
		||||
        uses: cloudflare/wrangler-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
 | 
			
		||||
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
 | 
			
		||||
          command: pages deploy site --project-name=trilium-docs --branch=pr-${{ github.event.pull_request.number }}
 | 
			
		||||
          wranglerVersion: ''  # Use pre-installed version
 | 
			
		||||
      
 | 
			
		||||
      # Post deployment URL as PR comment
 | 
			
		||||
      - name: Comment PR with Preview URL
 | 
			
		||||
        if: github.event_name == 'pull_request'
 | 
			
		||||
        uses: actions/github-script@v8
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
            const prNumber = context.issue.number;
 | 
			
		||||
            // Construct preview URL based on Cloudflare Pages pattern
 | 
			
		||||
            const previewUrl = `https://pr-${prNumber}.trilium-docs.pages.dev`;
 | 
			
		||||
            const mainUrl = 'https://docs.triliumnotes.org';
 | 
			
		||||
            
 | 
			
		||||
            // Check if we already commented
 | 
			
		||||
            const comments = await github.rest.issues.listComments({
 | 
			
		||||
              owner: context.repo.owner,
 | 
			
		||||
              repo: context.repo.repo,
 | 
			
		||||
              issue_number: prNumber
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            const botComment = comments.data.find(comment => 
 | 
			
		||||
              comment.user.type === 'Bot' && 
 | 
			
		||||
              comment.body.includes('Documentation preview is ready')
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            const commentBody = `📚 Documentation preview is ready!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`;
 | 
			
		||||
            
 | 
			
		||||
            if (botComment) {
 | 
			
		||||
              // Update existing comment
 | 
			
		||||
              await github.rest.issues.updateComment({
 | 
			
		||||
                owner: context.repo.owner,
 | 
			
		||||
                repo: context.repo.repo,
 | 
			
		||||
                comment_id: botComment.id,
 | 
			
		||||
                body: commentBody
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              // Create new comment
 | 
			
		||||
              await github.rest.issues.createComment({
 | 
			
		||||
                issue_number: prNumber,
 | 
			
		||||
                owner: context.repo.owner,
 | 
			
		||||
                repo: context.repo.repo,
 | 
			
		||||
                body: commentBody
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
							
								
								
									
										47
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							@@ -19,24 +19,45 @@ permissions:
 | 
			
		||||
  pull-requests: write  # for PR comments
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test_dev:
 | 
			
		||||
    name: Test development
 | 
			
		||||
  check-affected:
 | 
			
		||||
    name: Check affected jobs (NX)
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0  # needed for https://github.com/marketplace/actions/nx-set-shas
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
      - uses: nrwl/nx-set-shas@v4
 | 
			
		||||
      - name: Check affected
 | 
			
		||||
        run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build
 | 
			
		||||
 | 
			
		||||
  test_dev:
 | 
			
		||||
    name: Test development
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - check-affected
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: "pnpm"
 | 
			
		||||
      - run: pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
      - name: Typecheck
 | 
			
		||||
        run: pnpm typecheck
 | 
			
		||||
 | 
			
		||||
      - name: Run the unit tests
 | 
			
		||||
        run: pnpm run test:all
 | 
			
		||||
 | 
			
		||||
@@ -45,15 +66,16 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - test_dev
 | 
			
		||||
      - check-affected
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
      - name: Update build info
 | 
			
		||||
        run: pnpm run chore:update-build-info
 | 
			
		||||
      - name: Trigger client build
 | 
			
		||||
        run: pnpm client:build
 | 
			
		||||
        run: pnpm nx run client:build
 | 
			
		||||
      - name: Send client bundle stats to RelativeCI
 | 
			
		||||
        if: false
 | 
			
		||||
        uses: relative-ci/agent-action@v3
 | 
			
		||||
@@ -61,7 +83,7 @@ jobs:
 | 
			
		||||
          webpackStatsFile: ./apps/client/dist/webpack-stats.json
 | 
			
		||||
          key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
 | 
			
		||||
      - name: Trigger server build
 | 
			
		||||
        run: pnpm run server:build
 | 
			
		||||
        run: pnpm nx run server:build
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
@@ -73,6 +95,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - build_docker
 | 
			
		||||
      - check-affected
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
@@ -80,7 +103,7 @@ jobs:
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
@@ -89,7 +112,7 @@ jobs:
 | 
			
		||||
      - name: Update build info
 | 
			
		||||
        run: pnpm run chore:update-build-info
 | 
			
		||||
      - name: Trigger build
 | 
			
		||||
        run: pnpm server:build
 | 
			
		||||
        run: pnpm nx run server:build
 | 
			
		||||
 | 
			
		||||
      - name: Set IMAGE_NAME to lowercase
 | 
			
		||||
        run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -32,7 +32,7 @@ jobs:
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Set IMAGE_NAME to lowercase
 | 
			
		||||
        run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
 | 
			
		||||
@@ -44,7 +44,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: "pnpm"
 | 
			
		||||
@@ -82,7 +82,7 @@ jobs:
 | 
			
		||||
          require-healthy: true
 | 
			
		||||
 | 
			
		||||
      - name: Run Playwright tests
 | 
			
		||||
        run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e
 | 
			
		||||
        run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
 | 
			
		||||
 | 
			
		||||
      - name: Upload Playwright trace
 | 
			
		||||
        if: failure()
 | 
			
		||||
@@ -141,10 +141,10 @@ jobs:
 | 
			
		||||
        run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
@@ -223,7 +223,7 @@ jobs:
 | 
			
		||||
      - build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,6 @@ permissions:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  nightly-electron:
 | 
			
		||||
    if: github.repository == ${{ vars.REPO_MAIN }}
 | 
			
		||||
    name: Deploy nightly
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
@@ -48,15 +47,16 @@ jobs:
 | 
			
		||||
            forge_platform: win32
 | 
			
		||||
    runs-on: ${{ matrix.os.image }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
      - uses: nrwl/nx-set-shas@v4
 | 
			
		||||
      - name: Update nightly version
 | 
			
		||||
        run: npm run chore:ci-update-nightly-version
 | 
			
		||||
      - name: Run the build
 | 
			
		||||
@@ -75,10 +75,9 @@ jobs:
 | 
			
		||||
          APPLE_ID: ${{ secrets.APPLE_ID }}
 | 
			
		||||
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
 | 
			
		||||
          WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
 | 
			
		||||
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
 | 
			
		||||
 | 
			
		||||
      - name: Publish release
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.3
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.2
 | 
			
		||||
        if: ${{ github.event_name != 'pull_request' }}
 | 
			
		||||
        with:
 | 
			
		||||
          make_latest: false
 | 
			
		||||
@@ -97,7 +96,6 @@ jobs:
 | 
			
		||||
          path: apps/desktop/upload
 | 
			
		||||
 | 
			
		||||
  nightly-server:
 | 
			
		||||
    if: github.repository == ${{ vars.REPO_MAIN }}
 | 
			
		||||
    name: Deploy server nightly
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
@@ -110,7 +108,7 @@ jobs:
 | 
			
		||||
            runs-on: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.runs-on }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Run the build
 | 
			
		||||
        uses: ./.github/actions/build-server
 | 
			
		||||
@@ -119,7 +117,7 @@ jobs:
 | 
			
		||||
          arch: ${{ matrix.arch }}
 | 
			
		||||
 | 
			
		||||
      - name: Publish release
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.3
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.2
 | 
			
		||||
        if: ${{ github.event_name != 'pull_request' }}
 | 
			
		||||
        with:
 | 
			
		||||
          make_latest: false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,13 +14,19 @@ jobs:
 | 
			
		||||
  main:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          filter: tree:0
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
 | 
			
		||||
      # This enables task distribution via Nx Cloud
 | 
			
		||||
      # Run this command as early as possible, before dependencies are installed
 | 
			
		||||
      # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
 | 
			
		||||
      # Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
 | 
			
		||||
      # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
@@ -28,12 +34,10 @@ jobs:
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
      - run: pnpm exec playwright install --with-deps
 | 
			
		||||
      - uses: nrwl/nx-set-shas@v4
 | 
			
		||||
 | 
			
		||||
      - run: pnpm --filter server-e2e e2e
 | 
			
		||||
 | 
			
		||||
      - name: Upload test report
 | 
			
		||||
        if: failure()
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: e2e report
 | 
			
		||||
          path: apps/server-e2e/test-output
 | 
			
		||||
      # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
 | 
			
		||||
      # - run: npx nx-cloud record -- echo Hello World
 | 
			
		||||
      # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
 | 
			
		||||
      # When you enable task distribution, run the e2e-ci task instead of e2e
 | 
			
		||||
      - run: pnpm exec nx affected -t e2e --exclude desktop-e2e
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -32,15 +32,16 @@ jobs:
 | 
			
		||||
            forge_platform: win32
 | 
			
		||||
    runs-on: ${{ matrix.os.image }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v5
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
      - uses: nrwl/nx-set-shas@v4
 | 
			
		||||
      - name: Run the build
 | 
			
		||||
        uses: ./.github/actions/build-electron
 | 
			
		||||
        with:
 | 
			
		||||
@@ -57,7 +58,6 @@ jobs:
 | 
			
		||||
          APPLE_ID: ${{ secrets.APPLE_ID }}
 | 
			
		||||
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
 | 
			
		||||
          WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
 | 
			
		||||
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload the artifact
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
@@ -78,7 +78,7 @@ jobs:
 | 
			
		||||
            runs-on: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.runs-on }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Run the build
 | 
			
		||||
        uses: ./.github/actions/build-server
 | 
			
		||||
@@ -101,20 +101,20 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: mkdir upload
 | 
			
		||||
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          sparse-checkout: |
 | 
			
		||||
            docs/Release Notes
 | 
			
		||||
 | 
			
		||||
      - name: Download all artifacts
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
          pattern: release-*
 | 
			
		||||
          path: upload
 | 
			
		||||
 | 
			
		||||
      - name: Publish stable release
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.3
 | 
			
		||||
        uses: softprops/action-gh-release@v2.3.2
 | 
			
		||||
        with:
 | 
			
		||||
          draft: false
 | 
			
		||||
          body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								.github/workflows/unblock_signing.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/unblock_signing.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,11 +0,0 @@
 | 
			
		||||
name: Unblock signing
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  unblock-win-signing:
 | 
			
		||||
    runs-on: win-signing
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: |
 | 
			
		||||
          cat ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
 | 
			
		||||
          rm ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
 | 
			
		||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,4 @@
 | 
			
		||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
 | 
			
		||||
/.cache
 | 
			
		||||
 | 
			
		||||
# compiled output
 | 
			
		||||
dist
 | 
			
		||||
@@ -11,7 +10,6 @@ node_modules
 | 
			
		||||
 | 
			
		||||
# IDEs and editors
 | 
			
		||||
/.idea
 | 
			
		||||
.idea
 | 
			
		||||
.project
 | 
			
		||||
.classpath
 | 
			
		||||
.c9/
 | 
			
		||||
@@ -33,11 +31,14 @@ testem.log
 | 
			
		||||
.DS_Store
 | 
			
		||||
Thumbs.db
 | 
			
		||||
 | 
			
		||||
.nx/cache
 | 
			
		||||
.nx/workspace-data
 | 
			
		||||
 | 
			
		||||
vite.config.*.timestamp*
 | 
			
		||||
vitest.config.*.timestamp*
 | 
			
		||||
test-output
 | 
			
		||||
 | 
			
		||||
apps/*/data*
 | 
			
		||||
apps/*/data
 | 
			
		||||
apps/*/out
 | 
			
		||||
upload
 | 
			
		||||
 | 
			
		||||
@@ -46,6 +47,3 @@ upload
 | 
			
		||||
 | 
			
		||||
/result
 | 
			
		||||
.svelte-kit
 | 
			
		||||
 | 
			
		||||
# docs
 | 
			
		||||
site/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
# Default ignored files
 | 
			
		||||
/workspace.xml
 | 
			
		||||
 | 
			
		||||
# Datasource local storage ignored files
 | 
			
		||||
/dataSources.local.xml
 | 
			
		||||
/dataSources/
 | 
			
		||||
							
								
								
									
										15
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
<component name="ProjectCodeStyleConfiguration">
 | 
			
		||||
  <code_scheme name="Project" version="173">
 | 
			
		||||
    <option name="OTHER_INDENT_OPTIONS">
 | 
			
		||||
      <value>
 | 
			
		||||
        <option name="INDENT_SIZE" value="2" />
 | 
			
		||||
        <option name="TAB_SIZE" value="2" />
 | 
			
		||||
      </value>
 | 
			
		||||
    </option>
 | 
			
		||||
    <codeStyleSettings language="JSON">
 | 
			
		||||
      <indentOptions>
 | 
			
		||||
        <option name="INDENT_SIZE" value="4" />
 | 
			
		||||
      </indentOptions>
 | 
			
		||||
    </codeStyleSettings>
 | 
			
		||||
  </code_scheme>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<component name="ProjectCodeStyleConfiguration">
 | 
			
		||||
  <state>
 | 
			
		||||
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
 | 
			
		||||
  </state>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
 | 
			
		||||
    <data-source source="LOCAL" name="document.db" uuid="2a4ac1e6-b828-4a2a-8e4a-3f59f10aff26">
 | 
			
		||||
      <driver-ref>sqlite.xerial</driver-ref>
 | 
			
		||||
      <synchronize>true</synchronize>
 | 
			
		||||
      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
			
		||||
      <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data/document.db</jdbc-url>
 | 
			
		||||
      <working-dir>$ProjectFileDir$</working-dir>
 | 
			
		||||
    </data-source>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										4
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="Encoding" addBOMForNewFiles="with NO BOM" />
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										15
									
								
								.idea/git_toolbox_prj.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.idea/git_toolbox_prj.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="GitToolBoxProjectSettings">
 | 
			
		||||
    <option name="commitMessageIssueKeyValidationOverride">
 | 
			
		||||
      <BoolValueOverride>
 | 
			
		||||
        <option name="enabled" value="true" />
 | 
			
		||||
      </BoolValueOverride>
 | 
			
		||||
    </option>
 | 
			
		||||
    <option name="commitMessageValidationEnabledOverride">
 | 
			
		||||
      <BoolValueOverride>
 | 
			
		||||
        <option name="enabled" value="true" />
 | 
			
		||||
      </BoolValueOverride>
 | 
			
		||||
    </option>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
<component name="InspectionProjectProfileManager">
 | 
			
		||||
  <profile version="1.0">
 | 
			
		||||
    <option name="myName" value="Project Default" />
 | 
			
		||||
    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
 | 
			
		||||
    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
 | 
			
		||||
      <option name="processCode" value="true" />
 | 
			
		||||
      <option name="processLiterals" value="true" />
 | 
			
		||||
      <option name="processComments" value="true" />
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
  </profile>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/jsLibraryMappings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/jsLibraryMappings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="JavaScriptLibraryMappings">
 | 
			
		||||
    <includedPredefinedLibrary name="Node.js Core" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="JSLintConfiguration">
 | 
			
		||||
    <option devel="true" />
 | 
			
		||||
    <option es6="true" />
 | 
			
		||||
    <option maxerr="50" />
 | 
			
		||||
    <option node="true" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="JavaScriptSettings">
 | 
			
		||||
    <option name="languageLevel" value="ES6" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectRootManager" version="2" languageLevel="JDK_16" default="true" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK">
 | 
			
		||||
    <output url="file://$PROJECT_DIR$/out" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectModuleManager">
 | 
			
		||||
    <modules>
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" />
 | 
			
		||||
    </modules>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										7
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="SqlDialectMappings">
 | 
			
		||||
    <file url="file://$PROJECT_DIR$" dialect="SQLite" />
 | 
			
		||||
    <file url="PROJECT" dialect="SQLite" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="VcsDirectoryMappings">
 | 
			
		||||
    <mapping directory="" vcs="Git" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										4
									
								
								.mailmap
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.mailmap
									
									
									
									
									
								
							@@ -1,2 +1,2 @@
 | 
			
		||||
zadam <adam.zivner@gmail.com>
 | 
			
		||||
zadam <zadam.apps@gmail.com>
 | 
			
		||||
Adam Zivner <adam.zivner@gmail.com>
 | 
			
		||||
Adam Zivner <zadam.apps@gmail.com>
 | 
			
		||||
							
								
								
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							@@ -5,6 +5,7 @@
 | 
			
		||||
    "lokalise.i18n-ally",
 | 
			
		||||
    "ms-azuretools.vscode-docker",
 | 
			
		||||
    "ms-playwright.playwright",
 | 
			
		||||
    "nrwl.angular-console",
 | 
			
		||||
    "redhat.vscode-yaml",
 | 
			
		||||
    "tobermory.es6-string-html",
 | 
			
		||||
    "vitest.explorer",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.vscode/i18n-ally-custom-framework.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/i18n-ally-custom-framework.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,6 @@
 | 
			
		||||
languageIds:
 | 
			
		||||
  - javascript
 | 
			
		||||
  - typescript
 | 
			
		||||
  - typescriptreact
 | 
			
		||||
  - html
 | 
			
		||||
 | 
			
		||||
# An array of RegExes to find the key usage. **The key should be captured in the first match group**.
 | 
			
		||||
@@ -26,7 +25,6 @@ scopeRangeRegex: "useTranslation\\(\\s*\\[?\\s*['\"`](.*?)['\"`]"
 | 
			
		||||
# The "$1" will be replaced by the keypath specified.
 | 
			
		||||
refactorTemplates:
 | 
			
		||||
  - t("$1")
 | 
			
		||||
  - {t("$1")}
 | 
			
		||||
  - ${t("$1")}
 | 
			
		||||
  - <%= t("$1") %>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -28,12 +28,5 @@
 | 
			
		||||
    "typescript.validate.enable": true,
 | 
			
		||||
    "typescript.tsserver.experimental.enableProjectDiagnostics": true,
 | 
			
		||||
    "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
    "typescript.enablePromptUseWorkspaceTsdk": true,
 | 
			
		||||
    "search.exclude": {
 | 
			
		||||
        "**/node_modules": true,
 | 
			
		||||
        "docs/**/*.html": true,
 | 
			
		||||
        "docs/**/*.png": true,
 | 
			
		||||
        "apps/server/src/assets/doc_notes/**": true,
 | 
			
		||||
        "apps/edit-docs/demo/**": true
 | 
			
		||||
    }
 | 
			
		||||
    "typescript.enablePromptUseWorkspaceTsdk": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/snippets.code-snippets
									
									
									
									
										vendored
									
									
								
							@@ -20,10 +20,5 @@
 | 
			
		||||
        "scope": "typescript",
 | 
			
		||||
        "prefix": "jqf",
 | 
			
		||||
        "body": ["private $${1:name}!: JQuery<HTMLElement>;"]
 | 
			
		||||
    },
 | 
			
		||||
    "region": {
 | 
			
		||||
        "scope": "css",
 | 
			
		||||
        "prefix": "region",
 | 
			
		||||
        "body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										152
									
								
								CLAUDE.md
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								CLAUDE.md
									
									
									
									
									
								
							@@ -1,152 +0,0 @@
 | 
			
		||||
# CLAUDE.md
 | 
			
		||||
 | 
			
		||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
 | 
			
		||||
 | 
			
		||||
## Development Commands
 | 
			
		||||
 | 
			
		||||
### Setup
 | 
			
		||||
- `pnpm install` - Install all dependencies
 | 
			
		||||
- `corepack enable` - Enable pnpm if not available
 | 
			
		||||
 | 
			
		||||
### Running Applications
 | 
			
		||||
- `pnpm run server:start` - Start development server (http://localhost:8080)
 | 
			
		||||
- `pnpm run server:start-prod` - Run server in production mode
 | 
			
		||||
 | 
			
		||||
### Building
 | 
			
		||||
- `pnpm run client:build` - Build client application
 | 
			
		||||
- `pnpm run server:build` - Build server application
 | 
			
		||||
- `pnpm run electron:build` - Build desktop application
 | 
			
		||||
 | 
			
		||||
### Testing
 | 
			
		||||
- `pnpm test:all` - Run all tests (parallel + sequential)
 | 
			
		||||
- `pnpm test:parallel` - Run tests that can run in parallel
 | 
			
		||||
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
 | 
			
		||||
- `pnpm coverage` - Generate coverage reports
 | 
			
		||||
 | 
			
		||||
## Architecture Overview
 | 
			
		||||
 | 
			
		||||
### Monorepo Structure
 | 
			
		||||
- **apps/**: Runnable applications
 | 
			
		||||
  - `client/` - Frontend application (shared by server and desktop)
 | 
			
		||||
  - `server/` - Node.js server with web interface
 | 
			
		||||
  - `desktop/` - Electron desktop application
 | 
			
		||||
  - `web-clipper/` - Browser extension for saving web content
 | 
			
		||||
  - Additional tools: `db-compare`, `dump-db`, `edit-docs`
 | 
			
		||||
 | 
			
		||||
- **packages/**: Shared libraries
 | 
			
		||||
  - `commons/` - Shared interfaces and utilities
 | 
			
		||||
  - `ckeditor5/` - Custom rich text editor with Trilium-specific plugins
 | 
			
		||||
  - `codemirror/` - Code editor customizations
 | 
			
		||||
  - `highlightjs/` - Syntax highlighting
 | 
			
		||||
  - Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid`
 | 
			
		||||
 | 
			
		||||
### Core Architecture Patterns
 | 
			
		||||
 | 
			
		||||
#### Three-Layer Cache System
 | 
			
		||||
- **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`)
 | 
			
		||||
- **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`)
 | 
			
		||||
- **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`)
 | 
			
		||||
 | 
			
		||||
#### Entity System
 | 
			
		||||
Core entities are defined in `apps/server/src/becca/entities/`:
 | 
			
		||||
- `BNote` - Notes with content and metadata
 | 
			
		||||
- `BBranch` - Hierarchical relationships between notes (allows multiple parents)
 | 
			
		||||
- `BAttribute` - Key-value metadata attached to notes
 | 
			
		||||
- `BRevision` - Note version history
 | 
			
		||||
- `BOption` - Application configuration
 | 
			
		||||
 | 
			
		||||
#### Widget-Based UI
 | 
			
		||||
Frontend uses a widget system (`apps/client/src/widgets/`):
 | 
			
		||||
- `BasicWidget` - Base class for all UI components
 | 
			
		||||
- `NoteContextAwareWidget` - Widgets that respond to note changes
 | 
			
		||||
- `RightPanelWidget` - Widgets displayed in the right panel
 | 
			
		||||
- Type-specific widgets in `type_widgets/` directory
 | 
			
		||||
 | 
			
		||||
#### API Architecture
 | 
			
		||||
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
 | 
			
		||||
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
 | 
			
		||||
- **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`)
 | 
			
		||||
 | 
			
		||||
### Key Files for Understanding Architecture
 | 
			
		||||
 | 
			
		||||
1. **Application Entry Points**:
 | 
			
		||||
   - `apps/server/src/main.ts` - Server startup
 | 
			
		||||
   - `apps/client/src/desktop.ts` - Client initialization
 | 
			
		||||
 | 
			
		||||
2. **Core Services**:
 | 
			
		||||
   - `apps/server/src/becca/becca.ts` - Backend data management
 | 
			
		||||
   - `apps/client/src/services/froca.ts` - Frontend data synchronization
 | 
			
		||||
   - `apps/server/src/services/backend_script_api.ts` - Scripting API
 | 
			
		||||
 | 
			
		||||
3. **Database Schema**:
 | 
			
		||||
   - `apps/server/src/assets/db/schema.sql` - Core database structure
 | 
			
		||||
 | 
			
		||||
4. **Configuration**:
 | 
			
		||||
   - `package.json` - Project dependencies and scripts
 | 
			
		||||
 | 
			
		||||
## Note Types and Features
 | 
			
		||||
 | 
			
		||||
Trilium supports multiple note types, each with specialized widgets:
 | 
			
		||||
- **Text**: Rich text with CKEditor5 (markdown import/export)
 | 
			
		||||
- **Code**: Syntax-highlighted code editing with CodeMirror
 | 
			
		||||
- **File**: Binary file attachments
 | 
			
		||||
- **Image**: Image display with editing capabilities
 | 
			
		||||
- **Canvas**: Drawing/diagramming with Excalidraw
 | 
			
		||||
- **Mermaid**: Diagram generation
 | 
			
		||||
- **Relation Map**: Visual note relationship mapping
 | 
			
		||||
- **Web View**: Embedded web pages
 | 
			
		||||
- **Doc/Book**: Hierarchical documentation structure
 | 
			
		||||
 | 
			
		||||
## Development Guidelines
 | 
			
		||||
 | 
			
		||||
### Testing Strategy
 | 
			
		||||
- Server tests run sequentially due to shared database
 | 
			
		||||
- Client tests can run in parallel
 | 
			
		||||
- E2E tests use Playwright for both server and desktop apps
 | 
			
		||||
- Build validation tests check artifact integrity
 | 
			
		||||
 | 
			
		||||
### Scripting System
 | 
			
		||||
Trilium provides powerful user scripting capabilities:
 | 
			
		||||
- Frontend scripts run in browser context
 | 
			
		||||
- Backend scripts run in Node.js context with full API access
 | 
			
		||||
- Script API documentation available in `docs/Script API/`
 | 
			
		||||
 | 
			
		||||
### Internationalization
 | 
			
		||||
- Translation files in `apps/client/src/translations/`
 | 
			
		||||
- Supported languages: English, German, Spanish, French, Romanian, Chinese
 | 
			
		||||
 | 
			
		||||
### Security Considerations
 | 
			
		||||
- Per-note encryption with granular protected sessions
 | 
			
		||||
- CSRF protection for API endpoints
 | 
			
		||||
- OpenID and TOTP authentication support
 | 
			
		||||
- Sanitization of user-generated content
 | 
			
		||||
 | 
			
		||||
## Common Development Tasks
 | 
			
		||||
 | 
			
		||||
### Adding New Note Types
 | 
			
		||||
1. Create widget in `apps/client/src/widgets/type_widgets/`
 | 
			
		||||
2. Register in `apps/client/src/services/note_types.ts`
 | 
			
		||||
3. Add backend handling in `apps/server/src/services/notes.ts`
 | 
			
		||||
 | 
			
		||||
### Extending Search
 | 
			
		||||
- Search expressions handled in `apps/server/src/services/search/`
 | 
			
		||||
- Add new search operators in search context files
 | 
			
		||||
 | 
			
		||||
### Custom CKEditor Plugins
 | 
			
		||||
- Create new package in `packages/` following existing plugin structure
 | 
			
		||||
- Register in `packages/ckeditor5/src/plugins.ts`
 | 
			
		||||
 | 
			
		||||
### Database Migrations
 | 
			
		||||
- Add migration scripts in `apps/server/src/migrations/`
 | 
			
		||||
- Update schema in `apps/server/src/assets/db/schema.sql`
 | 
			
		||||
 | 
			
		||||
## Build System Notes
 | 
			
		||||
- Uses pnpm for monorepo management
 | 
			
		||||
- Vite for fast development builds
 | 
			
		||||
- ESBuild for production optimization
 | 
			
		||||
- pnpm workspaces for dependency management
 | 
			
		||||
- Docker support with multi-stage builds
 | 
			
		||||
							
								
								
									
										66
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								README.md
									
									
									
									
									
								
							@@ -1,11 +1,11 @@
 | 
			
		||||
# Trilium Notes
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||

 | 
			
		||||
  
 | 
			
		||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
 | 
			
		||||
 | 
			
		||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
 | 
			
		||||
[English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
 | 
			
		||||
 | 
			
		||||
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
 | 
			
		||||
 | 
			
		||||
@@ -46,15 +46,15 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
 | 
			
		||||
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
 | 
			
		||||
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
 | 
			
		||||
 | 
			
		||||
## ❓Why TriliumNext?
 | 
			
		||||
## ⚠️ Why TriliumNext?
 | 
			
		||||
 | 
			
		||||
The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext
 | 
			
		||||
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
 | 
			
		||||
 | 
			
		||||
### ⬆️Migrating from Zadam/Trilium?
 | 
			
		||||
### Migrating from Trilium?
 | 
			
		||||
 | 
			
		||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database.
 | 
			
		||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
 | 
			
		||||
 | 
			
		||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
 | 
			
		||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
 | 
			
		||||
 | 
			
		||||
## 📖 Documentation
 | 
			
		||||
 | 
			
		||||
@@ -75,14 +75,14 @@ Feel free to join our official conversations. We would love to hear what feature
 | 
			
		||||
 | 
			
		||||
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
 | 
			
		||||
  - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
 | 
			
		||||
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.)
 | 
			
		||||
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.)
 | 
			
		||||
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
 | 
			
		||||
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
 | 
			
		||||
 | 
			
		||||
## 🏗 Installation
 | 
			
		||||
 | 
			
		||||
### Windows / MacOS
 | 
			
		||||
 | 
			
		||||
Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable.
 | 
			
		||||
Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
 | 
			
		||||
 | 
			
		||||
### Linux
 | 
			
		||||
 | 
			
		||||
@@ -90,7 +90,7 @@ If your distribution is listed in the table below, use your distribution's packa
 | 
			
		||||
 | 
			
		||||
[](https://repology.org/project/triliumnext/versions)
 | 
			
		||||
 | 
			
		||||
You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable.
 | 
			
		||||
You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
 | 
			
		||||
 | 
			
		||||
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
 | 
			
		||||
 | 
			
		||||
@@ -104,33 +104,23 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested
 | 
			
		||||
 | 
			
		||||
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
 | 
			
		||||
 | 
			
		||||
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support.
 | 
			
		||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
 | 
			
		||||
 | 
			
		||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
 | 
			
		||||
Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
 | 
			
		||||
Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid.
 | 
			
		||||
See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
 | 
			
		||||
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
 | 
			
		||||
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 💻 Contribute
 | 
			
		||||
 | 
			
		||||
### Translations
 | 
			
		||||
 | 
			
		||||
If you are a native speaker, help us translate Trilium by heading over to our [Weblate page](https://hosted.weblate.org/engage/trilium/).
 | 
			
		||||
 | 
			
		||||
Here's the language coverage we have so far:
 | 
			
		||||
 | 
			
		||||
[](https://hosted.weblate.org/engage/trilium/)
 | 
			
		||||
 | 
			
		||||
### Code
 | 
			
		||||
 | 
			
		||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
 | 
			
		||||
```shell
 | 
			
		||||
git clone https://github.com/TriliumNext/Trilium.git
 | 
			
		||||
cd Trilium
 | 
			
		||||
git clone https://github.com/TriliumNext/Notes.git
 | 
			
		||||
cd Notes
 | 
			
		||||
pnpm install
 | 
			
		||||
pnpm run server:start
 | 
			
		||||
```
 | 
			
		||||
@@ -139,26 +129,26 @@ pnpm run server:start
 | 
			
		||||
 | 
			
		||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
 | 
			
		||||
```shell
 | 
			
		||||
git clone https://github.com/TriliumNext/Trilium.git
 | 
			
		||||
cd Trilium
 | 
			
		||||
git clone https://github.com/TriliumNext/Notes.git
 | 
			
		||||
cd Notes
 | 
			
		||||
pnpm install
 | 
			
		||||
pnpm edit-docs:edit-docs
 | 
			
		||||
pnpm nx run edit-docs:edit-docs
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Building the Executable
 | 
			
		||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
 | 
			
		||||
```shell
 | 
			
		||||
git clone https://github.com/TriliumNext/Trilium.git
 | 
			
		||||
cd Trilium
 | 
			
		||||
git clone https://github.com/TriliumNext/Notes.git
 | 
			
		||||
cd Notes
 | 
			
		||||
pnpm install
 | 
			
		||||
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
 | 
			
		||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
 | 
			
		||||
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
 | 
			
		||||
 | 
			
		||||
### Developer Documentation
 | 
			
		||||
 | 
			
		||||
Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
 | 
			
		||||
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
 | 
			
		||||
 | 
			
		||||
## 👏 Shoutouts
 | 
			
		||||
 | 
			
		||||
@@ -170,7 +160,7 @@ Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blo
 | 
			
		||||
## 🤝 Support
 | 
			
		||||
 | 
			
		||||
Support for the TriliumNext organization will be possible in the near future. For now, you can:
 | 
			
		||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/trilium/graphs/contributors))) for a full list)
 | 
			
		||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
 | 
			
		||||
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,13 +35,13 @@
 | 
			
		||||
    "chore:generate-openapi": "tsx bin/generate-openapi.js"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {    
 | 
			
		||||
    "@playwright/test": "1.55.0",
 | 
			
		||||
    "@stylistic/eslint-plugin": "5.3.1",        
 | 
			
		||||
    "@playwright/test": "1.53.2",
 | 
			
		||||
    "@stylistic/eslint-plugin": "5.1.0",        
 | 
			
		||||
    "@types/express": "5.0.3",    
 | 
			
		||||
    "@types/node": "22.18.1",    
 | 
			
		||||
    "@types/node": "22.16.2",    
 | 
			
		||||
    "@types/yargs": "17.0.33",
 | 
			
		||||
    "@vitest/coverage-v8": "3.2.4",
 | 
			
		||||
    "eslint": "9.35.0",
 | 
			
		||||
    "eslint": "9.30.1",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "12.1.1",
 | 
			
		||||
    "esm": "3.2.25",
 | 
			
		||||
    "jsdoc": "4.0.4",
 | 
			
		||||
@@ -49,8 +49,8 @@
 | 
			
		||||
    "rcedit": "4.0.1",
 | 
			
		||||
    "rimraf": "6.0.1",    
 | 
			
		||||
    "tslib": "2.8.1",    
 | 
			
		||||
    "typedoc": "0.28.12",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.1.0"
 | 
			
		||||
    "typedoc": "0.28.7",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "appdmg": "0.6.6"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@triliumnext/client",
 | 
			
		||||
  "version": "0.98.1",
 | 
			
		||||
  "version": "0.96.0",
 | 
			
		||||
  "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
@@ -9,22 +9,16 @@
 | 
			
		||||
    "email": "contact@eliandoran.me",
 | 
			
		||||
    "url": "https://github.com/TriliumNext/Notes"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
 | 
			
		||||
    "test": "vitest",
 | 
			
		||||
    "circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@eslint/js": "9.35.0",
 | 
			
		||||
    "@eslint/js": "9.30.1",
 | 
			
		||||
    "@excalidraw/excalidraw": "0.18.0",
 | 
			
		||||
    "@fullcalendar/core": "6.1.19",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.19",
 | 
			
		||||
    "@fullcalendar/interaction": "6.1.19",
 | 
			
		||||
    "@fullcalendar/list": "6.1.19",
 | 
			
		||||
    "@fullcalendar/multimonth": "6.1.19",
 | 
			
		||||
    "@fullcalendar/timegrid": "6.1.19",
 | 
			
		||||
    "@maplibre/maplibre-gl-leaflet": "0.1.3",
 | 
			
		||||
    "@mermaid-js/layout-elk": "0.2.0",
 | 
			
		||||
    "@fullcalendar/core": "6.1.18",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.18",
 | 
			
		||||
    "@fullcalendar/interaction": "6.1.18",
 | 
			
		||||
    "@fullcalendar/list": "6.1.18",
 | 
			
		||||
    "@fullcalendar/multimonth": "6.1.18",
 | 
			
		||||
    "@fullcalendar/timegrid": "6.1.18",
 | 
			
		||||
    "@mermaid-js/layout-elk": "0.1.8",
 | 
			
		||||
    "@mind-elixir/node-menu": "5.0.0",
 | 
			
		||||
    "@popperjs/core": "2.11.8",
 | 
			
		||||
    "@triliumnext/ckeditor5": "workspace:*",
 | 
			
		||||
@@ -33,17 +27,18 @@
 | 
			
		||||
    "@triliumnext/highlightjs": "workspace:*",
 | 
			
		||||
    "@triliumnext/share-theme": "workspace:*",
 | 
			
		||||
    "autocomplete.js": "0.38.1",
 | 
			
		||||
    "bootstrap": "5.3.8",
 | 
			
		||||
    "bootstrap": "5.3.7",
 | 
			
		||||
    "boxicons": "2.1.4",
 | 
			
		||||
    "dayjs": "1.11.18",
 | 
			
		||||
    "dayjs": "1.11.13",
 | 
			
		||||
    "dayjs-plugin-utc": "0.1.2",
 | 
			
		||||
    "debounce": "2.2.0",
 | 
			
		||||
    "draggabilly": "3.0.0",
 | 
			
		||||
    "force-graph": "1.51.0",
 | 
			
		||||
    "force-graph": "1.50.1",
 | 
			
		||||
    "globals": "16.3.0",
 | 
			
		||||
    "i18next": "25.5.2",
 | 
			
		||||
    "i18next": "25.3.2",
 | 
			
		||||
    "i18next-http-backend": "3.0.2",
 | 
			
		||||
    "jquery": "3.7.1",
 | 
			
		||||
    "jquery-hotkeys": "0.2.2",
 | 
			
		||||
    "jquery.fancytree": "2.38.5",
 | 
			
		||||
    "jsplumb": "2.15.6",
 | 
			
		||||
    "katex": "0.16.22",
 | 
			
		||||
@@ -51,30 +46,41 @@
 | 
			
		||||
    "leaflet": "1.9.4",
 | 
			
		||||
    "leaflet-gpx": "2.2.0",
 | 
			
		||||
    "mark.js": "8.11.1",
 | 
			
		||||
    "marked": "16.2.1",
 | 
			
		||||
    "mermaid": "11.11.0",
 | 
			
		||||
    "mind-elixir": "5.1.1",
 | 
			
		||||
    "marked": "16.0.0",
 | 
			
		||||
    "mermaid": "11.8.1",
 | 
			
		||||
    "mind-elixir": "5.0.1",
 | 
			
		||||
    "normalize.css": "8.0.1",
 | 
			
		||||
    "panzoom": "9.4.3",
 | 
			
		||||
    "preact": "10.27.1",
 | 
			
		||||
    "react-i18next": "15.7.3",
 | 
			
		||||
    "preact": "10.26.9",
 | 
			
		||||
    "split.js": "1.6.5",
 | 
			
		||||
    "svg-pan-zoom": "3.6.2",
 | 
			
		||||
    "tabulator-tables": "6.3.1",
 | 
			
		||||
    "vanilla-js-wheel-zoom": "9.0.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@ckeditor/ckeditor5-inspector": "5.0.0",
 | 
			
		||||
    "@preact/preset-vite": "2.10.2",
 | 
			
		||||
    "@ckeditor/ckeditor5-inspector": "4.1.0",
 | 
			
		||||
    "@types/bootstrap": "5.2.10",
 | 
			
		||||
    "@types/jquery": "3.5.33",
 | 
			
		||||
    "@types/jquery": "3.5.32",
 | 
			
		||||
    "@types/leaflet": "1.9.20",
 | 
			
		||||
    "@types/leaflet-gpx": "1.3.8",
 | 
			
		||||
    "@types/leaflet-gpx": "1.3.7",
 | 
			
		||||
    "@types/mark.js": "8.11.12",
 | 
			
		||||
    "@types/tabulator-tables": "6.2.10",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "@types/tabulator-tables": "6.2.7",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.0",
 | 
			
		||||
    "happy-dom": "18.0.1",
 | 
			
		||||
    "script-loader": "0.7.2",
 | 
			
		||||
    "vite-plugin-static-copy": "3.1.2"
 | 
			
		||||
    "vite-plugin-static-copy": "3.1.0"
 | 
			
		||||
  },
 | 
			
		||||
  "nx": {
 | 
			
		||||
    "name": "client",
 | 
			
		||||
    "targets": {
 | 
			
		||||
      "serve": {
 | 
			
		||||
        "dependsOn": [
 | 
			
		||||
          "^build"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "circular-deps": {
 | 
			
		||||
        "command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import froca from "../services/froca.js";
 | 
			
		||||
import RootCommandExecutor from "./root_command_executor.js";
 | 
			
		||||
import Entrypoints from "./entrypoints.js";
 | 
			
		||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
import utils, { hasTouchBar } from "../services/utils.js";
 | 
			
		||||
import zoomComponent from "./zoom.js";
 | 
			
		||||
@@ -28,17 +28,16 @@ import TouchBarComponent from "./touch_bar.js";
 | 
			
		||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
 | 
			
		||||
import type CodeMirror from "@triliumnext/codemirror";
 | 
			
		||||
import { StartupChecks } from "./startup_checks.js";
 | 
			
		||||
import type { CreateNoteOpts } from "../services/note_create.js";
 | 
			
		||||
import { ColumnComponent } from "tabulator-tables";
 | 
			
		||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
 | 
			
		||||
import type RootContainer from "../widgets/containers/root_container.js";
 | 
			
		||||
import { SqlExecuteResults } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
interface Layout {
 | 
			
		||||
    getRootWidget: (appContext: AppContext) => RootContainer;
 | 
			
		||||
    getRootWidget: (appContext: AppContext) => RootWidget;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BeforeUploadListener extends Component {
 | 
			
		||||
interface RootWidget extends Component {
 | 
			
		||||
    render: () => JQuery<HTMLElement>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BeforeUploadListener extends Component {
 | 
			
		||||
    beforeUnloadEvent(): boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -83,6 +82,7 @@ export type CommandMappings = {
 | 
			
		||||
    focusTree: CommandData;
 | 
			
		||||
    focusOnTitle: CommandData;
 | 
			
		||||
    focusOnDetail: CommandData;
 | 
			
		||||
    focusOnSearchDefinition: Required<CommandData>;
 | 
			
		||||
    searchNotes: CommandData & {
 | 
			
		||||
        searchString?: string;
 | 
			
		||||
        ancestorNoteId?: string | null;
 | 
			
		||||
@@ -90,14 +90,7 @@ export type CommandMappings = {
 | 
			
		||||
    closeTocCommand: CommandData;
 | 
			
		||||
    closeHlt: CommandData;
 | 
			
		||||
    showLaunchBarSubtree: CommandData;
 | 
			
		||||
    showHiddenSubtree: CommandData;
 | 
			
		||||
    showSQLConsoleHistory: CommandData;
 | 
			
		||||
    logout: CommandData;
 | 
			
		||||
    switchToMobileVersion: CommandData;
 | 
			
		||||
    switchToDesktopVersion: CommandData;
 | 
			
		||||
    showRevisions: CommandData & {
 | 
			
		||||
        noteId?: string | null;
 | 
			
		||||
    };
 | 
			
		||||
    showRevisions: CommandData;
 | 
			
		||||
    showLlmChat: CommandData;
 | 
			
		||||
    createAiChat: CommandData;
 | 
			
		||||
    showOptions: CommandData & {
 | 
			
		||||
@@ -138,9 +131,6 @@ export type CommandMappings = {
 | 
			
		||||
    hideLeftPane: CommandData;
 | 
			
		||||
    showCpuArchWarning: CommandData;
 | 
			
		||||
    showLeftPane: CommandData;
 | 
			
		||||
    showAttachments: CommandData;
 | 
			
		||||
    showSearchHistory: CommandData;
 | 
			
		||||
    showShareSubtree: CommandData;
 | 
			
		||||
    hoistNote: CommandData & { noteId: string };
 | 
			
		||||
    leaveProtectedSession: CommandData;
 | 
			
		||||
    enterProtectedSession: CommandData;
 | 
			
		||||
@@ -181,7 +171,7 @@ export type CommandMappings = {
 | 
			
		||||
    deleteNotes: ContextMenuCommandData;
 | 
			
		||||
    importIntoNote: ContextMenuCommandData;
 | 
			
		||||
    exportNote: ContextMenuCommandData;
 | 
			
		||||
    searchInSubtree: CommandData & { notePath: string; };
 | 
			
		||||
    searchInSubtree: ContextMenuCommandData;
 | 
			
		||||
    moveNoteUp: ContextMenuCommandData;
 | 
			
		||||
    moveNoteDown: ContextMenuCommandData;
 | 
			
		||||
    moveNoteUpInHierarchy: ContextMenuCommandData;
 | 
			
		||||
@@ -270,74 +260,6 @@ export type CommandMappings = {
 | 
			
		||||
    closeThisNoteSplit: CommandData;
 | 
			
		||||
    moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
 | 
			
		||||
    jumpToNote: CommandData;
 | 
			
		||||
    commandPalette: CommandData;
 | 
			
		||||
 | 
			
		||||
    // Keyboard shortcuts
 | 
			
		||||
    backInNoteHistory: CommandData;
 | 
			
		||||
    forwardInNoteHistory: CommandData;
 | 
			
		||||
    forceSaveRevision: CommandData;
 | 
			
		||||
    scrollToActiveNote: CommandData;
 | 
			
		||||
    quickSearch: CommandData;
 | 
			
		||||
    collapseTree: CommandData;
 | 
			
		||||
    createNoteAfter: CommandData;
 | 
			
		||||
    createNoteInto: CommandData;
 | 
			
		||||
    addNoteAboveToSelection: CommandData;
 | 
			
		||||
    addNoteBelowToSelection: CommandData;
 | 
			
		||||
    openNewTab: CommandData;
 | 
			
		||||
    activateNextTab: CommandData;
 | 
			
		||||
    activatePreviousTab: CommandData;
 | 
			
		||||
    openNewWindow: CommandData;
 | 
			
		||||
    toggleTray: CommandData;
 | 
			
		||||
    firstTab: CommandData;
 | 
			
		||||
    secondTab: CommandData;
 | 
			
		||||
    thirdTab: CommandData;
 | 
			
		||||
    fourthTab: CommandData;
 | 
			
		||||
    fifthTab: CommandData;
 | 
			
		||||
    sixthTab: CommandData;
 | 
			
		||||
    seventhTab: CommandData;
 | 
			
		||||
    eigthTab: CommandData;
 | 
			
		||||
    ninthTab: CommandData;
 | 
			
		||||
    lastTab: CommandData;
 | 
			
		||||
    showNoteSource: CommandData;
 | 
			
		||||
    showSQLConsole: CommandData;
 | 
			
		||||
    showBackendLog: CommandData;
 | 
			
		||||
    showCheatsheet: CommandData;
 | 
			
		||||
    showHelp: CommandData;
 | 
			
		||||
    addLinkToText: CommandData;
 | 
			
		||||
    followLinkUnderCursor: CommandData;
 | 
			
		||||
    insertDateTimeToText: CommandData;
 | 
			
		||||
    pasteMarkdownIntoText: CommandData;
 | 
			
		||||
    cutIntoNote: CommandData;
 | 
			
		||||
    addIncludeNoteToText: CommandData;
 | 
			
		||||
    editReadOnlyNote: CommandData;
 | 
			
		||||
    toggleRibbonTabClassicEditor: CommandData;
 | 
			
		||||
    toggleRibbonTabBasicProperties: CommandData;
 | 
			
		||||
    toggleRibbonTabBookProperties: CommandData;
 | 
			
		||||
    toggleRibbonTabFileProperties: CommandData;
 | 
			
		||||
    toggleRibbonTabImageProperties: CommandData;
 | 
			
		||||
    toggleRibbonTabOwnedAttributes: CommandData;
 | 
			
		||||
    toggleRibbonTabInheritedAttributes: CommandData;
 | 
			
		||||
    toggleRibbonTabPromotedAttributes: CommandData;
 | 
			
		||||
    toggleRibbonTabNoteMap: CommandData;
 | 
			
		||||
    toggleRibbonTabNoteInfo: CommandData;
 | 
			
		||||
    toggleRibbonTabNotePaths: CommandData;
 | 
			
		||||
    toggleRibbonTabSimilarNotes: CommandData;
 | 
			
		||||
    toggleRightPane: CommandData;
 | 
			
		||||
    printActiveNote: CommandData;
 | 
			
		||||
    exportAsPdf: CommandData;
 | 
			
		||||
    openNoteExternally: CommandData;
 | 
			
		||||
    openNoteCustom: CommandData;
 | 
			
		||||
    renderActiveNote: CommandData;
 | 
			
		||||
    unhoist: CommandData;
 | 
			
		||||
    reloadFrontendApp: CommandData;
 | 
			
		||||
    openDevTools: CommandData;
 | 
			
		||||
    findInText: CommandData;
 | 
			
		||||
    toggleLeftPane: CommandData;
 | 
			
		||||
    toggleFullscreen: CommandData;
 | 
			
		||||
    zoomOut: CommandData;
 | 
			
		||||
    zoomIn: CommandData;
 | 
			
		||||
    zoomReset: CommandData;
 | 
			
		||||
    copyWithoutFormatting: CommandData;
 | 
			
		||||
 | 
			
		||||
    // Geomap
 | 
			
		||||
    deleteFromMap: { noteId: string };
 | 
			
		||||
@@ -354,30 +276,12 @@ export type CommandMappings = {
 | 
			
		||||
 | 
			
		||||
    geoMapCreateChildNote: CommandData;
 | 
			
		||||
 | 
			
		||||
    // Table view
 | 
			
		||||
    addNewRow: CommandData & {
 | 
			
		||||
        customOpts: CreateNoteOpts;
 | 
			
		||||
        parentNotePath?: string;
 | 
			
		||||
    };
 | 
			
		||||
    addNewTableColumn: CommandData & {
 | 
			
		||||
        columnToEdit?: ColumnComponent;
 | 
			
		||||
        referenceColumn?: ColumnComponent;
 | 
			
		||||
        direction?: "before" | "after";
 | 
			
		||||
        type?: "label" | "relation";
 | 
			
		||||
    };
 | 
			
		||||
    deleteTableColumn: CommandData & {
 | 
			
		||||
        columnToDelete?: ColumnComponent;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    buildTouchBar: CommandData & {
 | 
			
		||||
        TouchBar: typeof TouchBar;
 | 
			
		||||
        buildIcon(name: string): NativeImage;
 | 
			
		||||
    };
 | 
			
		||||
    refreshTouchBar: CommandData;
 | 
			
		||||
    reloadTextEditor: CommandData;
 | 
			
		||||
    chooseNoteType: CommandData & {
 | 
			
		||||
        callback: ChooseNoteTypeCallback
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type EventMappings = {
 | 
			
		||||
@@ -530,7 +434,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
 | 
			
		||||
export class AppContext extends Component {
 | 
			
		||||
    isMainWindow: boolean;
 | 
			
		||||
    components: Component[];
 | 
			
		||||
    beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
 | 
			
		||||
    beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
 | 
			
		||||
    tabManager!: TabManager;
 | 
			
		||||
    layout?: Layout;
 | 
			
		||||
    noteTreeWidget?: NoteTreeWidget;
 | 
			
		||||
@@ -623,7 +527,7 @@ export class AppContext extends Component {
 | 
			
		||||
            component.triggerCommand(commandName, { $el: $(this) });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.child(rootWidget as Component);
 | 
			
		||||
        this.child(rootWidget);
 | 
			
		||||
 | 
			
		||||
        this.triggerEvent("initialRenderComplete", {});
 | 
			
		||||
    }
 | 
			
		||||
@@ -653,17 +557,13 @@ export class AppContext extends Component {
 | 
			
		||||
        return $(el).closest(".component").prop("component");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) {
 | 
			
		||||
    addBeforeUnloadListener(obj: BeforeUploadListener) {
 | 
			
		||||
        if (typeof WeakRef !== "function") {
 | 
			
		||||
            // older browsers don't support WeakRef
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (typeof obj === "object") {
 | 
			
		||||
            this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
 | 
			
		||||
        } else {
 | 
			
		||||
            this.beforeUnloadListeners.push(obj);
 | 
			
		||||
        }
 | 
			
		||||
        this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -673,29 +573,25 @@ const appContext = new AppContext(window.glob.isMainWindow);
 | 
			
		||||
$(window).on("beforeunload", () => {
 | 
			
		||||
    let allSaved = true;
 | 
			
		||||
 | 
			
		||||
    appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref());
 | 
			
		||||
    appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
 | 
			
		||||
 | 
			
		||||
    for (const listener of appContext.beforeUnloadListeners) {
 | 
			
		||||
        if (typeof listener === "object") {
 | 
			
		||||
            const component = listener.deref();
 | 
			
		||||
    for (const weakRef of appContext.beforeUnloadListeners) {
 | 
			
		||||
        const component = weakRef.deref();
 | 
			
		||||
 | 
			
		||||
            if (!component) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        if (!component) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (!component.beforeUnloadEvent()) {
 | 
			
		||||
                console.log(`Component ${component.componentId} is not finished saving its state.`);
 | 
			
		||||
                allSaved = false;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (!listener()) {
 | 
			
		||||
                allSaved = false;
 | 
			
		||||
            }
 | 
			
		||||
        if (!component.beforeUnloadEvent()) {
 | 
			
		||||
            console.log(`Component ${component.componentId} is not finished saving its state.`);
 | 
			
		||||
 | 
			
		||||
            toast.showMessage(t("app_context.please_wait_for_save"), 10000);
 | 
			
		||||
 | 
			
		||||
            allSaved = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!allSaved) {
 | 
			
		||||
        toast.showMessage(t("app_context.please_wait_for_save"), 10000);
 | 
			
		||||
        return "some string";
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
 | 
			
		||||
 | 
			
		||||
type EventHandler = ((data: any) => void);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract class for all components in the Trilium's frontend.
 | 
			
		||||
 *
 | 
			
		||||
@@ -21,7 +19,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
 | 
			
		||||
    initialized: Promise<void> | null;
 | 
			
		||||
    parent?: TypedComponent<any>;
 | 
			
		||||
    _position!: number;
 | 
			
		||||
    private listeners: Record<string, EventHandler[]> | null = {};
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
 | 
			
		||||
@@ -79,14 +76,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
 | 
			
		||||
    handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Handle React children.
 | 
			
		||||
        if (this.listeners?.[name]) {
 | 
			
		||||
            for (const listener of this.listeners[name]) {
 | 
			
		||||
                listener(data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle legacy children.
 | 
			
		||||
        for (const child of this.children) {
 | 
			
		||||
            const ret = child.handleEvent(name, data) as Promise<void>;
 | 
			
		||||
 | 
			
		||||
@@ -131,35 +120,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
 | 
			
		||||
 | 
			
		||||
        return promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
 | 
			
		||||
        if (!this.listeners) {
 | 
			
		||||
            this.listeners = {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.listeners[name]) {
 | 
			
		||||
            this.listeners[name] = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.listeners[name].includes(handler)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.listeners[name].push(handler);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
 | 
			
		||||
        if (!this.listeners?.[name]?.includes(handler)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.listeners[name] = this.listeners[name]
 | 
			
		||||
            .filter(listener => listener !== handler);
 | 
			
		||||
 | 
			
		||||
        if (!this.listeners[name].length) {
 | 
			
		||||
            delete this.listeners[name];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class Component extends TypedComponent<Component> {}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,16 +10,38 @@ import bundleService from "../services/bundle.js";
 | 
			
		||||
import froca from "../services/froca.js";
 | 
			
		||||
import linkService from "../services/link.js";
 | 
			
		||||
import { t } from "../services/i18n.js";
 | 
			
		||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
 | 
			
		||||
import type FNote from "../entities/fnote.js";
 | 
			
		||||
 | 
			
		||||
// TODO: Move somewhere else nicer.
 | 
			
		||||
export type SqlExecuteResults = string[][][];
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate with server.
 | 
			
		||||
interface SqlExecuteResponse {
 | 
			
		||||
    success: boolean;
 | 
			
		||||
    error?: string;
 | 
			
		||||
    results: SqlExecuteResults;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate with server.
 | 
			
		||||
interface CreateChildrenResponse {
 | 
			
		||||
    note: FNote;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class Entrypoints extends Component {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        if (jQuery.hotkeys) {
 | 
			
		||||
            // hot keys are active also inside inputs and content editables
 | 
			
		||||
            jQuery.hotkeys.options.filterInputAcceptingElements = false;
 | 
			
		||||
            jQuery.hotkeys.options.filterContentEditable = false;
 | 
			
		||||
            jQuery.hotkeys.options.filterTextInputs = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openDevToolsCommand() {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools();
 | 
			
		||||
            utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -91,9 +113,7 @@ export default class Entrypoints extends Component {
 | 
			
		||||
            if (win.isFullScreenable()) {
 | 
			
		||||
                win.setFullScreen(!win.isFullScreen());
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            document.documentElement.requestFullscreen();
 | 
			
		||||
        }
 | 
			
		||||
        } // outside of electron this is handled by the browser
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reloadFrontendAppCommand() {
 | 
			
		||||
@@ -109,7 +129,7 @@ export default class Entrypoints extends Component {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            // standard JS version does not work completely correctly in electron
 | 
			
		||||
            const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
 | 
			
		||||
            const activeIndex = webContents.navigationHistory.getActiveIndex();
 | 
			
		||||
            const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
 | 
			
		||||
 | 
			
		||||
            webContents.goToIndex(activeIndex - 1);
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -121,7 +141,7 @@ export default class Entrypoints extends Component {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            // standard JS version does not work completely correctly in electron
 | 
			
		||||
            const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
 | 
			
		||||
            const activeIndex = webContents.navigationHistory.getActiveIndex();
 | 
			
		||||
            const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
 | 
			
		||||
 | 
			
		||||
            webContents.goToIndex(activeIndex + 1);
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -325,9 +325,8 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Collections must always display a note list, even if no children.
 | 
			
		||||
        const viewType = note.getLabelValue("viewType") ?? "grid";
 | 
			
		||||
        if (!["list", "grid"].includes(viewType)) {
 | 
			
		||||
        // Some book types must always display a note list, even if no children.
 | 
			
		||||
        if (["calendar", "table", "geoMap"].includes(note.getLabelValue("viewType") ?? "")) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,8 @@ export default class RootCommandExecutor extends Component {
 | 
			
		||||
        const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
 | 
			
		||||
            activate: true
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,12 @@ import electronContextMenu from "./menus/electron_context_menu.js";
 | 
			
		||||
import glob from "./services/glob.js";
 | 
			
		||||
import { t } from "./services/i18n.js";
 | 
			
		||||
import options from "./services/options.js";
 | 
			
		||||
import server from "./services/server.js";
 | 
			
		||||
import type ElectronRemote from "@electron/remote";
 | 
			
		||||
import type Electron from "electron";
 | 
			
		||||
import "bootstrap/dist/css/bootstrap.min.css";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
import "boxicons/css/boxicons.min.css";
 | 
			
		||||
import "jquery-hotkeys";
 | 
			
		||||
import "autocomplete.js/index_jquery.js";
 | 
			
		||||
 | 
			
		||||
await appContext.earlyInit();
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ export interface NoteMetaData {
 | 
			
		||||
/**
 | 
			
		||||
 * Note is the main node and concept in Trilium.
 | 
			
		||||
 */
 | 
			
		||||
export default class FNote {
 | 
			
		||||
class FNote {
 | 
			
		||||
    private froca: Froca;
 | 
			
		||||
 | 
			
		||||
    noteId!: string;
 | 
			
		||||
@@ -256,20 +256,6 @@ export default class FNote {
 | 
			
		||||
        return this.children;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getSubtreeNoteIds() {
 | 
			
		||||
        let noteIds: (string | string[])[] = [];
 | 
			
		||||
        for (const child of await this.getChildNotes()) {
 | 
			
		||||
            noteIds.push(child.noteId);
 | 
			
		||||
            noteIds.push(await child.getSubtreeNoteIds());
 | 
			
		||||
        }
 | 
			
		||||
        return noteIds.flat();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getSubtreeNotes() {
 | 
			
		||||
        const noteIds = await this.getSubtreeNoteIds();
 | 
			
		||||
        return this.froca.getNotes(noteIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
        return await this.froca.getNotes(this.children);
 | 
			
		||||
    }
 | 
			
		||||
@@ -1020,14 +1006,6 @@ export default class FNote {
 | 
			
		||||
        return this.noteId.startsWith("_options");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isTriliumSqlite() {
 | 
			
		||||
        return this.mime === "text/x-sqlite;schema=trilium";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isTriliumScript() {
 | 
			
		||||
        return this.mime.startsWith("application/javascript");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Provides note's date metadata.
 | 
			
		||||
     */
 | 
			
		||||
@@ -1035,3 +1013,5 @@ export default class FNote {
 | 
			
		||||
        return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default FNote;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +1,78 @@
 | 
			
		||||
import FlexContainer from "../widgets/containers/flex_container.js";
 | 
			
		||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
 | 
			
		||||
import TabRowWidget from "../widgets/tab_row.js";
 | 
			
		||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
 | 
			
		||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
 | 
			
		||||
import NoteTreeWidget from "../widgets/note_tree.js";
 | 
			
		||||
import NoteTitleWidget from "../widgets/note_title.jsx";
 | 
			
		||||
import NoteTitleWidget from "../widgets/note_title.js";
 | 
			
		||||
import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js";
 | 
			
		||||
import NoteActionsWidget from "../widgets/buttons/note_actions.js";
 | 
			
		||||
import NoteDetailWidget from "../widgets/note_detail.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
 | 
			
		||||
import RibbonContainer from "../widgets/containers/ribbon_container.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
 | 
			
		||||
import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js";
 | 
			
		||||
import NoteListWidget from "../widgets/note_list.js";
 | 
			
		||||
import NoteIconWidget from "../widgets/note_icon.jsx";
 | 
			
		||||
import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
 | 
			
		||||
import SqlResultWidget from "../widgets/sql_result.js";
 | 
			
		||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
 | 
			
		||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
 | 
			
		||||
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
 | 
			
		||||
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js";
 | 
			
		||||
import NoteIconWidget from "../widgets/note_icon.js";
 | 
			
		||||
import SearchResultWidget from "../widgets/search_result.js";
 | 
			
		||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
 | 
			
		||||
import RootContainer from "../widgets/containers/root_container.js";
 | 
			
		||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
 | 
			
		||||
import SpacerWidget from "../widgets/spacer.js";
 | 
			
		||||
import QuickSearchWidget from "../widgets/quick_search.js";
 | 
			
		||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
 | 
			
		||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
 | 
			
		||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
 | 
			
		||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
 | 
			
		||||
import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js";
 | 
			
		||||
import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js";
 | 
			
		||||
import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js";
 | 
			
		||||
import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
 | 
			
		||||
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
 | 
			
		||||
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
 | 
			
		||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
 | 
			
		||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
 | 
			
		||||
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
 | 
			
		||||
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
 | 
			
		||||
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
 | 
			
		||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
 | 
			
		||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
 | 
			
		||||
import SharedInfoWidget from "../widgets/shared_info.js";
 | 
			
		||||
import FindWidget from "../widgets/find.js";
 | 
			
		||||
import TocWidget from "../widgets/toc.js";
 | 
			
		||||
import HighlightsListWidget from "../widgets/highlights_list.js";
 | 
			
		||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
 | 
			
		||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
 | 
			
		||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
 | 
			
		||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
 | 
			
		||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
 | 
			
		||||
import RevisionsButton from "../widgets/buttons/revisions_button.js";
 | 
			
		||||
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
 | 
			
		||||
import ApiLogWidget from "../widgets/api_log.js";
 | 
			
		||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
 | 
			
		||||
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
 | 
			
		||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
 | 
			
		||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
 | 
			
		||||
import ScrollPadding from "../widgets/scroll_padding.js";
 | 
			
		||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
 | 
			
		||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
 | 
			
		||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
 | 
			
		||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
 | 
			
		||||
import CloseZenButton from "../widgets/close_zen_button.js";
 | 
			
		||||
import type { AppContext } from "../components/app_context.js";
 | 
			
		||||
import type { WidgetsByParent } from "../services/bundle.js";
 | 
			
		||||
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
 | 
			
		||||
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
 | 
			
		||||
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
 | 
			
		||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
 | 
			
		||||
import { applyModals } from "./layout_commons.js";
 | 
			
		||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
 | 
			
		||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
 | 
			
		||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
 | 
			
		||||
import SearchResult from "../widgets/search_result.jsx";
 | 
			
		||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
 | 
			
		||||
import SqlResults from "../widgets/sql_result.js";
 | 
			
		||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
 | 
			
		||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
 | 
			
		||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
 | 
			
		||||
import ApiLog from "../widgets/api_log.jsx";
 | 
			
		||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
 | 
			
		||||
import SharedInfo from "../widgets/shared_info.jsx";
 | 
			
		||||
 | 
			
		||||
export default class DesktopLayout {
 | 
			
		||||
 | 
			
		||||
@@ -76,9 +107,9 @@ export default class DesktopLayout {
 | 
			
		||||
                new FlexContainer("row")
 | 
			
		||||
                    .class("tab-row-container")
 | 
			
		||||
                    .child(new FlexContainer("row").id("tab-row-left-spacer"))
 | 
			
		||||
                    .optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
 | 
			
		||||
                    .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
 | 
			
		||||
                    .child(new TabRowWidget().class("full-width"))
 | 
			
		||||
                    .optChild(customTitleBarButtons, <TitleBarButtons />)
 | 
			
		||||
                    .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
 | 
			
		||||
                    .css("height", "40px")
 | 
			
		||||
                    .css("background-color", "var(--launcher-pane-background-color)")
 | 
			
		||||
                    .setParent(appContext)
 | 
			
		||||
@@ -99,7 +130,7 @@ export default class DesktopLayout {
 | 
			
		||||
                        new FlexContainer("column")
 | 
			
		||||
                            .id("rest-pane")
 | 
			
		||||
                            .css("flex-grow", "1")
 | 
			
		||||
                            .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
 | 
			
		||||
                            .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
 | 
			
		||||
                            .child(
 | 
			
		||||
                                new FlexContainer("row")
 | 
			
		||||
                                    .filling()
 | 
			
		||||
@@ -120,30 +151,69 @@ export default class DesktopLayout {
 | 
			
		||||
                                                                .css("min-height", "50px")
 | 
			
		||||
                                                                .css("align-items", "center")
 | 
			
		||||
                                                                .cssBlock(".title-row > * { margin: 5px; }")
 | 
			
		||||
                                                                .child(<NoteIconWidget />)
 | 
			
		||||
                                                                .child(<NoteTitleWidget />)
 | 
			
		||||
                                                                .child(new NoteIconWidget())
 | 
			
		||||
                                                                .child(new NoteTitleWidget())
 | 
			
		||||
                                                                .child(new SpacerWidget(0, 1))
 | 
			
		||||
                                                                .child(new MovePaneButton(true))
 | 
			
		||||
                                                                .child(new MovePaneButton(false))
 | 
			
		||||
                                                                .child(new ClosePaneButton())
 | 
			
		||||
                                                                .child(new CreatePaneButton())
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                        .child(<Ribbon />)
 | 
			
		||||
                                                        .child(<SharedInfo />)
 | 
			
		||||
                                                        .child(
 | 
			
		||||
                                                            new RibbonContainer()
 | 
			
		||||
                                                                // the order of the widgets matter. Some of these want to "activate" themselves
 | 
			
		||||
                                                                // when visible. When this happens to multiple of them, the first one "wins".
 | 
			
		||||
                                                                // promoted attributes should always win.
 | 
			
		||||
                                                                .ribbon(new ClassicEditorToolbar())
 | 
			
		||||
                                                                .ribbon(new ScriptExecutorWidget())
 | 
			
		||||
                                                                .ribbon(new SearchDefinitionWidget())
 | 
			
		||||
                                                                .ribbon(new EditedNotesWidget())
 | 
			
		||||
                                                                .ribbon(new BookPropertiesWidget())
 | 
			
		||||
                                                                .ribbon(new NotePropertiesWidget())
 | 
			
		||||
                                                                .ribbon(new FilePropertiesWidget())
 | 
			
		||||
                                                                .ribbon(new ImagePropertiesWidget())
 | 
			
		||||
                                                                .ribbon(new BasicPropertiesWidget())
 | 
			
		||||
                                                                .ribbon(new OwnedAttributeListWidget())
 | 
			
		||||
                                                                .ribbon(new InheritedAttributesWidget())
 | 
			
		||||
                                                                .ribbon(new NotePathsWidget())
 | 
			
		||||
                                                                .ribbon(new NoteMapRibbonWidget())
 | 
			
		||||
                                                                .ribbon(new SimilarNotesWidget())
 | 
			
		||||
                                                                .ribbon(new NoteInfoWidget())
 | 
			
		||||
                                                                .button(new RevisionsButton())
 | 
			
		||||
                                                                .button(new NoteActionsWidget())
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                        .child(new SharedInfoWidget())
 | 
			
		||||
                                                        .child(new WatchedFileUpdateStatusWidget())
 | 
			
		||||
                                                        .child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
 | 
			
		||||
                                                        .child(
 | 
			
		||||
                                                            new FloatingButtons()
 | 
			
		||||
                                                                .child(new RefreshButton())
 | 
			
		||||
                                                                .child(new SwitchSplitOrientationButton())
 | 
			
		||||
                                                                .child(new ToggleReadOnlyButton())
 | 
			
		||||
                                                                .child(new EditButton())
 | 
			
		||||
                                                                .child(new ShowTocWidgetButton())
 | 
			
		||||
                                                                .child(new ShowHighlightsListWidgetButton())
 | 
			
		||||
                                                                .child(new CodeButtonsWidget())
 | 
			
		||||
                                                                .child(new RelationMapButtons())
 | 
			
		||||
                                                                .child(new GeoMapButtons())
 | 
			
		||||
                                                                .child(new CopyImageReferenceButton())
 | 
			
		||||
                                                                .child(new SvgExportButton())
 | 
			
		||||
                                                                .child(new PngExportButton())
 | 
			
		||||
                                                                .child(new BacklinksWidget())
 | 
			
		||||
                                                                .child(new ContextualHelpButton())
 | 
			
		||||
                                                                .child(new HideFloatingButtonsButton())
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                        .child(
 | 
			
		||||
                                                            new ScrollingContainer()
 | 
			
		||||
                                                                .filling()
 | 
			
		||||
                                                                .child(new PromotedAttributesWidget())
 | 
			
		||||
                                                                .child(<SqlTableSchemas />)
 | 
			
		||||
                                                                .child(new SqlTableSchemasWidget())
 | 
			
		||||
                                                                .child(new NoteDetailWidget())
 | 
			
		||||
                                                                .child(new NoteListWidget(false))
 | 
			
		||||
                                                                .child(<SearchResult />)
 | 
			
		||||
                                                                .child(<SqlResults />)
 | 
			
		||||
                                                                .child(<ScrollPadding />)
 | 
			
		||||
                                                                .child(new SearchResultWidget())
 | 
			
		||||
                                                                .child(new SqlResultWidget())
 | 
			
		||||
                                                                .child(new ScrollPaddingWidget())
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                        .child(<ApiLog />)
 | 
			
		||||
                                                        .child(new ApiLogWidget())
 | 
			
		||||
                                                        .child(new FindWidget())
 | 
			
		||||
                                                        .child(
 | 
			
		||||
                                                            ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
 | 
			
		||||
@@ -162,11 +232,11 @@ export default class DesktopLayout {
 | 
			
		||||
                            )
 | 
			
		||||
                    )
 | 
			
		||||
            )
 | 
			
		||||
            .child(<CloseZenModeButton />)
 | 
			
		||||
            .child(new CloseZenButton())
 | 
			
		||||
 | 
			
		||||
            // Desktop-specific dialogs.
 | 
			
		||||
            .child(<PasswordNoteSetDialog />)
 | 
			
		||||
            .child(<UploadAttachmentsDialog />);
 | 
			
		||||
            .child(new PasswordNoteSetDialog())
 | 
			
		||||
            .child(new UploadAttachmentsDialog());
 | 
			
		||||
 | 
			
		||||
        applyModals(rootContainer);
 | 
			
		||||
        return rootContainer;
 | 
			
		||||
@@ -176,18 +246,14 @@ export default class DesktopLayout {
 | 
			
		||||
        let launcherPane;
 | 
			
		||||
 | 
			
		||||
        if (isHorizontal) {
 | 
			
		||||
            launcherPane = new FlexContainer("row")
 | 
			
		||||
                .css("height", "53px")
 | 
			
		||||
                .class("horizontal")
 | 
			
		||||
                .child(new LauncherContainer(true))
 | 
			
		||||
                .child(<GlobalMenu isHorizontalLayout={true} />);
 | 
			
		||||
            launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
 | 
			
		||||
        } else {
 | 
			
		||||
            launcherPane = new FlexContainer("column")
 | 
			
		||||
                .css("width", "53px")
 | 
			
		||||
                .class("vertical")
 | 
			
		||||
                .child(<GlobalMenu isHorizontalLayout={false} />)
 | 
			
		||||
                .child(new GlobalMenuWidget(false))
 | 
			
		||||
                .child(new LauncherContainer(false))
 | 
			
		||||
                .child(<LeftPaneToggle isHorizontalLayout={false} />);
 | 
			
		||||
                .child(new LeftPaneToggleWidget(false));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        launcherPane.id("launcher-pane");
 | 
			
		||||
@@ -24,48 +24,46 @@ import InfoDialog from "../widgets/dialogs/info.js";
 | 
			
		||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
 | 
			
		||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
 | 
			
		||||
import FlexContainer from "../widgets/containers/flex_container.js";
 | 
			
		||||
import NoteIconWidget from "../widgets/note_icon";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
 | 
			
		||||
import NoteIconWidget from "../widgets/note_icon.js";
 | 
			
		||||
import NoteTitleWidget from "../widgets/note_title.js";
 | 
			
		||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
 | 
			
		||||
import NoteDetailWidget from "../widgets/note_detail.js";
 | 
			
		||||
import NoteListWidget from "../widgets/note_list.js";
 | 
			
		||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
 | 
			
		||||
import NoteTitleWidget from "../widgets/note_title.jsx";
 | 
			
		||||
import { PopupEditorFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.js";
 | 
			
		||||
 | 
			
		||||
export function applyModals(rootContainer: RootContainer) {
 | 
			
		||||
    rootContainer
 | 
			
		||||
        .child(<BulkActionsDialog />)
 | 
			
		||||
        .child(<AboutDialog />)
 | 
			
		||||
        .child(<HelpDialog />)
 | 
			
		||||
        .child(<RecentChangesDialog />)
 | 
			
		||||
        .child(<BranchPrefixDialog />)
 | 
			
		||||
        .child(<SortChildNotesDialog />)
 | 
			
		||||
        .child(<IncludeNoteDialog />)
 | 
			
		||||
        .child(<NoteTypeChooserDialog />)
 | 
			
		||||
        .child(<JumpToNoteDialog />)
 | 
			
		||||
        .child(<AddLinkDialog />)
 | 
			
		||||
        .child(<CloneToDialog />)
 | 
			
		||||
        .child(<MoveToDialog />)
 | 
			
		||||
        .child(<ImportDialog />)
 | 
			
		||||
        .child(<ExportDialog />)
 | 
			
		||||
        .child(<MarkdownImportDialog />)
 | 
			
		||||
        .child(<ProtectedSessionPasswordDialog />)
 | 
			
		||||
        .child(<RevisionsDialog />)
 | 
			
		||||
        .child(<DeleteNotesDialog />)
 | 
			
		||||
        .child(<InfoDialog />)
 | 
			
		||||
        .child(<ConfirmDialog />)
 | 
			
		||||
        .child(<PromptDialog />)
 | 
			
		||||
        .child(<IncorrectCpuArchDialog />)
 | 
			
		||||
        .child(new BulkActionsDialog())
 | 
			
		||||
        .child(new AboutDialog())
 | 
			
		||||
        .child(new HelpDialog())
 | 
			
		||||
        .child(new RecentChangesDialog())
 | 
			
		||||
        .child(new BranchPrefixDialog())
 | 
			
		||||
        .child(new SortChildNotesDialog())
 | 
			
		||||
        .child(new IncludeNoteDialog())
 | 
			
		||||
        .child(new NoteTypeChooserDialog())
 | 
			
		||||
        .child(new JumpToNoteDialog())
 | 
			
		||||
        .child(new AddLinkDialog())
 | 
			
		||||
        .child(new CloneToDialog())
 | 
			
		||||
        .child(new MoveToDialog())
 | 
			
		||||
        .child(new ImportDialog())
 | 
			
		||||
        .child(new ExportDialog())
 | 
			
		||||
        .child(new MarkdownImportDialog())
 | 
			
		||||
        .child(new ProtectedSessionPasswordDialog())
 | 
			
		||||
        .child(new RevisionsDialog())
 | 
			
		||||
        .child(new DeleteNotesDialog())
 | 
			
		||||
        .child(new InfoDialog())
 | 
			
		||||
        .child(new ConfirmDialog())
 | 
			
		||||
        .child(new PromptDialog())
 | 
			
		||||
        .child(new IncorrectCpuArchDialog())
 | 
			
		||||
        .child(new PopupEditorDialog()
 | 
			
		||||
                .child(new FlexContainer("row")
 | 
			
		||||
                    .class("title-row")
 | 
			
		||||
                    .css("align-items", "center")
 | 
			
		||||
                    .cssBlock(".title-row > * { margin: 5px; }")
 | 
			
		||||
                    .child(<NoteIconWidget />)
 | 
			
		||||
                    .child(<NoteTitleWidget />))
 | 
			
		||||
                .child(<PopupEditorFormattingToolbar />)
 | 
			
		||||
                    .child(new NoteIconWidget())
 | 
			
		||||
                    .child(new NoteTitleWidget()))
 | 
			
		||||
                .child(new ClassicEditorToolbar())
 | 
			
		||||
                .child(new PromotedAttributesWidget())
 | 
			
		||||
                .child(new NoteDetailWidget())
 | 
			
		||||
                .child(new NoteListWidget(true)))
 | 
			
		||||
        .child(<CallToActionDialog />);
 | 
			
		||||
}
 | 
			
		||||
@@ -3,27 +3,29 @@ import NoteTitleWidget from "../widgets/note_title.js";
 | 
			
		||||
import NoteDetailWidget from "../widgets/note_detail.js";
 | 
			
		||||
import QuickSearchWidget from "../widgets/quick_search.js";
 | 
			
		||||
import NoteTreeWidget from "../widgets/note_tree.js";
 | 
			
		||||
import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js";
 | 
			
		||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
 | 
			
		||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
 | 
			
		||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
 | 
			
		||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
 | 
			
		||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
 | 
			
		||||
import EditButton from "../widgets/floating_buttons/edit_button.js";
 | 
			
		||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
 | 
			
		||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
 | 
			
		||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
 | 
			
		||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
 | 
			
		||||
import NoteListWidget from "../widgets/note_list.js";
 | 
			
		||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
 | 
			
		||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
 | 
			
		||||
import RootContainer from "../widgets/containers/root_container.js";
 | 
			
		||||
import SharedInfoWidget from "../widgets/shared_info.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
 | 
			
		||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
 | 
			
		||||
import type AppContext from "../components/app_context.js";
 | 
			
		||||
import TabRowWidget from "../widgets/tab_row.js";
 | 
			
		||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
 | 
			
		||||
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
 | 
			
		||||
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
 | 
			
		||||
import { applyModals } from "./layout_commons.js";
 | 
			
		||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
 | 
			
		||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
 | 
			
		||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
 | 
			
		||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
 | 
			
		||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
 | 
			
		||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
 | 
			
		||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
 | 
			
		||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
 | 
			
		||||
 | 
			
		||||
const MOBILE_CSS = `
 | 
			
		||||
<style>
 | 
			
		||||
@@ -132,33 +134,38 @@ export default class MobileLayout {
 | 
			
		||||
                            .child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
 | 
			
		||||
                    )
 | 
			
		||||
                    .child(
 | 
			
		||||
                        new ScreenContainer("detail", "row")
 | 
			
		||||
                        new ScreenContainer("detail", "column")
 | 
			
		||||
                            .id("detail-container")
 | 
			
		||||
                            .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
 | 
			
		||||
                            .child(
 | 
			
		||||
                                new NoteWrapperWidget()
 | 
			
		||||
                                    .child(
 | 
			
		||||
                                        new FlexContainer("row")
 | 
			
		||||
                                            .contentSized()
 | 
			
		||||
                                            .css("font-size", "larger")
 | 
			
		||||
                                            .css("align-items", "center")
 | 
			
		||||
                                            .child(<ToggleSidebarButton />)
 | 
			
		||||
                                            .child(<NoteTitleWidget />)
 | 
			
		||||
                                            .child(<MobileDetailMenu />)
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .child(<SharedInfoWidget />)
 | 
			
		||||
                                    .child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
 | 
			
		||||
                                    .child(new PromotedAttributesWidget())
 | 
			
		||||
                                    .child(
 | 
			
		||||
                                        new ScrollingContainer()
 | 
			
		||||
                                            .filling()
 | 
			
		||||
                                            .contentSized()
 | 
			
		||||
                                            .child(new NoteDetailWidget())
 | 
			
		||||
                                            .child(new NoteListWidget(false))
 | 
			
		||||
                                            .child(<FilePropertiesWrapper />)
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .child(<MobileEditorToolbar />)
 | 
			
		||||
                                new FlexContainer("row")
 | 
			
		||||
                                    .contentSized()
 | 
			
		||||
                                    .css("font-size", "larger")
 | 
			
		||||
                                    .css("align-items", "center")
 | 
			
		||||
                                    .child(new ToggleSidebarButtonWidget().contentSized())
 | 
			
		||||
                                    .child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
 | 
			
		||||
                                    .child(new MobileDetailMenuWidget(true).contentSized())
 | 
			
		||||
                            )
 | 
			
		||||
                            .child(new SharedInfoWidget())
 | 
			
		||||
                            .child(
 | 
			
		||||
                                new FloatingButtons()
 | 
			
		||||
                                    .child(new RefreshButton())
 | 
			
		||||
                                    .child(new EditButton())
 | 
			
		||||
                                    .child(new RelationMapButtons())
 | 
			
		||||
                                    .child(new SvgExportButton())
 | 
			
		||||
                                    .child(new BacklinksWidget())
 | 
			
		||||
                                    .child(new HideFloatingButtonsButton())
 | 
			
		||||
                            )
 | 
			
		||||
                            .child(new PromotedAttributesWidget())
 | 
			
		||||
                            .child(
 | 
			
		||||
                                new ScrollingContainer()
 | 
			
		||||
                                    .filling()
 | 
			
		||||
                                    .contentSized()
 | 
			
		||||
                                    .child(new NoteDetailWidget())
 | 
			
		||||
                                    .child(new NoteListWidget(false))
 | 
			
		||||
                                    .child(new FilePropertiesWidget().css("font-size", "smaller"))
 | 
			
		||||
                            )
 | 
			
		||||
                            .child(new MobileEditorToolbar())
 | 
			
		||||
                    )
 | 
			
		||||
            )
 | 
			
		||||
            .child(
 | 
			
		||||
@@ -166,25 +173,9 @@ export default class MobileLayout {
 | 
			
		||||
                    .contentSized()
 | 
			
		||||
                    .id("mobile-bottom-bar")
 | 
			
		||||
                    .child(new TabRowWidget().css("height", "40px"))
 | 
			
		||||
                    .child(new FlexContainer("row")
 | 
			
		||||
                        .class("horizontal")
 | 
			
		||||
                        .css("height", "53px")
 | 
			
		||||
                        .child(new LauncherContainer(true))
 | 
			
		||||
                        .child(<GlobalMenuWidget isHorizontalLayout />)
 | 
			
		||||
                        .id("launcher-pane"))
 | 
			
		||||
            )
 | 
			
		||||
            .child(<CloseZenModeButton />);
 | 
			
		||||
                    .child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
 | 
			
		||||
            );
 | 
			
		||||
        applyModals(rootContainer);
 | 
			
		||||
        return rootContainer;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FilePropertiesWrapper() {
 | 
			
		||||
    const { note } = useNoteContext();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
            {note?.type === "file" && <FilePropertiesTab note={note} />}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import "bootstrap/dist/css/bootstrap.min.css";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
 | 
			
		||||
// @ts-ignore - module = undefined
 | 
			
		||||
// Required for correct loading of scripts in Electron
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,6 @@ export interface MenuCommandItem<T> {
 | 
			
		||||
    title: string;
 | 
			
		||||
    command?: T;
 | 
			
		||||
    type?: string;
 | 
			
		||||
    /**
 | 
			
		||||
     * The icon to display in the menu item.
 | 
			
		||||
     *
 | 
			
		||||
     * If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`.
 | 
			
		||||
     */
 | 
			
		||||
    uiIcon?: string;
 | 
			
		||||
    badges?: MenuItemBadge[];
 | 
			
		||||
    templateNoteId?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ let lastTargetNode: HTMLElement | null = null;
 | 
			
		||||
 | 
			
		||||
// This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator,
 | 
			
		||||
// so they need to be added manually.
 | 
			
		||||
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog" | "searchInSubtree";
 | 
			
		||||
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog";
 | 
			
		||||
 | 
			
		||||
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
 | 
			
		||||
    private treeWidget: NoteTreeWidget;
 | 
			
		||||
@@ -129,6 +129,12 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
 | 
			
		||||
                        enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
 | 
			
		||||
                    },
 | 
			
		||||
                    { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
 | 
			
		||||
                    {
 | 
			
		||||
                        title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
 | 
			
		||||
                        command: "duplicateSubtree",
 | 
			
		||||
                        uiIcon: "bx bx-outline",
 | 
			
		||||
                        enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
                    { title: "----" },
 | 
			
		||||
 | 
			
		||||
@@ -182,13 +188,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`,
 | 
			
		||||
                command: "duplicateSubtree",
 | 
			
		||||
                uiIcon: "bx bx-outline",
 | 
			
		||||
                enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
 | 
			
		||||
                command: "deleteNotes",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import appContext from "./components/app_context.js";
 | 
			
		||||
import noteAutocompleteService from "./services/note_autocomplete.js";
 | 
			
		||||
import glob from "./services/glob.js";
 | 
			
		||||
import "bootstrap/dist/css/bootstrap.min.css";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
import "boxicons/css/boxicons.min.css";
 | 
			
		||||
import "autocomplete.js/index_jquery.js";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -79,19 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
 | 
			
		||||
    return $container;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HIDDEN_ATTRIBUTES = [
 | 
			
		||||
    "originalFileName",
 | 
			
		||||
    "fileSize",
 | 
			
		||||
    "template",
 | 
			
		||||
    "inherit",
 | 
			
		||||
    "cssClass",
 | 
			
		||||
    "iconClass",
 | 
			
		||||
    "pageSize",
 | 
			
		||||
    "viewType",
 | 
			
		||||
    "geolocation",
 | 
			
		||||
    "docName",
 | 
			
		||||
    "webViewSrc"
 | 
			
		||||
];
 | 
			
		||||
const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName"];
 | 
			
		||||
 | 
			
		||||
async function renderNormalAttributes(note: FNote) {
 | 
			
		||||
    const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import server from "./server.js";
 | 
			
		||||
import froca from "./froca.js";
 | 
			
		||||
import type FNote from "../entities/fnote.js";
 | 
			
		||||
import type { AttributeRow } from "./load_results.js";
 | 
			
		||||
import { AttributeType } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
 | 
			
		||||
    await server.put(`notes/${noteId}/attribute`, {
 | 
			
		||||
@@ -13,12 +12,11 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
 | 
			
		||||
export async function setLabel(noteId: string, name: string, value: string = "") {
 | 
			
		||||
    await server.put(`notes/${noteId}/set-attribute`, {
 | 
			
		||||
        type: "label",
 | 
			
		||||
        name: name,
 | 
			
		||||
        value: value,
 | 
			
		||||
        isInheritable
 | 
			
		||||
        value: value
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -26,14 +24,6 @@ async function removeAttributeById(noteId: string, attributeId: string) {
 | 
			
		||||
    await server.remove(`notes/${noteId}/attributes/${attributeId}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
 | 
			
		||||
    for (const attr of note.getOwnedAttributes()) {
 | 
			
		||||
        if (attr.type === type && attr.name === name) {
 | 
			
		||||
            await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
 | 
			
		||||
 * it will not remove inherited attributes.
 | 
			
		||||
@@ -61,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
 | 
			
		||||
 * @param value the value of the attribute to set.
 | 
			
		||||
 */
 | 
			
		||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
 | 
			
		||||
    if (value !== null && value !== undefined) {
 | 
			
		||||
    if (value) {
 | 
			
		||||
        // Create or update the attribute.
 | 
			
		||||
        await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
 | 
			
		||||
    } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -95,15 +95,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Shows the delete confirmation screen
 | 
			
		||||
 *
 | 
			
		||||
 * @param branchIdsToDelete the list of branch IDs to delete.
 | 
			
		||||
 * @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox.
 | 
			
		||||
 * @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
 | 
			
		||||
 * @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
 | 
			
		||||
 */
 | 
			
		||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
 | 
			
		||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) {
 | 
			
		||||
    branchIdsToDelete = filterRootNote(branchIdsToDelete);
 | 
			
		||||
 | 
			
		||||
    if (branchIdsToDelete.length === 0) {
 | 
			
		||||
@@ -118,12 +110,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (moveToParent) {
 | 
			
		||||
        try {
 | 
			
		||||
            await activateParentNotePath();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
        }
 | 
			
		||||
    try {
 | 
			
		||||
        await activateParentNotePath();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const taskId = utils.randomString(10);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation
 | 
			
		||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
 | 
			
		||||
import { t } from "./i18n.js";
 | 
			
		||||
import type FNote from "../entities/fnote.js";
 | 
			
		||||
import toast from "./toast.js";
 | 
			
		||||
import { BulkAction } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
export const ACTION_GROUPS = [
 | 
			
		||||
const ACTION_GROUPS = [
 | 
			
		||||
    {
 | 
			
		||||
        title: t("bulk_actions.labels"),
 | 
			
		||||
        actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
 | 
			
		||||
@@ -91,17 +89,6 @@ function parseActions(note: FNote) {
 | 
			
		||||
        .filter((action) => !!action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function executeBulkActions(targetNoteIds: string[], actions: BulkAction[], includeDescendants = false) {
 | 
			
		||||
    await server.post("bulk-action/execute", {
 | 
			
		||||
        noteIds: targetNoteIds,
 | 
			
		||||
        includeDescendants,
 | 
			
		||||
        actions
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await ws.waitForMaxKnownEntityChangeId();
 | 
			
		||||
    toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    addAction,
 | 
			
		||||
    parseActions,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,295 +0,0 @@
 | 
			
		||||
import { ActionKeyboardShortcut } from "@triliumnext/commons";
 | 
			
		||||
import appContext, { type CommandNames } from "../components/app_context.js";
 | 
			
		||||
import type NoteTreeWidget from "../widgets/note_tree.js";
 | 
			
		||||
import { t, translationsInitializedPromise } from "./i18n.js";
 | 
			
		||||
import keyboardActions from "./keyboard_actions.js";
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
 | 
			
		||||
export interface CommandDefinition {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    shortcut?: string;
 | 
			
		||||
    commandName?: CommandNames;
 | 
			
		||||
    handler?: () => Promise<unknown> | null | undefined | void;
 | 
			
		||||
    aliases?: string[];
 | 
			
		||||
    source?: "manual" | "keyboard-action";
 | 
			
		||||
    /** Reference to the original keyboard action for scope checking. */
 | 
			
		||||
    keyboardAction?: ActionKeyboardShortcut;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CommandRegistry {
 | 
			
		||||
    private commands: Map<string, CommandDefinition> = new Map();
 | 
			
		||||
    private aliases: Map<string, string> = new Map();
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.loadCommands();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async loadCommands() {
 | 
			
		||||
        await translationsInitializedPromise;
 | 
			
		||||
        this.registerDefaultCommands();
 | 
			
		||||
        await this.loadKeyboardActionsAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private registerDefaultCommands() {
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "export-note",
 | 
			
		||||
            name: t("command_palette.export_note_title"),
 | 
			
		||||
            description: t("command_palette.export_note_description"),
 | 
			
		||||
            icon: "bx bx-export",
 | 
			
		||||
            handler: () => {
 | 
			
		||||
                const notePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
                if (notePath) {
 | 
			
		||||
                    appContext.triggerCommand("showExportDialog", {
 | 
			
		||||
                        notePath,
 | 
			
		||||
                        defaultType: "single"
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "show-attachments",
 | 
			
		||||
            name: t("command_palette.show_attachments_title"),
 | 
			
		||||
            description: t("command_palette.show_attachments_description"),
 | 
			
		||||
            icon: "bx bx-paperclip",
 | 
			
		||||
            handler: () => appContext.triggerCommand("showAttachments")
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Special search commands with custom logic
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "search-notes",
 | 
			
		||||
            name: t("command_palette.search_notes_title"),
 | 
			
		||||
            description: t("command_palette.search_notes_description"),
 | 
			
		||||
            icon: "bx bx-search",
 | 
			
		||||
            handler: () => appContext.triggerCommand("searchNotes", {})
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "search-in-subtree",
 | 
			
		||||
            name: t("command_palette.search_subtree_title"),
 | 
			
		||||
            description: t("command_palette.search_subtree_description"),
 | 
			
		||||
            icon: "bx bx-search-alt",
 | 
			
		||||
            handler: () => {
 | 
			
		||||
                const notePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
                if (notePath) {
 | 
			
		||||
                    appContext.triggerCommand("searchInSubtree", { notePath });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "show-search-history",
 | 
			
		||||
            name: t("command_palette.search_history_title"),
 | 
			
		||||
            description: t("command_palette.search_history_description"),
 | 
			
		||||
            icon: "bx bx-history",
 | 
			
		||||
            handler: () => appContext.triggerCommand("showSearchHistory")
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.register({
 | 
			
		||||
            id: "show-launch-bar",
 | 
			
		||||
            name: t("command_palette.configure_launch_bar_title"),
 | 
			
		||||
            description: t("command_palette.configure_launch_bar_description"),
 | 
			
		||||
            icon: "bx bx-sidebar",
 | 
			
		||||
            handler: () => appContext.triggerCommand("showLaunchBarSubtree")
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async loadKeyboardActionsAsync() {
 | 
			
		||||
        try {
 | 
			
		||||
            const actions = await keyboardActions.getActions();
 | 
			
		||||
            this.registerKeyboardActions(actions);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error("Failed to load keyboard actions:", error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private registerKeyboardActions(actions: ActionKeyboardShortcut[]) {
 | 
			
		||||
        for (const action of actions) {
 | 
			
		||||
            // Skip actions that we've already manually registered
 | 
			
		||||
            if (this.commands.has(action.actionName)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Skip actions that don't have a description (likely separators)
 | 
			
		||||
            if (!action.description) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Skip Electron-only actions if not in Electron environment
 | 
			
		||||
            if (action.isElectronOnly && !utils.isElectron()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Skip actions that should not appear in the command palette
 | 
			
		||||
            if (action.ignoreFromCommandPalette) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get the primary shortcut (first one in the list)
 | 
			
		||||
            const primaryShortcut = action.effectiveShortcuts?.[0];
 | 
			
		||||
 | 
			
		||||
            let name = action.friendlyName;
 | 
			
		||||
            if (action.scope === "note-tree") {
 | 
			
		||||
                name = t("command_palette.tree-action-name", { name: action.friendlyName });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create a command definition from the keyboard action
 | 
			
		||||
            const commandDef: CommandDefinition = {
 | 
			
		||||
                id: action.actionName,
 | 
			
		||||
                name,
 | 
			
		||||
                description: action.description,
 | 
			
		||||
                icon: action.iconClass,
 | 
			
		||||
                shortcut: primaryShortcut ? this.formatShortcut(primaryShortcut) : undefined,
 | 
			
		||||
                commandName: action.actionName as CommandNames,
 | 
			
		||||
                source: "keyboard-action",
 | 
			
		||||
                keyboardAction: action
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            this.register(commandDef);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private formatShortcut(shortcut: string): string {
 | 
			
		||||
        // Convert electron accelerator format to display format
 | 
			
		||||
        return shortcut
 | 
			
		||||
            .replace(/CommandOrControl/g, 'Ctrl')
 | 
			
		||||
            .replace(/\+/g, ' + ');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    register(command: CommandDefinition) {
 | 
			
		||||
        this.commands.set(command.id, command);
 | 
			
		||||
 | 
			
		||||
        // Register aliases
 | 
			
		||||
        if (command.aliases) {
 | 
			
		||||
            for (const alias of command.aliases) {
 | 
			
		||||
                this.aliases.set(alias.toLowerCase(), command.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCommand(id: string): CommandDefinition | undefined {
 | 
			
		||||
        return this.commands.get(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAllCommands(): CommandDefinition[] {
 | 
			
		||||
        const commands = Array.from(this.commands.values());
 | 
			
		||||
 | 
			
		||||
        // Sort commands by name
 | 
			
		||||
        commands.sort((a, b) => a.name.localeCompare(b.name));
 | 
			
		||||
 | 
			
		||||
        return commands;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    searchCommands(query: string): CommandDefinition[] {
 | 
			
		||||
        const normalizedQuery = query.toLowerCase();
 | 
			
		||||
        const results: { command: CommandDefinition; score: number }[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const command of this.commands.values()) {
 | 
			
		||||
            let score = 0;
 | 
			
		||||
 | 
			
		||||
            // Exact match on name
 | 
			
		||||
            if (command.name.toLowerCase() === normalizedQuery) {
 | 
			
		||||
                score = 100;
 | 
			
		||||
            }
 | 
			
		||||
            // Name starts with query
 | 
			
		||||
            else if (command.name.toLowerCase().startsWith(normalizedQuery)) {
 | 
			
		||||
                score = 80;
 | 
			
		||||
            }
 | 
			
		||||
            // Name contains query
 | 
			
		||||
            else if (command.name.toLowerCase().includes(normalizedQuery)) {
 | 
			
		||||
                score = 60;
 | 
			
		||||
            }
 | 
			
		||||
            // Description contains query
 | 
			
		||||
            else if (command.description?.toLowerCase().includes(normalizedQuery)) {
 | 
			
		||||
                score = 40;
 | 
			
		||||
            }
 | 
			
		||||
            // Check aliases
 | 
			
		||||
            else if (command.aliases?.some(alias => alias.toLowerCase().includes(normalizedQuery))) {
 | 
			
		||||
                score = 50;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (score > 0) {
 | 
			
		||||
                results.push({ command, score });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sort by score (highest first) and then by name
 | 
			
		||||
        results.sort((a, b) => {
 | 
			
		||||
            if (a.score !== b.score) {
 | 
			
		||||
                return b.score - a.score;
 | 
			
		||||
            }
 | 
			
		||||
            return a.command.name.localeCompare(b.command.name);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return results.map(r => r.command);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async executeCommand(commandId: string) {
 | 
			
		||||
        const command = this.getCommand(commandId);
 | 
			
		||||
        if (!command) {
 | 
			
		||||
            console.error(`Command not found: ${commandId}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Execute custom handler if provided
 | 
			
		||||
        if (command.handler) {
 | 
			
		||||
            await command.handler();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle keyboard action with scope-aware execution
 | 
			
		||||
        if (command.keyboardAction && command.commandName) {
 | 
			
		||||
            if (command.keyboardAction.scope === "note-tree") {
 | 
			
		||||
                this.executeWithNoteTreeFocus(command.commandName);
 | 
			
		||||
            } else if (command.keyboardAction.scope === "text-detail") {
 | 
			
		||||
                this.executeWithTextDetail(command.commandName);
 | 
			
		||||
            } else {
 | 
			
		||||
                appContext.triggerCommand(command.commandName, {
 | 
			
		||||
                    ntxId: appContext.tabManager.activeNtxId
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fallback for commands without keyboard action reference
 | 
			
		||||
        if (command.commandName) {
 | 
			
		||||
            appContext.triggerCommand(command.commandName, {
 | 
			
		||||
                ntxId: appContext.tabManager.activeNtxId
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.error(`Command ${commandId} has no handler or commandName`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private executeWithNoteTreeFocus(actionName: CommandNames) {
 | 
			
		||||
        const tree = document.querySelector(".tree-wrapper") as HTMLElement;
 | 
			
		||||
        if (!tree) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const treeComponent = appContext.getComponentByEl(tree) as NoteTreeWidget;
 | 
			
		||||
        const activeNode = treeComponent.getActiveNode();
 | 
			
		||||
        treeComponent.triggerCommand(actionName, {
 | 
			
		||||
            ntxId: appContext.tabManager.activeNtxId,
 | 
			
		||||
            node: activeNode
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async executeWithTextDetail(actionName: CommandNames) {
 | 
			
		||||
        const typeWidget = await appContext.tabManager.getActiveContext()?.getTypeWidget();
 | 
			
		||||
        if (!typeWidget) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        typeWidget.triggerCommand(actionName, {
 | 
			
		||||
            ntxId: appContext.tabManager.activeNtxId
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const commandRegistry = new CommandRegistry();
 | 
			
		||||
export default commandRegistry;
 | 
			
		||||
@@ -65,9 +65,6 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA
 | 
			
		||||
 | 
			
		||||
        $renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
 | 
			
		||||
    } else if (entity instanceof FNote) {
 | 
			
		||||
        $renderedContent
 | 
			
		||||
            .css("display", "flex")
 | 
			
		||||
            .css("flex-direction", "column");
 | 
			
		||||
        $renderedContent.append(
 | 
			
		||||
            $("<div>")
 | 
			
		||||
                .css("display", "flex")
 | 
			
		||||
@@ -75,33 +72,8 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA
 | 
			
		||||
                .css("align-items", "center")
 | 
			
		||||
                .css("height", "100%")
 | 
			
		||||
                .css("font-size", "500%")
 | 
			
		||||
                .css("flex-grow", "1")
 | 
			
		||||
                .append($("<span>").addClass(entity.getIcon()))
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
 | 
			
		||||
            const $footer = $("<footer>")
 | 
			
		||||
                .addClass("webview-footer");
 | 
			
		||||
            const $openButton = $(`
 | 
			
		||||
                <button class="file-open btn btn-primary" type="button">
 | 
			
		||||
                    <span class="bx bx-link-external"></span>
 | 
			
		||||
                    ${t("content_renderer.open_externally")}
 | 
			
		||||
                </button>
 | 
			
		||||
            `)
 | 
			
		||||
                .appendTo($footer)
 | 
			
		||||
                .on("click", () => {
 | 
			
		||||
                    const webViewSrc = entity.getLabelValue("webViewSrc");
 | 
			
		||||
                    if (webViewSrc) {
 | 
			
		||||
                        if (utils.isElectron()) {
 | 
			
		||||
                            const electron = utils.dynamicRequire("electron");
 | 
			
		||||
                            electron.shell.openExternal(webViewSrc);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            window.open(webViewSrc, '_blank', 'noopener,noreferrer');
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            $footer.appendTo($renderedContent);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (entity instanceof FNote) {
 | 
			
		||||
 
 | 
			
		||||
@@ -41,14 +41,8 @@ async function info(message: string) {
 | 
			
		||||
    return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Displays a confirmation dialog with the given message.
 | 
			
		||||
 *
 | 
			
		||||
 * @param message the message to display in the dialog.
 | 
			
		||||
 * @returns A promise that resolves to true if the user confirmed, false otherwise.
 | 
			
		||||
 */
 | 
			
		||||
async function confirm(message: string) {
 | 
			
		||||
    return new Promise<boolean>((res) =>
 | 
			
		||||
    return new Promise((res) =>
 | 
			
		||||
        appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
 | 
			
		||||
            message,
 | 
			
		||||
            callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,6 @@ function getUrl(docNameValue: string, language: string) {
 | 
			
		||||
    // Cannot have spaces in the URL due to how JQuery.load works.
 | 
			
		||||
    docNameValue = docNameValue.replaceAll(" ", "%20");
 | 
			
		||||
 | 
			
		||||
    const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
 | 
			
		||||
    const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath;
 | 
			
		||||
    return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,10 +35,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
 | 
			
		||||
                loadResults.addOption(attributeEntity.name);
 | 
			
		||||
            } else if (ec.entityName === "attachments") {
 | 
			
		||||
                processAttachment(loadResults, ec);
 | 
			
		||||
            } else if (ec.entityName === "blobs") {
 | 
			
		||||
            } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
 | 
			
		||||
                // NOOP - these entities are handled at the backend level and don't require frontend processing
 | 
			
		||||
            } else if (ec.entityName === "etapi_tokens") {
 | 
			
		||||
                loadResults.hasEtapiTokenChanges = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error(`Unknown entityName '${ec.entityName}'`);
 | 
			
		||||
            }
 | 
			
		||||
@@ -79,7 +77,9 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
 | 
			
		||||
            noteAttributeCache.invalidate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const appContext = (await import("../components/app_context.js")).default;
 | 
			
		||||
        // TODO: Remove after porting the file
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        const appContext = (await import("../components/app_context.js")).default as any;
 | 
			
		||||
        await appContext.triggerEvent("entitiesReloaded", { loadResults });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,21 +3,14 @@ import i18next from "i18next";
 | 
			
		||||
import i18nextHttpBackend from "i18next-http-backend";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import type { Locale } from "@triliumnext/commons";
 | 
			
		||||
import { initReactI18next } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
let locales: Locale[] | null;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A deferred promise that resolves when translations are initialized.
 | 
			
		||||
 */
 | 
			
		||||
export let translationsInitializedPromise = $.Deferred();
 | 
			
		||||
 | 
			
		||||
export async function initLocale() {
 | 
			
		||||
    const locale = (options.get("locale") as string) || "en";
 | 
			
		||||
 | 
			
		||||
    locales = await server.get<Locale[]>("options/locales");
 | 
			
		||||
 | 
			
		||||
    i18next.use(initReactI18next);
 | 
			
		||||
    await i18next.use(i18nextHttpBackend).init({
 | 
			
		||||
        lng: locale,
 | 
			
		||||
        fallbackLng: "en",
 | 
			
		||||
@@ -26,8 +19,6 @@ export async function initLocale() {
 | 
			
		||||
        },
 | 
			
		||||
        returnEmptyString: false
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    translationsInitializedPromise.resolve();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAvailableLocales() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { t } from "./i18n.js";
 | 
			
		||||
import toastService, { showError } from "./toast.js";
 | 
			
		||||
 | 
			
		||||
export function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
 | 
			
		||||
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
 | 
			
		||||
    try {
 | 
			
		||||
        $imageWrapper.attr("contenteditable", "true");
 | 
			
		||||
        selectImage($imageWrapper.get(0));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
import { NoteType } from "@triliumnext/commons";
 | 
			
		||||
import { ViewTypeOptions } from "./note_list_renderer";
 | 
			
		||||
import FNote from "../entities/fnote";
 | 
			
		||||
 | 
			
		||||
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
 | 
			
		||||
    canvas: null,
 | 
			
		||||
    code: null,
 | 
			
		||||
    contentWidget: null,
 | 
			
		||||
    doc: null,
 | 
			
		||||
    file: null,
 | 
			
		||||
    image: null,
 | 
			
		||||
    launcher: null,
 | 
			
		||||
    mermaid: null,
 | 
			
		||||
    mindMap: null,
 | 
			
		||||
    noteMap: null,
 | 
			
		||||
    relationMap: null,
 | 
			
		||||
    render: null,
 | 
			
		||||
    search: null,
 | 
			
		||||
    text: null,
 | 
			
		||||
    webView: null,
 | 
			
		||||
    aiChat: null
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
 | 
			
		||||
    list: "mULW0Q3VojwY",
 | 
			
		||||
    grid: "8QqnMzx393bx",
 | 
			
		||||
    calendar: "xWbu3jpNWapp",
 | 
			
		||||
    table: "2FvYrpmOXm29",
 | 
			
		||||
    geoMap: "81SGnPGMk7Xc",
 | 
			
		||||
    board: "CtBQqbwXDx1w"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
 | 
			
		||||
    if (note && note.type !== "book" && byNoteType[note.type]) {
 | 
			
		||||
        return byNoteType[note.type];
 | 
			
		||||
    } else if (note?.hasLabel("calendarRoot")) {
 | 
			
		||||
        return "l0tKav7yLHGF";
 | 
			
		||||
    } else if (note?.hasLabel("textSnippet")) {
 | 
			
		||||
        return "pwc194wlRzcH";
 | 
			
		||||
    } else if (note && note.type === "book") {
 | 
			
		||||
        return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,15 +2,21 @@ import server from "./server.js";
 | 
			
		||||
import appContext, { type CommandNames } from "../components/app_context.js";
 | 
			
		||||
import shortcutService from "./shortcuts.js";
 | 
			
		||||
import type Component from "../components/component.js";
 | 
			
		||||
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
const keyboardActionRepo: Record<string, ActionKeyboardShortcut> = {};
 | 
			
		||||
const keyboardActionRepo: Record<string, Action> = {};
 | 
			
		||||
 | 
			
		||||
const keyboardActionsLoaded = server.get<ActionKeyboardShortcut[]>("keyboard-actions").then((actions) => {
 | 
			
		||||
// TODO: Deduplicate with server.
 | 
			
		||||
export interface Action {
 | 
			
		||||
    actionName: CommandNames;
 | 
			
		||||
    effectiveShortcuts: string[];
 | 
			
		||||
    scope: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const keyboardActionsLoaded = server.get<Action[]>("keyboard-actions").then((actions) => {
 | 
			
		||||
    actions = actions.filter((a) => !!a.actionName); // filter out separators
 | 
			
		||||
 | 
			
		||||
    for (const action of actions) {
 | 
			
		||||
        action.effectiveShortcuts = (action.effectiveShortcuts ?? []).filter((shortcut) => !shortcut.startsWith("global:"));
 | 
			
		||||
        action.effectiveShortcuts = action.effectiveShortcuts.filter((shortcut) => !shortcut.startsWith("global:"));
 | 
			
		||||
 | 
			
		||||
        keyboardActionRepo[action.actionName] = action;
 | 
			
		||||
    }
 | 
			
		||||
@@ -32,7 +38,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
 | 
			
		||||
    const actions = await getActionsForScope(scope);
 | 
			
		||||
 | 
			
		||||
    for (const action of actions) {
 | 
			
		||||
        for (const shortcut of action.effectiveShortcuts ?? []) {
 | 
			
		||||
        for (const shortcut of action.effectiveShortcuts) {
 | 
			
		||||
            shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -40,7 +46,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
 | 
			
		||||
 | 
			
		||||
getActionsForScope("window").then((actions) => {
 | 
			
		||||
    for (const action of actions) {
 | 
			
		||||
        for (const shortcut of action.effectiveShortcuts ?? []) {
 | 
			
		||||
        for (const shortcut of action.effectiveShortcuts) {
 | 
			
		||||
            shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -62,10 +68,6 @@ async function getAction(actionName: string, silent = false) {
 | 
			
		||||
    return action;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getActionSync(actionName: string) {
 | 
			
		||||
    return keyboardActionRepo[actionName];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
 | 
			
		||||
    //@ts-ignore
 | 
			
		||||
    //TODO: each() does not support async callbacks.
 | 
			
		||||
@@ -78,7 +80,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
 | 
			
		||||
        const action = await getAction(actionName, true);
 | 
			
		||||
 | 
			
		||||
        if (action) {
 | 
			
		||||
            const keyboardActions = (action.effectiveShortcuts ?? []).join(", ");
 | 
			
		||||
            const keyboardActions = action.effectiveShortcuts.join(", ");
 | 
			
		||||
 | 
			
		||||
            if (keyboardActions || $(el).text() !== "not set") {
 | 
			
		||||
                $(el).text(keyboardActions);
 | 
			
		||||
@@ -97,7 +99,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
 | 
			
		||||
 | 
			
		||||
        if (action) {
 | 
			
		||||
            const title = $(el).attr("title");
 | 
			
		||||
            const shortcuts = (action.effectiveShortcuts ?? []).join(", ");
 | 
			
		||||
            const shortcuts = action.effectiveShortcuts.join(", ");
 | 
			
		||||
 | 
			
		||||
            if (title?.includes(shortcuts)) {
 | 
			
		||||
                return;
 | 
			
		||||
 
 | 
			
		||||
@@ -316,7 +316,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
 | 
			
		||||
    const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
        if (isLeftClick && openInPopup) {
 | 
			
		||||
        if (openInPopup) {
 | 
			
		||||
            appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
 | 
			
		||||
        } else if (openInNewWindow) {
 | 
			
		||||
            appContext.triggerCommand("openInWindow", { notePath, viewScope });
 | 
			
		||||
@@ -405,7 +405,7 @@ function linkContextMenu(e: PointerEvent) {
 | 
			
		||||
    linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
 | 
			
		||||
export async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
 | 
			
		||||
    const $link = $el[0].tagName === "A" ? $el : $el.find("a");
 | 
			
		||||
 | 
			
		||||
    href = href || $link.attr("href");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { AttachmentRow, EtapiTokenRow } from "@triliumnext/commons";
 | 
			
		||||
import type { AttachmentRow } from "@triliumnext/commons";
 | 
			
		||||
import type { AttributeType } from "../entities/fattribute.js";
 | 
			
		||||
import type { EntityChange } from "../server_types.js";
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +53,6 @@ type EntityRowMappings = {
 | 
			
		||||
    options: OptionRow;
 | 
			
		||||
    revisions: RevisionRow;
 | 
			
		||||
    note_reordering: NoteReorderingRow;
 | 
			
		||||
    etapi_tokens: EtapiTokenRow;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type EntityRowNames = keyof EntityRowMappings;
 | 
			
		||||
@@ -69,7 +68,6 @@ export default class LoadResults {
 | 
			
		||||
    private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
 | 
			
		||||
    private optionNames: string[];
 | 
			
		||||
    private attachmentRows: AttachmentRow[];
 | 
			
		||||
    public hasEtapiTokenChanges: boolean = false;
 | 
			
		||||
 | 
			
		||||
    constructor(entityChanges: EntityChange[]) {
 | 
			
		||||
        const entities: Record<string, Record<string, any>> = {};
 | 
			
		||||
@@ -217,8 +215,7 @@ export default class LoadResults {
 | 
			
		||||
            this.revisionRows.length === 0 &&
 | 
			
		||||
            this.contentNoteIdToComponentId.length === 0 &&
 | 
			
		||||
            this.optionNames.length === 0 &&
 | 
			
		||||
            this.attachmentRows.length === 0 &&
 | 
			
		||||
            !this.hasEtapiTokenChanges
 | 
			
		||||
            this.attachmentRows.length === 0
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import appContext from "../components/app_context.js";
 | 
			
		||||
import noteCreateService from "./note_create.js";
 | 
			
		||||
import froca from "./froca.js";
 | 
			
		||||
import { t } from "./i18n.js";
 | 
			
		||||
import commandRegistry from "./command_registry.js";
 | 
			
		||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
 | 
			
		||||
 | 
			
		||||
// this key needs to have this value, so it's hit by the tooltip
 | 
			
		||||
@@ -30,28 +29,18 @@ export interface Suggestion {
 | 
			
		||||
    notePathTitle?: string;
 | 
			
		||||
    notePath?: string;
 | 
			
		||||
    highlightedNotePathTitle?: string;
 | 
			
		||||
    action?: string | "create-note" | "search-notes" | "external-link" | "command";
 | 
			
		||||
    action?: string | "create-note" | "search-notes" | "external-link";
 | 
			
		||||
    parentNoteId?: string;
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    commandId?: string;
 | 
			
		||||
    commandDescription?: string;
 | 
			
		||||
    commandShortcut?: string;
 | 
			
		||||
    attributeSnippet?: string;
 | 
			
		||||
    highlightedAttributeSnippet?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Options {
 | 
			
		||||
    container?: HTMLElement | null;
 | 
			
		||||
interface Options {
 | 
			
		||||
    container?: HTMLElement;
 | 
			
		||||
    fastSearch?: boolean;
 | 
			
		||||
    allowCreatingNotes?: boolean;
 | 
			
		||||
    allowJumpToSearchNotes?: boolean;
 | 
			
		||||
    allowExternalLinks?: boolean;
 | 
			
		||||
    /** If set, hides the right-side button corresponding to go to selected note. */
 | 
			
		||||
    hideGoToSelectedNoteButton?: boolean;
 | 
			
		||||
    /** If set, hides all right-side buttons in the autocomplete dropdown */
 | 
			
		||||
    hideAllButtons?: boolean;
 | 
			
		||||
    /** If set, enables command palette mode */
 | 
			
		||||
    isCommandPalette?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function autocompleteSourceForCKEditor(queryText: string) {
 | 
			
		||||
@@ -81,31 +70,6 @@ async function autocompleteSourceForCKEditor(queryText: string) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
 | 
			
		||||
    // Check if we're in command mode
 | 
			
		||||
    if (options.isCommandPalette && term.startsWith(">")) {
 | 
			
		||||
        const commandQuery = term.substring(1).trim();
 | 
			
		||||
 | 
			
		||||
        // Get commands (all if no query, filtered if query provided)
 | 
			
		||||
        const commands = commandQuery.length === 0
 | 
			
		||||
            ? commandRegistry.getAllCommands()
 | 
			
		||||
            : commandRegistry.searchCommands(commandQuery);
 | 
			
		||||
 | 
			
		||||
        // Convert commands to suggestions
 | 
			
		||||
        const commandSuggestions: Suggestion[] = commands.map(cmd => ({
 | 
			
		||||
            action: "command",
 | 
			
		||||
            commandId: cmd.id,
 | 
			
		||||
            noteTitle: cmd.name,
 | 
			
		||||
            notePathTitle: `>${cmd.name}`,
 | 
			
		||||
            highlightedNotePathTitle: cmd.name,
 | 
			
		||||
            commandDescription: cmd.description,
 | 
			
		||||
            commandShortcut: cmd.shortcut,
 | 
			
		||||
            icon: cmd.icon
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        cb(commandSuggestions);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fastSearch = options.fastSearch === false ? false : true;
 | 
			
		||||
    if (fastSearch === false) {
 | 
			
		||||
        if (term.trim().length === 0) {
 | 
			
		||||
@@ -179,12 +143,6 @@ function showRecentNotes($el: JQuery<HTMLElement>) {
 | 
			
		||||
    $el.trigger("focus");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showAllCommands($el: JQuery<HTMLElement>) {
 | 
			
		||||
    searchDelay = 0;
 | 
			
		||||
    $el.setSelectedNotePath("");
 | 
			
		||||
    $el.autocomplete("val", ">").autocomplete("open");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
 | 
			
		||||
    const searchString = $el.autocomplete("val") as unknown as string;
 | 
			
		||||
    if (options.fastSearch === false || searchString?.trim().length === 0) {
 | 
			
		||||
@@ -232,11 +190,9 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
 | 
			
		||||
 | 
			
		||||
    const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
 | 
			
		||||
 | 
			
		||||
    if (!options.hideAllButtons) {
 | 
			
		||||
        $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
 | 
			
		||||
    }
 | 
			
		||||
    $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
 | 
			
		||||
 | 
			
		||||
    if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) {
 | 
			
		||||
    if (!options.hideGoToSelectedNoteButton) {
 | 
			
		||||
        $el.after($goToSelectedNoteButton);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -309,50 +265,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
 | 
			
		||||
                },
 | 
			
		||||
                displayKey: "notePathTitle",
 | 
			
		||||
                templates: {
 | 
			
		||||
                    suggestion: (suggestion) => {
 | 
			
		||||
                        if (suggestion.action === "command") {
 | 
			
		||||
                            let html = `<div class="command-suggestion">`;
 | 
			
		||||
                            html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
 | 
			
		||||
                            html += `<div class="command-content">`;
 | 
			
		||||
                            html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
 | 
			
		||||
                            if (suggestion.commandDescription) {
 | 
			
		||||
                                html += `<div class="command-description">${suggestion.commandDescription}</div>`;
 | 
			
		||||
                            }
 | 
			
		||||
                            html += `</div>`;
 | 
			
		||||
                            if (suggestion.commandShortcut) {
 | 
			
		||||
                                html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`;
 | 
			
		||||
                            }
 | 
			
		||||
                            html += '</div>';
 | 
			
		||||
                            return html;
 | 
			
		||||
                        }
 | 
			
		||||
                        // Add special class for search-notes action
 | 
			
		||||
                        const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
 | 
			
		||||
 | 
			
		||||
                        // Choose appropriate icon based on action
 | 
			
		||||
                        let iconClass = suggestion.icon ?? "bx bx-note";
 | 
			
		||||
                        if (suggestion.action === "search-notes") {
 | 
			
		||||
                            iconClass = "bx bx-search";
 | 
			
		||||
                        } else if (suggestion.action === "create-note") {
 | 
			
		||||
                            iconClass = "bx bx-plus";
 | 
			
		||||
                        } else if (suggestion.action === "external-link") {
 | 
			
		||||
                            iconClass = "bx bx-link-external";
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Simplified HTML structure without nested divs
 | 
			
		||||
                        let html = `<div class="note-suggestion ${actionClass}">`;
 | 
			
		||||
                        html += `<span class="icon ${iconClass}"></span>`;
 | 
			
		||||
                        html += `<span class="text">`;
 | 
			
		||||
                        html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
 | 
			
		||||
 | 
			
		||||
                        // Add attribute snippet inline if available
 | 
			
		||||
                        if (suggestion.highlightedAttributeSnippet) {
 | 
			
		||||
                            html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        html += `</span>`;
 | 
			
		||||
                        html += `</div>`;
 | 
			
		||||
                        return html;
 | 
			
		||||
                    }
 | 
			
		||||
                    suggestion: (suggestion) => `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`
 | 
			
		||||
                },
 | 
			
		||||
                // we can't cache identical searches because notes can be created / renamed, new recent notes can be added
 | 
			
		||||
                cache: false
 | 
			
		||||
@@ -362,12 +275,6 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
 | 
			
		||||
 | 
			
		||||
    // TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
 | 
			
		||||
    ($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
 | 
			
		||||
        if (suggestion.action === "command") {
 | 
			
		||||
            $el.autocomplete("close");
 | 
			
		||||
            $el.trigger("autocomplete:commandselected", [suggestion]);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (suggestion.action === "external-link") {
 | 
			
		||||
            $el.setSelectedNotePath(null);
 | 
			
		||||
            $el.setSelectedExternalLink(suggestion.externalLink);
 | 
			
		||||
@@ -480,26 +387,10 @@ function init() {
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convenience function which triggers the display of recent notes in the autocomplete input and focuses it.
 | 
			
		||||
 *
 | 
			
		||||
 * @param inputElement - The input element to trigger recent notes on.
 | 
			
		||||
 */
 | 
			
		||||
export function triggerRecentNotes(inputElement: HTMLInputElement | null | undefined) {
 | 
			
		||||
    if (!inputElement) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const $el = $(inputElement);
 | 
			
		||||
    showRecentNotes($el);
 | 
			
		||||
    $el.trigger("focus").trigger("select");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    autocompleteSourceForCKEditor,
 | 
			
		||||
    initNoteAutocomplete,
 | 
			
		||||
    showRecentNotes,
 | 
			
		||||
    showAllCommands,
 | 
			
		||||
    setText,
 | 
			
		||||
    init
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js";
 | 
			
		||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
 | 
			
		||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
 | 
			
		||||
 | 
			
		||||
export interface CreateNoteOpts {
 | 
			
		||||
interface CreateNoteOpts {
 | 
			
		||||
    isProtected?: boolean;
 | 
			
		||||
    saveSelection?: boolean;
 | 
			
		||||
    title?: string | null;
 | 
			
		||||
@@ -109,6 +109,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
 | 
			
		||||
 | 
			
		||||
async function chooseNoteType() {
 | 
			
		||||
    return new Promise<ChooseNoteTypeResponse>((res) => {
 | 
			
		||||
        // TODO: Remove ignore after callback for chooseNoteType is defined in app_context.ts
 | 
			
		||||
        //@ts-ignore
 | 
			
		||||
        appContext.triggerCommand("chooseNoteType", { callback: res });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import type FNote from "../entities/fnote.js";
 | 
			
		||||
import BoardView from "../widgets/view_widgets/board_view/index.js";
 | 
			
		||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
 | 
			
		||||
import GeoView from "../widgets/view_widgets/geo_view/index.js";
 | 
			
		||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
 | 
			
		||||
@@ -7,25 +6,39 @@ import TableView from "../widgets/view_widgets/table_view/index.js";
 | 
			
		||||
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
 | 
			
		||||
import type ViewMode from "../widgets/view_widgets/view_mode.js";
 | 
			
		||||
 | 
			
		||||
const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
 | 
			
		||||
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
 | 
			
		||||
export type ViewTypeOptions = typeof allViewTypes[number];
 | 
			
		||||
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap";
 | 
			
		||||
 | 
			
		||||
export default class NoteListRenderer {
 | 
			
		||||
 | 
			
		||||
    private viewType: ViewTypeOptions;
 | 
			
		||||
    private args: ArgsWithoutNoteId;
 | 
			
		||||
    public viewMode?: ViewMode<any>;
 | 
			
		||||
    public viewMode: ViewMode<any> | null;
 | 
			
		||||
 | 
			
		||||
    constructor(args: ArgsWithoutNoteId) {
 | 
			
		||||
        this.args = args;
 | 
			
		||||
    constructor(args: ViewModeArgs) {
 | 
			
		||||
        this.viewType = this.#getViewType(args.parentNote);
 | 
			
		||||
 | 
			
		||||
        switch (this.viewType) {
 | 
			
		||||
            case "list":
 | 
			
		||||
            case "grid":
 | 
			
		||||
                this.viewMode = new ListOrGridView(this.viewType, args);
 | 
			
		||||
                break;
 | 
			
		||||
            case "calendar":
 | 
			
		||||
                this.viewMode = new CalendarView(args);
 | 
			
		||||
                break;
 | 
			
		||||
            case "table":
 | 
			
		||||
                this.viewMode = new TableView(args);
 | 
			
		||||
                break;
 | 
			
		||||
            case "geoMap":
 | 
			
		||||
                this.viewMode = new GeoView(args);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                this.viewMode = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #getViewType(parentNote: FNote): ViewTypeOptions {
 | 
			
		||||
        const viewType = parentNote.getLabelValue("viewType");
 | 
			
		||||
 | 
			
		||||
        if (!(allViewTypes as readonly string[]).includes(viewType || "")) {
 | 
			
		||||
        if (!["list", "grid", "calendar", "table", "geoMap"].includes(viewType || "")) {
 | 
			
		||||
            // when not explicitly set, decide based on the note type
 | 
			
		||||
            return parentNote.type === "search" ? "list" : "grid";
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -34,38 +47,15 @@ export default class NoteListRenderer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get isFullHeight() {
 | 
			
		||||
        switch (this.viewType) {
 | 
			
		||||
            case "list":
 | 
			
		||||
            case "grid":
 | 
			
		||||
                return false;
 | 
			
		||||
            default:
 | 
			
		||||
                return true;
 | 
			
		||||
        }
 | 
			
		||||
        return this.viewMode?.isFullHeight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async renderList() {
 | 
			
		||||
        const args = this.args;
 | 
			
		||||
        const viewMode = this.#buildViewMode(args);
 | 
			
		||||
        this.viewMode = viewMode;
 | 
			
		||||
        await viewMode.beforeRender();
 | 
			
		||||
        return await viewMode.renderList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #buildViewMode(args: ViewModeArgs) {
 | 
			
		||||
        switch (this.viewType) {
 | 
			
		||||
            case "calendar":
 | 
			
		||||
                return new CalendarView(args);
 | 
			
		||||
            case "table":
 | 
			
		||||
                return new TableView(args);
 | 
			
		||||
            case "geoMap":
 | 
			
		||||
                return new GeoView(args);
 | 
			
		||||
            case "board":
 | 
			
		||||
                return new BoardView(args);
 | 
			
		||||
            case "list":
 | 
			
		||||
            case "grid":
 | 
			
		||||
            default:
 | 
			
		||||
                return new ListOrGridView(this.viewType, args);
 | 
			
		||||
        if (!this.viewMode) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await this.viewMode.renderList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,8 @@ let openTooltipElements: JQuery<HTMLElement>[] = [];
 | 
			
		||||
let dismissTimer: ReturnType<typeof setTimeout>;
 | 
			
		||||
 | 
			
		||||
function setupGlobalTooltip() {
 | 
			
		||||
    $(document).on("mouseenter", "a:not(.no-tooltip-preview)", mouseEnterHandler);
 | 
			
		||||
    $(document).on("mouseenter", "[data-href]:not(.no-tooltip-preview)", mouseEnterHandler);
 | 
			
		||||
    $(document).on("mouseenter", "a", mouseEnterHandler);
 | 
			
		||||
    $(document).on("mouseenter", "[data-href]", mouseEnterHandler);
 | 
			
		||||
 | 
			
		||||
    // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
 | 
			
		||||
    $(document).on("click", (e) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ function download(url: string) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function downloadFileNote(noteId: string) {
 | 
			
		||||
function downloadFileNote(noteId: string) {
 | 
			
		||||
    const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
 | 
			
		||||
 | 
			
		||||
    download(url);
 | 
			
		||||
@@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
 | 
			
		||||
const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime);
 | 
			
		||||
const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime);
 | 
			
		||||
 | 
			
		||||
function getHost() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,7 @@
 | 
			
		||||
import { OptionNames } from "@triliumnext/commons";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import { isShare } from "./utils.js";
 | 
			
		||||
 | 
			
		||||
export type OptionValue = number | string;
 | 
			
		||||
type OptionValue = number | string;
 | 
			
		||||
 | 
			
		||||
class Options {
 | 
			
		||||
    initializedPromise: Promise<void>;
 | 
			
		||||
@@ -77,14 +76,6 @@ class Options {
 | 
			
		||||
        await server.put(`options`, payload);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves multiple options at once, by supplying a record where the keys are the option names and the values represent the stringified value to set.
 | 
			
		||||
     * @param newValues the record of keys and values.
 | 
			
		||||
     */
 | 
			
		||||
    async saveMany<T extends OptionNames>(newValues: Record<T, OptionValue>) {
 | 
			
		||||
        await server.put<void>("options", newValues);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async toggle(key: string) {
 | 
			
		||||
        await this.save(key, (!this.is(key)).toString());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
 | 
			
		||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
 | 
			
		||||
type Multiplicity = "single" | "multi";
 | 
			
		||||
 | 
			
		||||
export interface DefinitionObject {
 | 
			
		||||
@@ -17,7 +17,7 @@ function parse(value: string) {
 | 
			
		||||
    for (const token of tokens) {
 | 
			
		||||
        if (token === "promoted") {
 | 
			
		||||
            defObj.isPromoted = true;
 | 
			
		||||
        } else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
 | 
			
		||||
        } else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
 | 
			
		||||
            defObj.labelType = token as LabelType;
 | 
			
		||||
        } else if (["single", "multi"].includes(token)) {
 | 
			
		||||
            defObj.multiplicity = token as Multiplicity;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,6 @@ let leftInstance: ReturnType<typeof Split> | null;
 | 
			
		||||
let rightPaneWidth: number;
 | 
			
		||||
let rightInstance: ReturnType<typeof Split> | null;
 | 
			
		||||
 | 
			
		||||
const noteSplitMap = new Map<string[], ReturnType<typeof Split> | undefined>(); // key: a group of ntxIds, value: the corresponding Split instance
 | 
			
		||||
const noteSplitRafMap = new Map<string[], number>();
 | 
			
		||||
let splitNoteContainer: HTMLElement | undefined;
 | 
			
		||||
 | 
			
		||||
function setupLeftPaneResizer(leftPaneVisible: boolean) {
 | 
			
		||||
    if (leftInstance) {
 | 
			
		||||
        leftInstance.destroy();
 | 
			
		||||
@@ -87,86 +83,7 @@ function setupRightPaneResizer() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function findKeyByNtxId(ntxId: string): string[] | undefined {
 | 
			
		||||
    // Find the corresponding key in noteSplitMap based on ntxId
 | 
			
		||||
    for (const key of noteSplitMap.keys()) {
 | 
			
		||||
        if (key.includes(ntxId)) return key;
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupNoteSplitResizer(ntxIds: string[]) {
 | 
			
		||||
    let targetNtxIds: string[] | undefined;
 | 
			
		||||
    for (const ntxId of ntxIds) {
 | 
			
		||||
        targetNtxIds = findKeyByNtxId(ntxId);
 | 
			
		||||
        if (targetNtxIds) break; 
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (targetNtxIds) {
 | 
			
		||||
        noteSplitMap.get(targetNtxIds)?.destroy();
 | 
			
		||||
        for (const id of ntxIds) {
 | 
			
		||||
            if (!targetNtxIds.includes(id)) {
 | 
			
		||||
                targetNtxIds.push(id)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        targetNtxIds = [...ntxIds];
 | 
			
		||||
    }
 | 
			
		||||
    noteSplitMap.set(targetNtxIds, undefined);
 | 
			
		||||
    createSplitInstance(targetNtxIds);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function delNoteSplitResizer(ntxIds: string[]) {
 | 
			
		||||
    let targetNtxIds = findKeyByNtxId(ntxIds[0]);
 | 
			
		||||
    if (!targetNtxIds) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    noteSplitMap.get(targetNtxIds)?.destroy();
 | 
			
		||||
    noteSplitMap.delete(targetNtxIds);
 | 
			
		||||
    targetNtxIds = targetNtxIds.filter(id => !ntxIds.includes(id));
 | 
			
		||||
 | 
			
		||||
    if (targetNtxIds.length >= 2) {
 | 
			
		||||
        noteSplitMap.set(targetNtxIds, undefined);
 | 
			
		||||
        createSplitInstance(targetNtxIds);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function moveNoteSplitResizer(ntxId: string) {
 | 
			
		||||
    const targetNtxIds = findKeyByNtxId(ntxId);
 | 
			
		||||
    if (!targetNtxIds) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    noteSplitMap.get(targetNtxIds)?.destroy();
 | 
			
		||||
    noteSplitMap.set(targetNtxIds, undefined);
 | 
			
		||||
    createSplitInstance(targetNtxIds);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSplitInstance(targetNtxIds: string[]) {
 | 
			
		||||
    const prevRafId = noteSplitRafMap.get(targetNtxIds);
 | 
			
		||||
    if (prevRafId) {
 | 
			
		||||
        cancelAnimationFrame(prevRafId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rafId = requestAnimationFrame(() => {
 | 
			
		||||
        splitNoteContainer = splitNoteContainer ?? $("#center-pane").find(".split-note-container-widget")[0];
 | 
			
		||||
        const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')]
 | 
			
		||||
            .filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? ""));
 | 
			
		||||
        const splitInstance = Split(splitPanels, {
 | 
			
		||||
            gutterSize: DEFAULT_GUTTER_SIZE,
 | 
			
		||||
            minSize: 150,
 | 
			
		||||
        });
 | 
			
		||||
        noteSplitMap.set(targetNtxIds, splitInstance);
 | 
			
		||||
        noteSplitRafMap.delete(targetNtxIds);
 | 
			
		||||
    });
 | 
			
		||||
    noteSplitRafMap.set(targetNtxIds, rafId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    setupLeftPaneResizer,
 | 
			
		||||
    setupRightPaneResizer,
 | 
			
		||||
    setupNoteSplitResizer,
 | 
			
		||||
    delNoteSplitResizer,
 | 
			
		||||
    moveNoteSplitResizer
 | 
			
		||||
    setupRightPaneResizer
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -218,7 +218,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
 | 
			
		||||
if (utils.isElectron()) {
 | 
			
		||||
    const ipc = utils.dynamicRequire("electron").ipcRenderer;
 | 
			
		||||
 | 
			
		||||
    ipc.on("server-response", async (_, arg: Arg) => {
 | 
			
		||||
    ipc.on("server-response", async (event: string, arg: Arg) => {
 | 
			
		||||
        if (arg.statusCode >= 200 && arg.statusCode < 300) {
 | 
			
		||||
            handleSuccessfulResponse(arg);
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,355 +0,0 @@
 | 
			
		||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
 | 
			
		||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
 | 
			
		||||
 | 
			
		||||
// Mock utils module
 | 
			
		||||
vi.mock("./utils.js", () => ({
 | 
			
		||||
    default: {
 | 
			
		||||
        isDesktop: () => true
 | 
			
		||||
    }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// Mock jQuery globally since it's used in the shortcuts module
 | 
			
		||||
const mockElement = {
 | 
			
		||||
    addEventListener: vi.fn(),
 | 
			
		||||
    removeEventListener: vi.fn()
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockJQuery = vi.fn(() => [mockElement]);
 | 
			
		||||
(mockJQuery as any).length = 1;
 | 
			
		||||
mockJQuery[0] = mockElement;
 | 
			
		||||
 | 
			
		||||
(global as any).$ = mockJQuery as any;
 | 
			
		||||
global.document = mockElement as any;
 | 
			
		||||
 | 
			
		||||
describe("shortcuts", () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        vi.clearAllMocks();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        // Clean up any active bindings after each test
 | 
			
		||||
        shortcuts.removeGlobalShortcut("test-namespace");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("normalizeShortcut", () => {
 | 
			
		||||
        it("should normalize shortcut to lowercase and remove whitespace", () => {
 | 
			
		||||
            expect(shortcuts.normalizeShortcut("Ctrl + A")).toBe("ctrl+a");
 | 
			
		||||
            expect(shortcuts.normalizeShortcut("  SHIFT + F1  ")).toBe("shift+f1");
 | 
			
		||||
            expect(shortcuts.normalizeShortcut("Alt+Space")).toBe("alt+space");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle empty or null shortcuts", () => {
 | 
			
		||||
            expect(shortcuts.normalizeShortcut("")).toBe("");
 | 
			
		||||
            expect(shortcuts.normalizeShortcut(null as any)).toBe(null);
 | 
			
		||||
            expect(shortcuts.normalizeShortcut(undefined as any)).toBe(undefined);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle shortcuts with multiple spaces", () => {
 | 
			
		||||
            expect(shortcuts.normalizeShortcut("Ctrl   +   Shift   +   A")).toBe("ctrl+shift+a");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should warn about malformed shortcuts", () => {
 | 
			
		||||
            const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 | 
			
		||||
 | 
			
		||||
            shortcuts.normalizeShortcut("ctrl+");
 | 
			
		||||
            shortcuts.normalizeShortcut("+a");
 | 
			
		||||
            shortcuts.normalizeShortcut("ctrl++a");
 | 
			
		||||
 | 
			
		||||
            expect(consoleSpy).toHaveBeenCalledTimes(3);
 | 
			
		||||
            consoleSpy.mockRestore();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("keyMatches", () => {
 | 
			
		||||
        const createKeyboardEvent = (key: string, code?: string) => ({
 | 
			
		||||
            key,
 | 
			
		||||
            code: code || `Key${key.toUpperCase()}`
 | 
			
		||||
        } as KeyboardEvent);
 | 
			
		||||
 | 
			
		||||
        it("should match regular letter keys using key code", () => {
 | 
			
		||||
            const event = createKeyboardEvent("a", "KeyA");
 | 
			
		||||
            expect(keyMatches(event, "a")).toBe(true);
 | 
			
		||||
            expect(keyMatches(event, "A")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should match number keys using digit codes", () => {
 | 
			
		||||
            const event = createKeyboardEvent("1", "Digit1");
 | 
			
		||||
            expect(keyMatches(event, "1")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should match special keys using key mapping", () => {
 | 
			
		||||
            expect(keyMatches({ key: "Enter" } as KeyboardEvent, "return")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: "Enter" } as KeyboardEvent, "enter")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: "Delete" } as KeyboardEvent, "del")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: "Escape" } as KeyboardEvent, "esc")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: " " } as KeyboardEvent, "space")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: "ArrowUp" } as KeyboardEvent, "up")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should match function keys", () => {
 | 
			
		||||
            expect(keyMatches({ key: "F1" } as KeyboardEvent, "f1")).toBe(true);
 | 
			
		||||
            expect(keyMatches({ key: "F12" } as KeyboardEvent, "f12")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle undefined or null keys", () => {
 | 
			
		||||
            const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 | 
			
		||||
 | 
			
		||||
            expect(keyMatches({} as KeyboardEvent, null as any)).toBe(false);
 | 
			
		||||
            expect(keyMatches({} as KeyboardEvent, undefined as any)).toBe(false);
 | 
			
		||||
 | 
			
		||||
            expect(consoleSpy).toHaveBeenCalled();
 | 
			
		||||
            consoleSpy.mockRestore();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("matchesShortcut", () => {
 | 
			
		||||
        const createKeyboardEvent = (options: {
 | 
			
		||||
            key: string;
 | 
			
		||||
            code?: string;
 | 
			
		||||
            ctrlKey?: boolean;
 | 
			
		||||
            altKey?: boolean;
 | 
			
		||||
            shiftKey?: boolean;
 | 
			
		||||
            metaKey?: boolean;
 | 
			
		||||
        }) => ({
 | 
			
		||||
            key: options.key,
 | 
			
		||||
            code: options.code || `Key${options.key.toUpperCase()}`,
 | 
			
		||||
            ctrlKey: options.ctrlKey || false,
 | 
			
		||||
            altKey: options.altKey || false,
 | 
			
		||||
            shiftKey: options.shiftKey || false,
 | 
			
		||||
            metaKey: options.metaKey || false
 | 
			
		||||
        } as KeyboardEvent);
 | 
			
		||||
 | 
			
		||||
        it("should match simple key shortcuts", () => {
 | 
			
		||||
            const event = createKeyboardEvent({ key: "a", code: "KeyA" });
 | 
			
		||||
            expect(matchesShortcut(event, "a")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should match shortcuts with modifiers", () => {
 | 
			
		||||
            const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
 | 
			
		||||
            expect(matchesShortcut(event, "ctrl+a")).toBe(true);
 | 
			
		||||
 | 
			
		||||
            const shiftEvent = createKeyboardEvent({ key: "a", code: "KeyA", shiftKey: true });
 | 
			
		||||
            expect(matchesShortcut(shiftEvent, "shift+a")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should match complex modifier combinations", () => {
 | 
			
		||||
            const event = createKeyboardEvent({
 | 
			
		||||
                key: "a",
 | 
			
		||||
                code: "KeyA",
 | 
			
		||||
                ctrlKey: true,
 | 
			
		||||
                shiftKey: true
 | 
			
		||||
            });
 | 
			
		||||
            expect(matchesShortcut(event, "ctrl+shift+a")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should not match when modifiers don't match", () => {
 | 
			
		||||
            const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
 | 
			
		||||
            expect(matchesShortcut(event, "alt+a")).toBe(false);
 | 
			
		||||
            expect(matchesShortcut(event, "a")).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle alternative modifier names", () => {
 | 
			
		||||
            const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
 | 
			
		||||
            expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true);
 | 
			
		||||
 | 
			
		||||
            const metaEvent = createKeyboardEvent({ key: "a", code: "KeyA", metaKey: true });
 | 
			
		||||
            expect(matchesShortcut(metaEvent, "cmd+a")).toBe(true);
 | 
			
		||||
            expect(matchesShortcut(metaEvent, "command+a")).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle empty or invalid shortcuts", () => {
 | 
			
		||||
            const event = createKeyboardEvent({ key: "a", code: "KeyA" });
 | 
			
		||||
            expect(matchesShortcut(event, "")).toBe(false);
 | 
			
		||||
            expect(matchesShortcut(event, null as any)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should handle invalid events", () => {
 | 
			
		||||
            const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 | 
			
		||||
 | 
			
		||||
            expect(matchesShortcut(null as any, "a")).toBe(false);
 | 
			
		||||
            expect(matchesShortcut({} as KeyboardEvent, "a")).toBe(false);
 | 
			
		||||
 | 
			
		||||
            expect(consoleSpy).toHaveBeenCalled();
 | 
			
		||||
            consoleSpy.mockRestore();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should warn about invalid shortcut formats", () => {
 | 
			
		||||
            const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
 | 
			
		||||
            const event = createKeyboardEvent({ key: "a", code: "KeyA" });
 | 
			
		||||
 | 
			
		||||
            matchesShortcut(event, "ctrl+");
 | 
			
		||||
            matchesShortcut(event, "+");
 | 
			
		||||
 | 
			
		||||
            expect(consoleSpy).toHaveBeenCalled();
 | 
			
		||||
            consoleSpy.mockRestore();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("bindGlobalShortcut", () => {
 | 
			
		||||
        it("should bind a global shortcut", () => {
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should not bind shortcuts when handler is null", () => {
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", null, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            expect(mockElement.addEventListener).not.toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should remove previous bindings when namespace is reused", () => {
 | 
			
		||||
            const handler1 = vi.fn();
 | 
			
		||||
            const handler2 = vi.fn();
 | 
			
		||||
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler1, "test-namespace");
 | 
			
		||||
            expect(mockElement.addEventListener).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+b", handler2, "test-namespace");
 | 
			
		||||
            expect(mockElement.removeEventListener).toHaveBeenCalledTimes(1);
 | 
			
		||||
            expect(mockElement.addEventListener).toHaveBeenCalledTimes(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("bindElShortcut", () => {
 | 
			
		||||
        it("should bind shortcut to specific element", () => {
 | 
			
		||||
            const mockEl = { addEventListener: vi.fn(), removeEventListener: vi.fn() };
 | 
			
		||||
            const mockJQueryEl = [mockEl] as any;
 | 
			
		||||
            mockJQueryEl.length = 1;
 | 
			
		||||
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should fall back to document when element is empty", () => {
 | 
			
		||||
            const emptyJQuery = [] as any;
 | 
			
		||||
            emptyJQuery.length = 0;
 | 
			
		||||
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("removeGlobalShortcut", () => {
 | 
			
		||||
        it("should remove shortcuts for a specific namespace", () => {
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            shortcuts.removeGlobalShortcut("test-namespace");
 | 
			
		||||
 | 
			
		||||
            expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("event handling", () => {
 | 
			
		||||
        it.skip("should call handler when shortcut matches", () => {
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            // Get the listener that was registered
 | 
			
		||||
            expect(mockElement.addEventListener.mock.calls).toHaveLength(1);
 | 
			
		||||
            const [, listener] = mockElement.addEventListener.mock.calls[0];
 | 
			
		||||
 | 
			
		||||
            // First verify that matchesShortcut works directly
 | 
			
		||||
            const testEvent = {
 | 
			
		||||
                type: "keydown",
 | 
			
		||||
                key: "a",
 | 
			
		||||
                code: "KeyA",
 | 
			
		||||
                ctrlKey: true,
 | 
			
		||||
                altKey: false,
 | 
			
		||||
                shiftKey: false,
 | 
			
		||||
                metaKey: false,
 | 
			
		||||
                preventDefault: vi.fn(),
 | 
			
		||||
                stopPropagation: vi.fn()
 | 
			
		||||
            } as any;
 | 
			
		||||
 | 
			
		||||
            // Test matchesShortcut directly first
 | 
			
		||||
            expect(matchesShortcut(testEvent, "ctrl+a")).toBe(true);
 | 
			
		||||
 | 
			
		||||
            // Now test the actual listener
 | 
			
		||||
            listener(testEvent);
 | 
			
		||||
 | 
			
		||||
            expect(handler).toHaveBeenCalled();
 | 
			
		||||
            expect(testEvent.preventDefault).toHaveBeenCalled();
 | 
			
		||||
            expect(testEvent.stopPropagation).toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should not call handler for non-keyboard events", () => {
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            const [, listener] = mockElement.addEventListener.mock.calls[0];
 | 
			
		||||
 | 
			
		||||
            // Simulate a non-keyboard event
 | 
			
		||||
            const event = {
 | 
			
		||||
                type: "click"
 | 
			
		||||
            } as any;
 | 
			
		||||
 | 
			
		||||
            listener(event);
 | 
			
		||||
 | 
			
		||||
            expect(handler).not.toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should not call handler when shortcut doesn't match", () => {
 | 
			
		||||
            const handler = vi.fn();
 | 
			
		||||
            shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
 | 
			
		||||
 | 
			
		||||
            const [, listener] = mockElement.addEventListener.mock.calls[0];
 | 
			
		||||
 | 
			
		||||
            // Simulate a non-matching keydown event
 | 
			
		||||
            const event = {
 | 
			
		||||
                type: "keydown",
 | 
			
		||||
                key: "b",
 | 
			
		||||
                code: "KeyB",
 | 
			
		||||
                ctrlKey: true,
 | 
			
		||||
                altKey: false,
 | 
			
		||||
                shiftKey: false,
 | 
			
		||||
                metaKey: false,
 | 
			
		||||
                preventDefault: vi.fn(),
 | 
			
		||||
                stopPropagation: vi.fn()
 | 
			
		||||
            } as any;
 | 
			
		||||
 | 
			
		||||
            listener(event);
 | 
			
		||||
 | 
			
		||||
            expect(handler).not.toHaveBeenCalled();
 | 
			
		||||
            expect(event.preventDefault).not.toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('isIMEComposing', () => {
 | 
			
		||||
        it('should return true when event.isComposing is true', () => {
 | 
			
		||||
            const event = { isComposing: true, keyCode: 65 } as KeyboardEvent;
 | 
			
		||||
            expect(isIMEComposing(event)).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return true when keyCode is 229', () => {
 | 
			
		||||
            const event = { isComposing: false, keyCode: 229 } as KeyboardEvent;
 | 
			
		||||
            expect(isIMEComposing(event)).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return true when both isComposing is true and keyCode is 229', () => {
 | 
			
		||||
            const event = { isComposing: true, keyCode: 229 } as KeyboardEvent;
 | 
			
		||||
            expect(isIMEComposing(event)).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return false for normal keys', () => {
 | 
			
		||||
            const event = { isComposing: false, keyCode: 65 } as KeyboardEvent;
 | 
			
		||||
            expect(isIMEComposing(event)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should return false when isComposing is undefined and keyCode is not 229', () => {
 | 
			
		||||
            const event = { keyCode: 13 } as KeyboardEvent;
 | 
			
		||||
            expect(isIMEComposing(event)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should handle null/undefined events gracefully', () => {
 | 
			
		||||
            expect(isIMEComposing(null as any)).toBe(false);
 | 
			
		||||
            expect(isIMEComposing(undefined as any)).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,62 +1,7 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
 | 
			
		||||
type ElementType = HTMLElement | Document;
 | 
			
		||||
type Handler = (e: KeyboardEvent) => void;
 | 
			
		||||
 | 
			
		||||
interface ShortcutBinding {
 | 
			
		||||
    element: HTMLElement | Document;
 | 
			
		||||
    shortcut: string;
 | 
			
		||||
    handler: Handler;
 | 
			
		||||
    namespace: string | null;
 | 
			
		||||
    listener: (evt: Event) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Store all active shortcut bindings for management
 | 
			
		||||
const activeBindings: Map<string, ShortcutBinding[]> = new Map();
 | 
			
		||||
 | 
			
		||||
// Handle special key mappings and aliases
 | 
			
		||||
const keyMap: { [key: string]: string[] } = {
 | 
			
		||||
    'return': ['Enter'],
 | 
			
		||||
    'enter': ['Enter'],  // alias for return
 | 
			
		||||
    'del': ['Delete'],
 | 
			
		||||
    'delete': ['Delete'], // alias for del
 | 
			
		||||
    'esc': ['Escape'],
 | 
			
		||||
    'escape': ['Escape'], // alias for esc
 | 
			
		||||
    'space': [' ', 'Space'],
 | 
			
		||||
    'tab': ['Tab'],
 | 
			
		||||
    'backspace': ['Backspace'],
 | 
			
		||||
    'home': ['Home'],
 | 
			
		||||
    'end': ['End'],
 | 
			
		||||
    'pageup': ['PageUp'],
 | 
			
		||||
    'pagedown': ['PageDown'],
 | 
			
		||||
    'up': ['ArrowUp'],
 | 
			
		||||
    'down': ['ArrowDown'],
 | 
			
		||||
    'left': ['ArrowLeft'],
 | 
			
		||||
    'right': ['ArrowRight']
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Function keys
 | 
			
		||||
for (let i = 1; i <= 19; i++) {
 | 
			
		||||
    keyMap[`f${i}`] = [`F${i}`];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if IME (Input Method Editor) is composing
 | 
			
		||||
 * This is used to prevent keyboard shortcuts from firing during IME composition
 | 
			
		||||
 * @param e - The keyboard event to check
 | 
			
		||||
 * @returns true if IME is currently composing, false otherwise
 | 
			
		||||
 */
 | 
			
		||||
export function isIMEComposing(e: KeyboardEvent): boolean {
 | 
			
		||||
    // Handle null/undefined events gracefully
 | 
			
		||||
    if (!e) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Standard check for composition state
 | 
			
		||||
    // e.isComposing is true when IME is actively composing
 | 
			
		||||
    // e.keyCode === 229 is a fallback for older browsers where 229 indicates IME processing
 | 
			
		||||
    return e.isComposing || e.keyCode === 229;
 | 
			
		||||
}
 | 
			
		||||
type Handler = (e: JQuery.TriggeredEvent<ElementType | Element, string, ElementType | Element, ElementType | Element>) => void;
 | 
			
		||||
 | 
			
		||||
function removeGlobalShortcut(namespace: string) {
 | 
			
		||||
    bindGlobalShortcut("", null, namespace);
 | 
			
		||||
@@ -70,148 +15,38 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
 | 
			
		||||
    if (utils.isDesktop()) {
 | 
			
		||||
        keyboardShortcut = normalizeShortcut(keyboardShortcut);
 | 
			
		||||
 | 
			
		||||
        // If namespace is provided, remove all previous bindings for this namespace
 | 
			
		||||
        let eventName = "keydown";
 | 
			
		||||
 | 
			
		||||
        if (namespace) {
 | 
			
		||||
            removeNamespaceBindings(namespace);
 | 
			
		||||
            eventName += `.${namespace}`;
 | 
			
		||||
 | 
			
		||||
            // if there's a namespace, then we replace the existing event handler with the new one
 | 
			
		||||
            $el.off(eventName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
 | 
			
		||||
        if (keyboardShortcut && handler) {
 | 
			
		||||
            const element = $el.length > 0 ? $el[0] as (HTMLElement | Document) : document;
 | 
			
		||||
 | 
			
		||||
            const listener = (evt: Event) => {
 | 
			
		||||
                // Only handle keyboard events
 | 
			
		||||
                if (evt.type !== 'keydown' || !(evt instanceof KeyboardEvent)) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const e = evt as KeyboardEvent;
 | 
			
		||||
                
 | 
			
		||||
                // Skip processing if IME is composing to prevent shortcuts from
 | 
			
		||||
                // interfering with text input in CJK languages
 | 
			
		||||
                if (isIMEComposing(e)) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (matchesShortcut(e, keyboardShortcut)) {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
        // method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
 | 
			
		||||
        if (keyboardShortcut) {
 | 
			
		||||
            $el.bind(eventName, keyboardShortcut, (e) => {
 | 
			
		||||
                if (handler) {
 | 
			
		||||
                    handler(e);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Add the event listener
 | 
			
		||||
            element.addEventListener('keydown', listener);
 | 
			
		||||
 | 
			
		||||
            // Store the binding for later cleanup
 | 
			
		||||
            const binding: ShortcutBinding = {
 | 
			
		||||
                element,
 | 
			
		||||
                shortcut: keyboardShortcut,
 | 
			
		||||
                handler,
 | 
			
		||||
                namespace,
 | 
			
		||||
                listener
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const key = namespace || 'global';
 | 
			
		||||
            if (!activeBindings.has(key)) {
 | 
			
		||||
                activeBindings.set(key, []);
 | 
			
		||||
            }
 | 
			
		||||
            activeBindings.get(key)!.push(binding);
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeNamespaceBindings(namespace: string) {
 | 
			
		||||
    const bindings = activeBindings.get(namespace);
 | 
			
		||||
    if (bindings) {
 | 
			
		||||
        // Remove all event listeners for this namespace
 | 
			
		||||
        bindings.forEach(binding => {
 | 
			
		||||
            binding.element.removeEventListener('keydown', binding.listener);
 | 
			
		||||
        });
 | 
			
		||||
        activeBindings.delete(namespace);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean {
 | 
			
		||||
    if (!shortcut) return false;
 | 
			
		||||
 | 
			
		||||
    // Ensure we have a proper KeyboardEvent with key property
 | 
			
		||||
    if (!e || typeof e.key !== 'string') {
 | 
			
		||||
        console.warn('matchesShortcut called with invalid event:', e);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parts = shortcut.toLowerCase().split('+');
 | 
			
		||||
    const key = parts[parts.length - 1]; // Last part is the actual key
 | 
			
		||||
    const modifiers = parts.slice(0, -1); // Everything before is modifiers
 | 
			
		||||
 | 
			
		||||
    // Defensive check - ensure we have a valid key
 | 
			
		||||
    if (!key || key.trim() === '') {
 | 
			
		||||
        console.warn('Invalid shortcut format:', shortcut);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if the main key matches
 | 
			
		||||
    if (!keyMatches(e, key)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check modifiers
 | 
			
		||||
    const expectedCtrl = modifiers.includes('ctrl') || modifiers.includes('control');
 | 
			
		||||
    const expectedAlt = modifiers.includes('alt');
 | 
			
		||||
    const expectedShift = modifiers.includes('shift');
 | 
			
		||||
    const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command');
 | 
			
		||||
 | 
			
		||||
    return e.ctrlKey === expectedCtrl &&
 | 
			
		||||
           e.altKey === expectedAlt &&
 | 
			
		||||
           e.shiftKey === expectedShift &&
 | 
			
		||||
           e.metaKey === expectedMeta;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function keyMatches(e: KeyboardEvent, key: string): boolean {
 | 
			
		||||
    // Defensive check for undefined/null key
 | 
			
		||||
    if (!key) {
 | 
			
		||||
        console.warn('keyMatches called with undefined/null key');
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mappedKeys = keyMap[key.toLowerCase()];
 | 
			
		||||
    if (mappedKeys) {
 | 
			
		||||
        return mappedKeys.includes(e.key) || mappedKeys.includes(e.code);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For number keys, use the physical key code regardless of modifiers
 | 
			
		||||
    // This works across all keyboard layouts
 | 
			
		||||
    if (key >= '0' && key <= '9') {
 | 
			
		||||
        return e.code === `Digit${key}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For letter keys, use the physical key code for consistency
 | 
			
		||||
    if (key.length === 1 && key >= 'a' && key <= 'z') {
 | 
			
		||||
        return e.key.toLowerCase() === key.toLowerCase();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For regular keys, check both key and code as fallback
 | 
			
		||||
    return e.key.toLowerCase() === key.toLowerCase() ||
 | 
			
		||||
           e.code.toLowerCase() === key.toLowerCase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple normalization - just lowercase and trim whitespace
 | 
			
		||||
 * Normalize to the form expected by the jquery.hotkeys.js
 | 
			
		||||
 */
 | 
			
		||||
function normalizeShortcut(shortcut: string): string {
 | 
			
		||||
    if (!shortcut) {
 | 
			
		||||
        return shortcut;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const normalized = shortcut.toLowerCase().trim().replace(/\s+/g, '');
 | 
			
		||||
 | 
			
		||||
    // Warn about potentially problematic shortcuts
 | 
			
		||||
    if (normalized.endsWith('+') || normalized.startsWith('+') || normalized.includes('++')) {
 | 
			
		||||
        console.warn('Potentially malformed shortcut:', shortcut, '-> normalized to:', normalized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return normalized;
 | 
			
		||||
    return shortcut.toLowerCase().replace("enter", "return").replace("delete", "del").replace("ctrl+alt", "alt+ctrl").replace("meta+alt", "alt+meta"); // alt needs to be first;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
 
 | 
			
		||||
@@ -51,14 +51,6 @@ export default class SpacedUpdate {
 | 
			
		||||
        this.lastUpdated = Date.now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the update interval for the spaced update.
 | 
			
		||||
     * @param interval The update interval in milliseconds.
 | 
			
		||||
     */
 | 
			
		||||
    setUpdateInterval(interval: number) {
 | 
			
		||||
        this.updateInterval = interval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    triggerUpdate() {
 | 
			
		||||
        if (!this.changed) {
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,9 +36,7 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
 | 
			
		||||
    const $copyButton = $("<button>")
 | 
			
		||||
        .addClass("bx component icon-action tn-tool-button bx-copy copy-button")
 | 
			
		||||
        .attr("title", t("code_block.copy_title"))
 | 
			
		||||
        .on("click", (e) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        .on("click", () => {
 | 
			
		||||
            if (!isShare) {
 | 
			
		||||
                copyTextWithToast($codeBlock.text());
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
import dayjs from "dayjs";
 | 
			
		||||
import type { ViewScope } from "./link.js";
 | 
			
		||||
import FNote from "../entities/fnote";
 | 
			
		||||
 | 
			
		||||
const SVG_MIME = "image/svg+xml";
 | 
			
		||||
 | 
			
		||||
export const isShare = !window.glob;
 | 
			
		||||
 | 
			
		||||
export function reloadFrontendApp(reason?: string) {
 | 
			
		||||
function reloadFrontendApp(reason?: string) {
 | 
			
		||||
    if (reason) {
 | 
			
		||||
        logInfo(`Frontend app reload: ${reason}`);
 | 
			
		||||
    }
 | 
			
		||||
@@ -14,7 +13,7 @@ export function reloadFrontendApp(reason?: string) {
 | 
			
		||||
    window.location.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function restartDesktopApp() {
 | 
			
		||||
function restartDesktopApp() {
 | 
			
		||||
    if (!isElectron()) {
 | 
			
		||||
        reloadFrontendApp();
 | 
			
		||||
        return;
 | 
			
		||||
@@ -126,7 +125,7 @@ function formatDateISO(date: Date) {
 | 
			
		||||
    return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
 | 
			
		||||
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
 | 
			
		||||
    if (userSuppliedFormat?.trim()) {
 | 
			
		||||
        return dayjs(date).format(userSuppliedFormat);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -145,11 +144,11 @@ function now() {
 | 
			
		||||
/**
 | 
			
		||||
 * Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
 | 
			
		||||
 */
 | 
			
		||||
export function isElectron() {
 | 
			
		||||
function isElectron() {
 | 
			
		||||
    return !!(window && window.process && window.process.type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMac() {
 | 
			
		||||
function isMac() {
 | 
			
		||||
    return navigator.platform.indexOf("Mac") > -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -186,11 +185,7 @@ export function escapeQuotes(value: string) {
 | 
			
		||||
    return value.replaceAll('"', """);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatSize(size: number | null | undefined) {
 | 
			
		||||
    if (size === null || size === undefined) {
 | 
			
		||||
        return "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
function formatSize(size: number) {
 | 
			
		||||
    size = Math.max(Math.round(size / 1024), 1);
 | 
			
		||||
 | 
			
		||||
    if (size < 1024) {
 | 
			
		||||
@@ -223,7 +218,7 @@ function randomString(len: number) {
 | 
			
		||||
    return text;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMobile() {
 | 
			
		||||
function isMobile() {
 | 
			
		||||
    return (
 | 
			
		||||
        window.glob?.device === "mobile" ||
 | 
			
		||||
        // window.glob.device is not available in setup
 | 
			
		||||
@@ -297,55 +292,7 @@ function isHtmlEmpty(html: string) {
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatHtml(html: string) {
 | 
			
		||||
    let indent = "\n";
 | 
			
		||||
    const tab = "\t";
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    let pre: { indent: string; tag: string }[] = [];
 | 
			
		||||
 | 
			
		||||
    html = html
 | 
			
		||||
        .replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), function (x) {
 | 
			
		||||
            pre.push({ indent: "", tag: x });
 | 
			
		||||
            return "<--TEMPPRE" + i++ + "/-->";
 | 
			
		||||
        })
 | 
			
		||||
        .replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) {
 | 
			
		||||
            let ret;
 | 
			
		||||
            const tagRegEx = /<\/?([^\s/>]+)/.exec(x);
 | 
			
		||||
            let tag = tagRegEx ? tagRegEx[1] : "";
 | 
			
		||||
            let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
 | 
			
		||||
 | 
			
		||||
            if (p) {
 | 
			
		||||
                const pInd = parseInt(p[1]);
 | 
			
		||||
                pre[pInd].indent = indent;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) {
 | 
			
		||||
                // self closing tag
 | 
			
		||||
                ret = indent + x;
 | 
			
		||||
            } else {
 | 
			
		||||
                if (x.indexOf("</") < 0) {
 | 
			
		||||
                    //open tag
 | 
			
		||||
                    if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
 | 
			
		||||
                    else ret = indent + x;
 | 
			
		||||
                    !p && (indent += tab);
 | 
			
		||||
                } else {
 | 
			
		||||
                    //close tag
 | 
			
		||||
                    indent = indent.substr(0, indent.length - 1);
 | 
			
		||||
                    if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
 | 
			
		||||
                    else ret = indent + x;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return ret;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    for (i = pre.length; i--;) {
 | 
			
		||||
        html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function clearBrowserCache() {
 | 
			
		||||
async function clearBrowserCache() {
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        const win = dynamicRequire("@electron/remote").getCurrentWindow();
 | 
			
		||||
        await win.webContents.session.clearCache();
 | 
			
		||||
@@ -359,13 +306,7 @@ function copySelectionToClipboard() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dynamicRequireMappings = {
 | 
			
		||||
    "@electron/remote": typeof import("@electron/remote"),
 | 
			
		||||
    "electron": typeof import("electron"),
 | 
			
		||||
    "child_process": typeof import("child_process")
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
 | 
			
		||||
function dynamicRequire(moduleName: string) {
 | 
			
		||||
    if (typeof __non_webpack_require__ !== "undefined") {
 | 
			
		||||
        return __non_webpack_require__(moduleName);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -433,42 +374,33 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
 | 
			
		||||
 | 
			
		||||
    const inAppHelpPage = $button.attr("data-in-app-help");
 | 
			
		||||
    if (inAppHelpPage) {
 | 
			
		||||
        openInAppHelpFromUrl(inAppHelpPage);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Opens the in-app help at the given page in a split note. If there already is a split note open with a help page, it will be replaced by this one.
 | 
			
		||||
 *
 | 
			
		||||
 * @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
 | 
			
		||||
 * @returns a promise that resolves once the help has been opened.
 | 
			
		||||
 */
 | 
			
		||||
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
 | 
			
		||||
    // Dynamic import to avoid import issues in tests.
 | 
			
		||||
    const appContext = (await import("../components/app_context.js")).default;
 | 
			
		||||
    const activeContext = appContext.tabManager.getActiveContext();
 | 
			
		||||
    if (!activeContext) {
 | 
			
		||||
        // Dynamic import to avoid import issues in tests.
 | 
			
		||||
        const appContext = (await import("../components/app_context.js")).default;
 | 
			
		||||
        const activeContext = appContext.tabManager.getActiveContext();
 | 
			
		||||
        if (!activeContext) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const subContexts = activeContext.getSubContexts();
 | 
			
		||||
        const targetNote = `_help_${inAppHelpPage}`;
 | 
			
		||||
        const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
 | 
			
		||||
        const viewScope: ViewScope = {
 | 
			
		||||
            viewMode: "contextual-help",
 | 
			
		||||
        };
 | 
			
		||||
        if (!helpSubcontext) {
 | 
			
		||||
            // The help is not already open, open a new split with it.
 | 
			
		||||
            const { ntxId } = subContexts[subContexts.length - 1];
 | 
			
		||||
            appContext.triggerCommand("openNewNoteSplit", {
 | 
			
		||||
                ntxId,
 | 
			
		||||
                notePath: targetNote,
 | 
			
		||||
                hoistedNoteId: "_help",
 | 
			
		||||
                viewScope
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            // There is already a help window open, make sure it opens on the right note.
 | 
			
		||||
            helpSubcontext.setNote(targetNote, { viewScope });
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const subContexts = activeContext.getSubContexts();
 | 
			
		||||
    const targetNote = `_help_${inAppHelpPage}`;
 | 
			
		||||
    const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
 | 
			
		||||
    const viewScope: ViewScope = {
 | 
			
		||||
        viewMode: "contextual-help",
 | 
			
		||||
    };
 | 
			
		||||
    if (!helpSubcontext) {
 | 
			
		||||
        // The help is not already open, open a new split with it.
 | 
			
		||||
        const { ntxId } = subContexts[subContexts.length - 1];
 | 
			
		||||
        appContext.triggerCommand("openNewNoteSplit", {
 | 
			
		||||
            ntxId,
 | 
			
		||||
            notePath: targetNote,
 | 
			
		||||
            hoistedNoteId: "_help",
 | 
			
		||||
            viewScope
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
        // There is already a help window open, make sure it opens on the right note.
 | 
			
		||||
        helpSubcontext.setNote(targetNote, { viewScope });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
 | 
			
		||||
@@ -629,7 +561,8 @@ function copyHtmlToClipboard(content: string) {
 | 
			
		||||
    document.removeEventListener("copy", listener);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createImageSrcUrl(note: FNote) {
 | 
			
		||||
// TODO: Set to FNote once the file is ported.
 | 
			
		||||
function createImageSrcUrl(note: { noteId: string; title: string }) {
 | 
			
		||||
    return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -798,86 +731,10 @@ function isUpdateAvailable(latestVersion: string | null | undefined, currentVers
 | 
			
		||||
    return compareVersions(latestVersion, currentVersion) > 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isLaunchBarConfig(noteId: string) {
 | 
			
		||||
function isLaunchBarConfig(noteId: string) {
 | 
			
		||||
    return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adds a class to the <body> of the page, where the class name is formed via a prefix and a value.
 | 
			
		||||
 * Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value.
 | 
			
		||||
 * There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix.
 | 
			
		||||
 *
 | 
			
		||||
 * @param prefix the prefix.
 | 
			
		||||
 * @param value the value to be appended to the prefix.
 | 
			
		||||
 */
 | 
			
		||||
export function toggleBodyClass(prefix: string, value: string) {
 | 
			
		||||
    const $body = $("body");
 | 
			
		||||
    for (const clazz of Array.from($body[0].classList)) {
 | 
			
		||||
        // create copy to safely iterate over while removing classes
 | 
			
		||||
        if (clazz.startsWith(prefix)) {
 | 
			
		||||
            $body.removeClass(clazz);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $body.addClass(prefix + value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic comparison for equality between the two arrays. The values are strictly checked via `===`.
 | 
			
		||||
 *
 | 
			
		||||
 * @param a the first array to compare.
 | 
			
		||||
 * @param b the second array to compare.
 | 
			
		||||
 * @returns `true` if both arrays are equals, `false` otherwise.
 | 
			
		||||
 */
 | 
			
		||||
export function arrayEqual<T>(a: T[], b: T[]) {
 | 
			
		||||
    if (a === b) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (a.length !== b.length) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i=0; i < a.length; i++) {
 | 
			
		||||
        if (a[i] !== b[i]) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Indexed<T extends object> = T & { index: number };
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Given an object array, alters every object in the array to have an index field assigned to it.
 | 
			
		||||
 *
 | 
			
		||||
 * @param items the objects to be numbered.
 | 
			
		||||
 * @returns the same object for convenience, with the type changed to indicate the new index field.
 | 
			
		||||
 */
 | 
			
		||||
export function numberObjectsInPlace<T extends object>(items: T[]): Indexed<T>[] {
 | 
			
		||||
    let index = 0;
 | 
			
		||||
    for (const item of items) {
 | 
			
		||||
        (item as Indexed<T>).index = index++;
 | 
			
		||||
    }
 | 
			
		||||
    return items as Indexed<T>[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mapToKeyValueArray<K extends string | number | symbol, V>(map: Record<K, V>) {
 | 
			
		||||
    const values: { key: K, value: V }[] = [];
 | 
			
		||||
    for (const [ key, value ] of Object.entries(map)) {
 | 
			
		||||
        values.push({ key: key as K, value: value as V });
 | 
			
		||||
    }
 | 
			
		||||
    return values;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getErrorMessage(e: unknown) {
 | 
			
		||||
    if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
 | 
			
		||||
        return e.message;
 | 
			
		||||
    } else {
 | 
			
		||||
        return "Unknown error";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    reloadFrontendApp,
 | 
			
		||||
    restartDesktopApp,
 | 
			
		||||
@@ -903,7 +760,6 @@ export default {
 | 
			
		||||
    getNoteTypeClass,
 | 
			
		||||
    getMimeTypeClass,
 | 
			
		||||
    isHtmlEmpty,
 | 
			
		||||
    formatHtml,
 | 
			
		||||
    clearBrowserCache,
 | 
			
		||||
    copySelectionToClipboard,
 | 
			
		||||
    dynamicRequire,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import "bootstrap/dist/css/bootstrap.min.css";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
import "./stylesheets/auth.css";
 | 
			
		||||
 | 
			
		||||
// @TriliumNextTODO: is this even needed anymore?
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import "jquery";
 | 
			
		||||
import "jquery-hotkeys";
 | 
			
		||||
import utils from "./services/utils.js";
 | 
			
		||||
import ko from "knockout";
 | 
			
		||||
import "bootstrap/dist/css/bootstrap.min.css";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
 | 
			
		||||
// TriliumNextTODO: properly make use of below types
 | 
			
		||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import "normalize.css";
 | 
			
		||||
import "boxicons/css/boxicons.min.css";
 | 
			
		||||
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
 | 
			
		||||
import "@triliumnext/ckeditor5/content.css";
 | 
			
		||||
import "@triliumnext/share-theme/styles/index.css";
 | 
			
		||||
import "@triliumnext/share-theme/scripts/index.js";
 | 
			
		||||
 | 
			
		||||
@@ -29,14 +29,6 @@ async function formatCodeBlocks() {
 | 
			
		||||
    await formatCodeBlocks($("#content"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setupTextNote() {
 | 
			
		||||
    formatCodeBlocks();
 | 
			
		||||
    applyMath();
 | 
			
		||||
 | 
			
		||||
    const setupMermaid = (await import("./share/mermaid.js")).default;
 | 
			
		||||
    setupMermaid();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch note with given ID from backend
 | 
			
		||||
 *
 | 
			
		||||
@@ -55,11 +47,8 @@ async function fetchNote(noteId: string | null = null) {
 | 
			
		||||
document.addEventListener(
 | 
			
		||||
    "DOMContentLoaded",
 | 
			
		||||
    () => {
 | 
			
		||||
        const noteType = determineNoteType();
 | 
			
		||||
 | 
			
		||||
        if (noteType === "text") {
 | 
			
		||||
            setupTextNote();
 | 
			
		||||
        }
 | 
			
		||||
        formatCodeBlocks();
 | 
			
		||||
        applyMath();
 | 
			
		||||
 | 
			
		||||
        const toggleMenuButton = document.getElementById("toggleMenuButton");
 | 
			
		||||
        const layout = document.getElementById("layout");
 | 
			
		||||
@@ -71,12 +60,6 @@ document.addEventListener(
 | 
			
		||||
    false
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function determineNoteType() {
 | 
			
		||||
    const bodyClass = document.body.className;
 | 
			
		||||
    const match = bodyClass.match(/type-([^\s]+)/);
 | 
			
		||||
    return match ? match[1] : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
 | 
			
		||||
// add fetchNote as property to the window object
 | 
			
		||||
Object.defineProperty(window, "fetchNote", {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
import mermaid from "mermaid";
 | 
			
		||||
 | 
			
		||||
export default function setupMermaid() {
 | 
			
		||||
    for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
 | 
			
		||||
        const parentPre = codeBlock.parentElement;
 | 
			
		||||
        if (!parentPre) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const mermaidDiv = document.createElement("div");
 | 
			
		||||
        mermaidDiv.classList.add("mermaid");
 | 
			
		||||
        mermaidDiv.innerHTML = codeBlock.innerHTML;
 | 
			
		||||
        parentPre.replaceWith(mermaidDiv);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mermaid.init();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/client/src/stylesheets/bootstrap.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								apps/client/src/stylesheets/bootstrap.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
/* Import all of Bootstrap's CSS */
 | 
			
		||||
@use "bootstrap/scss/bootstrap";
 | 
			
		||||
@@ -81,8 +81,8 @@ body {
 | 
			
		||||
 | 
			
		||||
    /* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
 | 
			
		||||
 | 
			
		||||
    --ck-content-color-image-caption-background: var(--main-background-color);
 | 
			
		||||
    --ck-content-color-image-caption-text: var(--main-text-color);
 | 
			
		||||
    --ck-color-image-caption-background: var(--main-background-color);
 | 
			
		||||
    --ck-color-image-caption-text: var(--main-text-color);
 | 
			
		||||
 | 
			
		||||
    /* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,28 +28,6 @@
 | 
			
		||||
    --ck-mention-list-max-height: 500px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body#trilium-app.motion-disabled *,
 | 
			
		||||
body#trilium-app.motion-disabled *::before,
 | 
			
		||||
body#trilium-app.motion-disabled *::after {
 | 
			
		||||
    /* Disable transitions and animations */
 | 
			
		||||
    transition: none !important;
 | 
			
		||||
    animation: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body#trilium-app.shadows-disabled *,
 | 
			
		||||
body#trilium-app.shadows-disabled *::before,
 | 
			
		||||
body#trilium-app.shadows-disabled *::after {
 | 
			
		||||
    /* Disable shadows */
 | 
			
		||||
    box-shadow: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body#trilium-app.backdrop-effects-disabled *,
 | 
			
		||||
body#trilium-app.backdrop-effects-disabled *::before,
 | 
			
		||||
body#trilium-app.backdrop-effects-disabled *::after {
 | 
			
		||||
    /* Disable backdrop effects */
 | 
			
		||||
    backdrop-filter: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
    --bs-table-bg: transparent !important;
 | 
			
		||||
}
 | 
			
		||||
@@ -161,13 +139,10 @@ textarea,
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group.disabled {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group {
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
/* Restore default apperance */
 | 
			
		||||
input[type="number"],
 | 
			
		||||
input[type="checkbox"] {
 | 
			
		||||
    appearance: auto !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Add a gap between consecutive radios / check boxes */
 | 
			
		||||
@@ -176,11 +151,6 @@ label.tn-checkbox + label.tn-checkbox {
 | 
			
		||||
    margin-left: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label.tn-radio input[type="radio"],
 | 
			
		||||
label.tn-checkbox input[type="checkbox"] {
 | 
			
		||||
    margin-right: .5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#left-pane input,
 | 
			
		||||
#left-pane select,
 | 
			
		||||
#left-pane textarea {
 | 
			
		||||
@@ -357,8 +327,7 @@ button kbd {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu,
 | 
			
		||||
.tabulator-popup-container {
 | 
			
		||||
.dropdown-menu {
 | 
			
		||||
    color: var(--menu-text-color) !important;
 | 
			
		||||
    font-size: inherit;
 | 
			
		||||
    background-color: var(--menu-background-color) !important;
 | 
			
		||||
@@ -373,8 +342,7 @@ button kbd {
 | 
			
		||||
    break-after: avoid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.desktop .dropdown-menu,
 | 
			
		||||
body.desktop .tabulator-popup-container {
 | 
			
		||||
body.desktop .dropdown-menu {
 | 
			
		||||
    border: 1px solid var(--dropdown-border-color);
 | 
			
		||||
    box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
 | 
			
		||||
    animation: dropdown-menu-opening 100ms ease-in;
 | 
			
		||||
@@ -382,7 +350,7 @@ body.desktop .tabulator-popup-container {
 | 
			
		||||
 | 
			
		||||
@supports (animation-fill-mode: forwards) {
 | 
			
		||||
    /* Delay the opening of submenus */
 | 
			
		||||
    body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
 | 
			
		||||
    body.desktop .dropdown-submenu .dropdown-menu {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        animation-fill-mode: forwards;
 | 
			
		||||
        animation-delay: var(--submenu-opening-delay);
 | 
			
		||||
@@ -417,8 +385,7 @@ body.desktop .tabulator-popup-container {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu a:hover:not(.disabled),
 | 
			
		||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container),
 | 
			
		||||
.tabulator-menu-item:hover {
 | 
			
		||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container) {
 | 
			
		||||
    color: var(--hover-item-text-color) !important;
 | 
			
		||||
    background-color: var(--hover-item-background-color) !important;
 | 
			
		||||
    border-color: var(--hover-item-border-color) !important;
 | 
			
		||||
@@ -442,20 +409,14 @@ body #context-menu-container .dropdown-item > span {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item span.keyboard-shortcut {
 | 
			
		||||
.dropdown-menu kbd {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu kbd {    
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
    border: none;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item,
 | 
			
		||||
@@ -684,10 +645,6 @@ table.promoted-attributes-in-tooltip th {
 | 
			
		||||
    z-index: calc(var(--ck-z-panel) - 1) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tooltip.tooltip-top {
 | 
			
		||||
    z-index: 32767 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tooltip-trigger {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
@@ -873,34 +830,10 @@ table.promoted-attributes-in-tooltip th {
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 6px 16px;
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion .icon {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    line-height: inherit;
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion .text {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: calc(100% - 20px);
 | 
			
		||||
    padding-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion .search-result-title {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion .search-result-attributes {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.aa-dropdown-menu .aa-suggestion p {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
@@ -988,18 +921,6 @@ div[data-notify="container"] {
 | 
			
		||||
    font-family: var(--monospace-font-family);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.ck-icon .note-icon {
 | 
			
		||||
    color: var(--main-text-color);
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck-content {
 | 
			
		||||
    --ck-content-font-family: var(--detail-font-family);
 | 
			
		||||
    --ck-content-font-size: 1.1em;
 | 
			
		||||
    --ck-content-font-color: var(--main-text-color);
 | 
			
		||||
    --ck-content-line-height: var(--bs-body-line-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck-content .table table th {
 | 
			
		||||
    background-color: var(--accented-background-color);
 | 
			
		||||
}
 | 
			
		||||
@@ -1134,7 +1055,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
 | 
			
		||||
 | 
			
		||||
.toast-body {
 | 
			
		||||
    white-space: preserve-breaks;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck-mentions .ck-button {
 | 
			
		||||
@@ -1243,10 +1163,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
 | 
			
		||||
    cursor: row-resize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hidden-ext.note-split + .gutter {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#context-menu-cover.show {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    top: 0;
 | 
			
		||||
@@ -1291,14 +1207,12 @@ body.mobile .dropdown-submenu > .dropdown-menu {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#context-menu-container,
 | 
			
		||||
#context-menu-container .dropdown-menu,
 | 
			
		||||
.tabulator-popup-container {
 | 
			
		||||
    padding: 3px 0;
 | 
			
		||||
#context-menu-container .dropdown-menu {
 | 
			
		||||
    padding: 3px 0 0;
 | 
			
		||||
    z-index: 2000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#context-menu-container .dropdown-item,
 | 
			
		||||
.tabulator-menu .tabulator-menu-item {
 | 
			
		||||
#context-menu-container .dropdown-item {
 | 
			
		||||
    padding: 0 7px 0 10px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
@@ -1468,7 +1382,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: var(--launcher-pane-text-color);
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    background-color: var(--launcher-pane-background-color);
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1776,6 +1690,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-split {
 | 
			
		||||
    flex-basis: 0; /* so that each split has same width */
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
@@ -1813,12 +1728,16 @@ button.close:hover {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.options-section input[type="number"] {
 | 
			
		||||
.options-number-input {
 | 
			
		||||
    /* overriding settings from .form-control */
 | 
			
		||||
    width: 10em !important;
 | 
			
		||||
    flex-grow: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.options-mime-types {
 | 
			
		||||
    column-width: 250px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea {
 | 
			
		||||
    cursor: auto;
 | 
			
		||||
}
 | 
			
		||||
@@ -1839,106 +1758,20 @@ textarea {
 | 
			
		||||
    font-size: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-dialog {
 | 
			
		||||
    max-width: 900px;
 | 
			
		||||
    width: 90%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-header {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-body {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    min-height: 200px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results .aa-dropdown-menu {
 | 
			
		||||
    max-height: calc(80vh - 200px);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-width: none;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    max-height: 40vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results .aa-suggestions {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results .aa-dropdown-menu .aa-suggestion:hover,
 | 
			
		||||
.jump-to-note-results .aa-dropdown-menu .aa-cursor {
 | 
			
		||||
    background-color: var(--hover-item-background-color, #f8f9fa);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Command palette styling */
 | 
			
		||||
.jump-to-note-dialog .command-suggestion {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: 0.75rem;
 | 
			
		||||
    font-size: 0.9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .aa-suggestion .command-suggestion,
 | 
			
		||||
.jump-to-note-dialog .aa-suggestion .command-suggestion div {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .aa-cursor .command-suggestion,
 | 
			
		||||
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .show-in-full-search,
 | 
			
		||||
.jump-to-note-results .show-in-full-search {
 | 
			
		||||
    border-top: 1px solid var(--main-border-color);
 | 
			
		||||
    padding-top: 12px;
 | 
			
		||||
    margin-top: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results .aa-suggestion .search-notes-action {
 | 
			
		||||
    border-top: 1px solid var(--main-border-color);
 | 
			
		||||
    margin-top: 8px;
 | 
			
		||||
    padding-top: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-results .aa-suggestion:has(.search-notes-action)::after {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .command-icon {
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
    font-size: 1.125rem;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    margin-top: 0.125rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .command-content {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .command-name {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .command-description {
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    line-height: 1.3;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog kbd.command-shortcut {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
    font-family: inherit !important;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.empty-table-placeholder {
 | 
			
		||||
@@ -1999,7 +1832,6 @@ body.zen #launcher-container,
 | 
			
		||||
body.zen #launcher-pane,
 | 
			
		||||
body.zen #left-pane,
 | 
			
		||||
body.zen #right-pane,
 | 
			
		||||
body.zen #mobile-sidebar-wrapper,
 | 
			
		||||
body.zen .tab-row-container,
 | 
			
		||||
body.zen .tab-row-widget,
 | 
			
		||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)),
 | 
			
		||||
@@ -2007,8 +1839,7 @@ body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
 | 
			
		||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
 | 
			
		||||
body.zen .note-icon-widget,
 | 
			
		||||
body.zen .title-row .button-widget,
 | 
			
		||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
 | 
			
		||||
body.zen .action-button {
 | 
			
		||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt) {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -2050,20 +1881,14 @@ body.zen .note-title-widget input {
 | 
			
		||||
    background: transparent !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.zen #detail-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Content renderer */
 | 
			
		||||
 | 
			
		||||
footer.file-footer,
 | 
			
		||||
footer.webview-footer {
 | 
			
		||||
footer.file-footer {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.file-footer button,
 | 
			
		||||
footer.webview-footer button {
 | 
			
		||||
footer.file-footer button {
 | 
			
		||||
    margin: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -2370,21 +2195,3 @@ footer.webview-footer button {
 | 
			
		||||
    content: "\ec24";
 | 
			
		||||
    transform: rotate(180deg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* CK Edito */
 | 
			
		||||
 | 
			
		||||
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
 | 
			
		||||
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
 | 
			
		||||
    max-width: 25vw;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.revision-diff-added {
 | 
			
		||||
    background: rgba(100, 200, 100, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.revision-diff-removed {
 | 
			
		||||
    background: rgba(255, 100, 100, 0.5);
 | 
			
		||||
    text-decoration: line-through;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,199 +0,0 @@
 | 
			
		||||
.tabulator {
 | 
			
		||||
    --table-background-color: var(--main-background-color);
 | 
			
		||||
 | 
			
		||||
    --col-header-background-color: var(--main-background-color);
 | 
			
		||||
    --col-header-hover-background-color: var(--accented-background-color);
 | 
			
		||||
    --col-header-text-color: var(--main-text-color);
 | 
			
		||||
    --col-header-arrow-active-color: var(--main-text-color);
 | 
			
		||||
    --col-header-arrow-inactive-color: var(--more-accented-background-color);
 | 
			
		||||
    --col-header-separator-border: none;
 | 
			
		||||
    --col-header-bottom-border: 2px solid var(--main-border-color);
 | 
			
		||||
 | 
			
		||||
    --row-background-color: var(--main-background-color);
 | 
			
		||||
    --row-alternate-background-color: var(--main-background-color);
 | 
			
		||||
    --row-moving-background-color: var(--accented-background-color);
 | 
			
		||||
    --row-text-color: var(--main-text-color);
 | 
			
		||||
    --row-delimiter-color: var(--more-accented-background-color);
 | 
			
		||||
    
 | 
			
		||||
    --cell-horiz-padding-size: 8px;
 | 
			
		||||
    --cell-vert-padding-size: 8px;
 | 
			
		||||
    
 | 
			
		||||
    --cell-editable-hover-outline-color: var(--main-border-color);
 | 
			
		||||
    --cell-read-only-text-color: var(--muted-text-color);
 | 
			
		||||
    
 | 
			
		||||
    --cell-editing-border-color: var(--main-border-color);
 | 
			
		||||
    --cell-editing-border-width: 2px;
 | 
			
		||||
    --cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
 | 
			
		||||
    --cell-editing-text-color: initial;
 | 
			
		||||
 | 
			
		||||
    background: unset;
 | 
			
		||||
    border: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-tableholder .tabulator-table {
 | 
			
		||||
    background: var(--table-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Column headers */
 | 
			
		||||
 | 
			
		||||
.tabulator div.tabulator-header {
 | 
			
		||||
    border-bottom: var(--col-header-bottom-border);
 | 
			
		||||
    background: var(--col-header-background-color);
 | 
			
		||||
    color: var(--col-header-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-col-content {
 | 
			
		||||
    padding: 8px 4px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (hover: hover) and (pointer: fine) {
 | 
			
		||||
  .tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover {
 | 
			
		||||
    background-color: var(--col-header-hover-background-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator div.tabulator-header .tabulator-col.tabulator-moving {
 | 
			
		||||
    border: none;
 | 
			
		||||
    background: var(--col-header-hover-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
 | 
			
		||||
    border-bottom-color: var(--col-header-arrow-active-color);
 | 
			
		||||
    border-top-color: var(--col-header-arrow-active-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
 | 
			
		||||
    border-bottom-color: var(--col-header-arrow-inactive-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
 | 
			
		||||
    margin-left: var(--cell-editing-border-width);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator div.tabulator-header .tabulator-col,
 | 
			
		||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
 | 
			
		||||
    background: var(--col-header-background-color);
 | 
			
		||||
    border-right: var(--col-header-separator-border);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Table body */
 | 
			
		||||
 | 
			
		||||
.tabulator-tableholder {
 | 
			
		||||
    padding-top: 10px;
 | 
			
		||||
    height: unset !important; /* Don't extend on the full height */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Rows */
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell {
 | 
			
		||||
    padding: var(--cell-vert-padding-size) var(--cell-horiz-padding-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell input {
 | 
			
		||||
    padding-left: var(--cell-horiz-padding-size) !important;
 | 
			
		||||
    padding-right: var(--cell-horiz-padding-size) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border-top: none;
 | 
			
		||||
    border-bottom: 1px solid var(--row-delimiter-color);
 | 
			
		||||
    color: var(--row-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row.tabulator-row-odd {
 | 
			
		||||
    background: var(--row-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row.tabulator-row-even {
 | 
			
		||||
    background: var(--row-alternate-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row.tabulator-moving {
 | 
			
		||||
    border-color: transparent;
 | 
			
		||||
    background-color: var(--row-moving-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Cell */
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
 | 
			
		||||
    margin-right: var(--cell-editing-border-width);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
 | 
			
		||||
.tabulator-row .tabulator-cell {
 | 
			
		||||
    border-right-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
 | 
			
		||||
    color: var(--cell-read-only-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator:not(.tabulator-editing) .tabulator-row .tabulator-cell.tabulator-editable:hover {
 | 
			
		||||
    outline: 2px solid var(--cell-editable-hover-outline-color);
 | 
			
		||||
    outline-offset: -1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row .tabulator-cell.tabulator-editing {
 | 
			
		||||
    border-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing {
 | 
			
		||||
    outline: calc(var(--cell-editing-border-width) - 1px) solid var(--cell-editing-border-color);
 | 
			
		||||
    border-color: var(--cell-editing-border-color);
 | 
			
		||||
    background: var(--cell-editing-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing > * {
 | 
			
		||||
    color: var(--cell-editing-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tree-collapse,
 | 
			
		||||
.tabulator .tree-expand {
 | 
			
		||||
    color: var(--row-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Align items without children/expander to the ones with. */
 | 
			
		||||
.tabulator-cell[tabulator-field="title"] > span:first-child,         /* 1st level */
 | 
			
		||||
.tabulator-cell[tabulator-field="title"] > div:first-child + span {  /* sub-level */
 | 
			
		||||
    padding-left: 21px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Checkbox cells */
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-cell:has(svg),
 | 
			
		||||
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
 | 
			
		||||
    padding-left: 8px;
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-cell input[type="checkbox"] {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator .tabulator-footer {
 | 
			
		||||
    color: var(--main-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Context menus */
 | 
			
		||||
 | 
			
		||||
.tabulator-popup-container {
 | 
			
		||||
    min-width: 10em;
 | 
			
		||||
    border-radius: var(--bs-border-radius);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tabulator-menu .tabulator-menu-item {
 | 
			
		||||
    border: 1px solid transparent;
 | 
			
		||||
    color: var(--menu-text-color);
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Footer */
 | 
			
		||||
 | 
			
		||||
:root .tabulator .tabulator-footer {
 | 
			
		||||
    border-top: unset;
 | 
			
		||||
    padding: 10px 0;
 | 
			
		||||
}
 | 
			
		||||
@@ -13,13 +13,12 @@
 | 
			
		||||
 | 
			
		||||
    --theme-style: dark;
 | 
			
		||||
    --native-titlebar-background: #00000000;
 | 
			
		||||
    --window-background-color-bgfx: transparent; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --main-background-color: #272727;
 | 
			
		||||
    --main-text-color: #ccc;
 | 
			
		||||
    --main-border-color: #454545;
 | 
			
		||||
    --subtle-border-color: #313131;
 | 
			
		||||
    --dropdown-border-color: #404040;
 | 
			
		||||
    --dropdown-border-color: #292929;
 | 
			
		||||
    --dropdown-shadow-opacity: 0.6;
 | 
			
		||||
    --dropdown-item-icon-destructive-color: #de6e5b;
 | 
			
		||||
    --disabled-tooltip-icon-color: #7fd2ef;
 | 
			
		||||
@@ -90,7 +89,6 @@
 | 
			
		||||
 | 
			
		||||
    --menu-text-color: #e3e3e3;
 | 
			
		||||
    --menu-background-color: #222222d9;
 | 
			
		||||
    --menu-background-color-no-backdrop: #1b1b1b;
 | 
			
		||||
    --menu-item-icon-color: #8c8c8c;
 | 
			
		||||
    --menu-item-disabled-opacity: 0.5;
 | 
			
		||||
    --menu-item-keyboard-shortcut-color: #ffffff8f;
 | 
			
		||||
@@ -122,8 +120,6 @@
 | 
			
		||||
    --quick-search-focus-border: #80808095;
 | 
			
		||||
    --quick-search-focus-background: #ffffff1f;
 | 
			
		||||
    --quick-search-focus-color: white;
 | 
			
		||||
    --quick-search-result-content-background: #0000004d;
 | 
			
		||||
    --quick-search-result-highlight-color: #a4d995;
 | 
			
		||||
 | 
			
		||||
    --left-pane-collapsed-border-color: #0009;
 | 
			
		||||
    --left-pane-background-color: #1f1f1f;
 | 
			
		||||
@@ -148,7 +144,6 @@
 | 
			
		||||
    --launcher-pane-vert-button-hover-background: #ffffff1c;
 | 
			
		||||
    --launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
 | 
			
		||||
    --launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
 | 
			
		||||
    --launcher-pane-vert-background-color-bgfx: #00000026; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --launcher-pane-horiz-border-color: rgb(22, 22, 22);
 | 
			
		||||
    --launcher-pane-horiz-background-color: #282828;
 | 
			
		||||
@@ -157,8 +152,6 @@
 | 
			
		||||
    --launcher-pane-horiz-button-hover-background: #ffffff1c;
 | 
			
		||||
    --launcher-pane-horiz-button-hover-shadow: unset;
 | 
			
		||||
    --launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
 | 
			
		||||
    --launcher-pane-horiz-background-color-bgfx: #ffffff17; /* When background effects enabled */
 | 
			
		||||
    --launcher-pane-horiz-border-color-bgfx: #00000080; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --protected-session-active-icon-color: #8edd8e;
 | 
			
		||||
    --sync-status-error-pulse-color: #f47871;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@
 | 
			
		||||
 | 
			
		||||
    --theme-style: light;
 | 
			
		||||
    --native-titlebar-background: #ffffff00;
 | 
			
		||||
    --window-background-color-bgfx: transparent; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --main-background-color: white;
 | 
			
		||||
    --main-text-color: black;
 | 
			
		||||
@@ -84,7 +83,6 @@
 | 
			
		||||
 | 
			
		||||
    --menu-text-color: #272727;
 | 
			
		||||
    --menu-background-color: #ffffffd9;
 | 
			
		||||
    --menu-background-color-no-backdrop: #fdfdfd;
 | 
			
		||||
    --menu-item-icon-color: #727272;
 | 
			
		||||
    --menu-item-disabled-opacity: 0.6;
 | 
			
		||||
    --menu-item-keyboard-shortcut-color: #666666a8;
 | 
			
		||||
@@ -116,17 +114,15 @@
 | 
			
		||||
    --quick-search-focus-border: #00000029;
 | 
			
		||||
    --quick-search-focus-background: #ffffff80;
 | 
			
		||||
    --quick-search-focus-color: #000;
 | 
			
		||||
    --quick-search-result-content-background: #0000000f;
 | 
			
		||||
    --quick-search-result-highlight-color: #c65050;
 | 
			
		||||
 | 
			
		||||
    --left-pane-collapsed-border-color: #0000000d;
 | 
			
		||||
    --left-pane-background-color: #f2f2f2;
 | 
			
		||||
    --left-pane-text-color: #383838;
 | 
			
		||||
    --left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
 | 
			
		||||
    --left-pane-item-hover-background: #eaeaea;
 | 
			
		||||
    --left-pane-item-selected-background: white;
 | 
			
		||||
    --left-pane-item-selected-color: black;
 | 
			
		||||
    --left-pane-item-selected-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
 | 
			
		||||
    --left-pane-item-action-button-background: rgba(0, 0, 0, 0.11);
 | 
			
		||||
    --left-pane-item-action-button-background: #d7d7d7;
 | 
			
		||||
    --left-pane-item-action-button-color: inherit;
 | 
			
		||||
    --left-pane-item-action-button-hover-background: white;
 | 
			
		||||
    --left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
 | 
			
		||||
@@ -142,7 +138,6 @@
 | 
			
		||||
    --launcher-pane-vert-button-hover-background: white;
 | 
			
		||||
    --launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
 | 
			
		||||
    --launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
 | 
			
		||||
    --launcher-pane-vert-background-color-bgfx: #00000009; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.1);
 | 
			
		||||
    --launcher-pane-horiz-background-color: #fafafa;
 | 
			
		||||
@@ -150,8 +145,6 @@
 | 
			
		||||
    --launcher-pane-horiz-button-hover-background: var(--icon-button-hover-background);
 | 
			
		||||
    --launcher-pane-horiz-button-hover-shadow: unset;
 | 
			
		||||
    --launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
 | 
			
		||||
    --launcher-pane-horiz-background-color-bgfx: #ffffffb3; /* When background effects enabled */
 | 
			
		||||
    --launcher-pane-horiz-border-color-bgfx: #00000026; /* When background effects enabled */
 | 
			
		||||
 | 
			
		||||
    --protected-session-active-icon-color: #16b516;
 | 
			
		||||
    --sync-status-error-pulse-color: #ff5528;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
@import url(./pages.css);
 | 
			
		||||
@import url(./ribbon.css);
 | 
			
		||||
@import url(./notes/text.css);
 | 
			
		||||
@import url(./notes/collections/table.css);
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: "Inter";
 | 
			
		||||
@@ -83,12 +82,6 @@
 | 
			
		||||
    --tab-note-icons: true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.backdrop-effects-disabled {
 | 
			
		||||
    /* Backdrop effects are disabled, replace the menu background color with the
 | 
			
		||||
     * no-backdrop fallback color */
 | 
			
		||||
    --menu-background-color: var(--menu-background-color-no-backdrop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * MENUS
 | 
			
		||||
 *
 | 
			
		||||
@@ -190,24 +183,20 @@ html body .dropdown-item[disabled] {
 | 
			
		||||
 | 
			
		||||
/* Menu item icon */
 | 
			
		||||
.dropdown-item .bx {
 | 
			
		||||
    translate: 0 var(--menu-item-icon-vert-offset);
 | 
			
		||||
    transform: translateY(var(--menu-item-icon-vert-offset));
 | 
			
		||||
    color: var(--menu-item-icon-color) !important;
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Menu item keyboard shortcut */
 | 
			
		||||
.dropdown-item kbd {
 | 
			
		||||
    margin-left: 16px;
 | 
			
		||||
    font-family: unset !important;
 | 
			
		||||
    font-size: unset !important;
 | 
			
		||||
    color: var(--menu-item-keyboard-shortcut-color) !important;
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item span.keyboard-shortcut {
 | 
			
		||||
    color: var(--menu-item-keyboard-shortcut-color) !important;
 | 
			
		||||
    margin-left: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-divider {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    border-color: transparent !important;
 | 
			
		||||
@@ -329,8 +318,6 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
 | 
			
		||||
 | 
			
		||||
#toast-container .toast .toast-body {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -470,11 +457,6 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-list-wrapper .note-book-card .note-book-content.type-image .rendered-content,
 | 
			
		||||
.note-list-wrapper .note-book-card .note-book-content.type-pdf .rendered-content {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-list-wrapper .note-book-card .note-book-content .rendered-content.text-with-ellipsis {
 | 
			
		||||
    padding: 1rem !important;
 | 
			
		||||
}
 | 
			
		||||
@@ -542,9 +524,10 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* List item */
 | 
			
		||||
.jump-to-note-dialog .aa-suggestion,
 | 
			
		||||
.note-detail-empty .aa-suggestion {
 | 
			
		||||
.jump-to-note-dialog .aa-suggestions div,
 | 
			
		||||
.note-detail-empty .aa-suggestions div {
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    padding: 6px 12px;
 | 
			
		||||
    color: var(--menu-text-color);
 | 
			
		||||
    cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -128,15 +128,10 @@ div.tn-tool-dialog {
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-header {
 | 
			
		||||
    padding: unset !important;
 | 
			
		||||
    padding-bottom: 26px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-body {
 | 
			
		||||
    padding: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-to-note-dialog .modal-footer {
 | 
			
		||||
    padding-top: 26px;
 | 
			
		||||
    padding: 26px 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Search box wrapper */
 | 
			
		||||
@@ -233,16 +228,16 @@ div.tn-tool-dialog {
 | 
			
		||||
 | 
			
		||||
/* Item title link */
 | 
			
		||||
 | 
			
		||||
.recent-changes-content ul li a {
 | 
			
		||||
.recent-changes-content ul li .note-title a {
 | 
			
		||||
    color: currentColor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.recent-changes-content ul li a:hover {
 | 
			
		||||
.recent-changes-content ul li .note-title a:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Item title for deleted notes */
 | 
			
		||||
.recent-changes-content ul li.deleted-note .note-title {
 | 
			
		||||
.recent-changes-content ul li.deleted-note .note-title > .note-title {
 | 
			
		||||
    text-decoration: line-through;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@
 | 
			
		||||
button.btn.btn-primary,
 | 
			
		||||
button.btn.btn-secondary,
 | 
			
		||||
button.btn.btn-sm:not(.select-button),
 | 
			
		||||
button.btn.btn-success,
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text {
 | 
			
		||||
button.btn.btn-success {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
@@ -22,8 +21,7 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
 | 
			
		||||
button.btn.btn-primary:hover,
 | 
			
		||||
button.btn.btn-secondary:hover,
 | 
			
		||||
button.btn.btn-sm:not(.select-button):hover,
 | 
			
		||||
button.btn.btn-success:hover,
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):hover {
 | 
			
		||||
button.btn.btn-success:hover {
 | 
			
		||||
    background: var(--cmd-button-hover-background-color);
 | 
			
		||||
    color: var(--cmd-button-hover-text-color);
 | 
			
		||||
}
 | 
			
		||||
@@ -31,8 +29,7 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
 | 
			
		||||
button.btn.btn-primary:active,
 | 
			
		||||
button.btn.btn-secondary:active,
 | 
			
		||||
button.btn.btn-sm:not(.select-button):active,
 | 
			
		||||
button.btn.btn-success:active,
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):active {
 | 
			
		||||
button.btn.btn-success:active {
 | 
			
		||||
    opacity: 0.85;
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
    background: var(--cmd-button-background-color) !important;
 | 
			
		||||
@@ -43,16 +40,14 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
 | 
			
		||||
button.btn.btn-primary:disabled,
 | 
			
		||||
button.btn.btn-secondary:disabled,
 | 
			
		||||
button.btn.btn-sm:not(.select-button):disabled,
 | 
			
		||||
button.btn.btn-success:disabled,
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text.ck-disabled {
 | 
			
		||||
button.btn.btn-success:disabled {
 | 
			
		||||
    opacity: var(--cmd-button-disabled-opacity);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.btn.btn-primary:focus-visible,
 | 
			
		||||
button.btn.btn-secondary:focus-visible,
 | 
			
		||||
button.btn.btn-sm:not(.select-button):focus-visible,
 | 
			
		||||
button.btn.btn-success:focus-visible,
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .ck-button-replaceall, .ck-button-replace).ck-button_with-text:not(.ck-disabled):focus-visible {
 | 
			
		||||
button.btn.btn-success:focus-visible {
 | 
			
		||||
    outline: 2px solid var(--input-focus-outline-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -154,11 +149,8 @@ input[type="password"],
 | 
			
		||||
input[type="date"],
 | 
			
		||||
input[type="time"],
 | 
			
		||||
input[type="datetime-local"],
 | 
			
		||||
:root input.ck.ck-input-text,
 | 
			
		||||
:root input.ck.ck-input-number,
 | 
			
		||||
textarea.form-control,
 | 
			
		||||
textarea,
 | 
			
		||||
:root textarea.ck.ck-textarea,
 | 
			
		||||
.tn-input-field {
 | 
			
		||||
    outline: 3px solid transparent;
 | 
			
		||||
    outline-offset: 6px;
 | 
			
		||||
@@ -175,11 +167,8 @@ input[type="password"]:hover,
 | 
			
		||||
input[type="date"]:hover,
 | 
			
		||||
input[type="time"]:hover,
 | 
			
		||||
input[type="datetime-local"]:hover,
 | 
			
		||||
:root input.ck.ck-input-text:not([readonly="true"]):hover,
 | 
			
		||||
:root input.ck.ck-input-number:not([readonly="true"]):hover,
 | 
			
		||||
textarea.form-control:hover,
 | 
			
		||||
textarea:hover,
 | 
			
		||||
:root textarea.ck.ck-textarea:hover,
 | 
			
		||||
.tn-input-field:hover {
 | 
			
		||||
    background: var(--input-hover-background);
 | 
			
		||||
    color: var(--input-hover-color);
 | 
			
		||||
@@ -192,11 +181,8 @@ input[type="password"]:focus,
 | 
			
		||||
input[type="date"]:focus,
 | 
			
		||||
input[type="time"]:focus,
 | 
			
		||||
input[type="datetime-local"]:focus,
 | 
			
		||||
:root input.ck.ck-input-text:focus,
 | 
			
		||||
:root input.ck.ck-input-number:focus,
 | 
			
		||||
textarea.form-control:focus,
 | 
			
		||||
textarea:focus,
 | 
			
		||||
:root textarea.ck.ck-textarea:focus,
 | 
			
		||||
.tn-input-field:focus,
 | 
			
		||||
.tn-input-field:focus-within {
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
@@ -469,7 +455,6 @@ optgroup {
 | 
			
		||||
        left: 0;
 | 
			
		||||
        width: var(--box-size);
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        margin: unset;
 | 
			
		||||
        opacity: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
:root .tabulator {
 | 
			
		||||
    --col-header-hover-background-color: var(--hover-item-background-color);
 | 
			
		||||
    --col-header-arrow-active-color: var(--active-item-text-color);
 | 
			
		||||
    --col-header-arrow-inactive-color: var(--main-border-color);
 | 
			
		||||
 | 
			
		||||
    --row-moving-background-color: var(--more-accented-background-color);
 | 
			
		||||
 | 
			
		||||
    --cell-editable-hover-outline-color: var(--input-focus-outline-color);
 | 
			
		||||
 | 
			
		||||
    --cell-editing-border-color: var(--input-focus-outline-color);
 | 
			
		||||
    --cell-editing-background-color: var(--input-background-color);
 | 
			
		||||
    --cell-editing-text-color: var(--input-text-color);
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
 :root {
 | 
			
		||||
    --ck-font-face: var(--main-font-family);
 | 
			
		||||
    --ck-input-label-height: 1.5em;
 | 
			
		||||
 }
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -308,11 +307,6 @@
 | 
			
		||||
    fill: black !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Hex color input box prefix */
 | 
			
		||||
:root .ck.ck-color-selector .ck-color-picker__hash-view {
 | 
			
		||||
    margin-top: var(--ck-input-label-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Numbered list */
 | 
			
		||||
 | 
			
		||||
:root .ck.ck-list-properties_with-numbered-properties .ck.ck-list-styles-list {
 | 
			
		||||
@@ -369,86 +363,19 @@
 | 
			
		||||
    color: var(--accent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Text snippet dropdown */
 | 
			
		||||
/* Action buttons */
 | 
			
		||||
 | 
			
		||||
div.ck-template-form {
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
:root .ck-link-actions button.ck-button,
 | 
			
		||||
:root .ck-link-form button.ck-button {
 | 
			
		||||
    --ck-border-radius: 6px;
 | 
			
		||||
    
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.ck-template-form .ck-labeled-field-view {
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Template item */
 | 
			
		||||
 | 
			
		||||
:root div.ck-template-form li.ck-list__item button.ck-template-button {
 | 
			
		||||
    padding: 4px 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Template icon */
 | 
			
		||||
:root .ck-template-form .ck-button__icon {
 | 
			
		||||
    --ck-spacing-medium: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root div.ck-template-form  .note-icon {
 | 
			
		||||
    color: var(--menu-item-icon-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Template name */
 | 
			
		||||
div.ck-template-form .ck-template-form__text-part {
 | 
			
		||||
    color: var(--hover-item-text-color);
 | 
			
		||||
    font-size: .9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.ck-template-form .ck-template-form__text-part mark {
 | 
			
		||||
    background: unset;
 | 
			
		||||
    color: var(--quick-search-result-highlight-color);
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Template description */
 | 
			
		||||
:root div.ck-template-form .ck-template-form__description {
 | 
			
		||||
    opacity: .5;
 | 
			
		||||
    font-size: .9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Messages */
 | 
			
		||||
div.ck-template-form .ck-search__info > span {
 | 
			
		||||
    line-height: initial;
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.ck-template-form .ck-search__info span:nth-child(2) {
 | 
			
		||||
    display: block;
 | 
			
		||||
    opacity: .5;
 | 
			
		||||
    margin-top: 8px;
 | 
			
		||||
    font-size: .9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Link dropdown */
 | 
			
		||||
 | 
			
		||||
:root .ck.ck-form.ck-link-form ul.ck-link-form__providers-list {
 | 
			
		||||
    border-top: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Math popup */
 | 
			
		||||
 | 
			
		||||
.ck-math-form .ck-labeled-field-view {
 | 
			
		||||
    --ck-input-label-height: 0;
 | 
			
		||||
 | 
			
		||||
    margin-inline-end: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Emoji dropdown */
 | 
			
		||||
 | 
			
		||||
.ck-emoji-picker-form .ck-emoji__search .ck-button_with-text:not(.ck-list-item-button) {
 | 
			
		||||
    margin-top: var(--ck-input-label-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Find and replace dialog */
 | 
			
		||||
 | 
			
		||||
.ck-find-and-replace-form .ck-find-and-replace-form__inputs button {
 | 
			
		||||
    margin-top: var(--ck-input-label-height);
 | 
			
		||||
:root .ck-link-actions button.ck-button:hover,
 | 
			
		||||
:root .ck-link-form button.ck-button:hover {
 | 
			
		||||
    background: var(--hover-item-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Mention list (the autocompletion list for emojis, labels and relations)  */
 | 
			
		||||
@@ -465,58 +392,6 @@ div.ck-template-form .ck-search__info span:nth-child(2) {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * FORMS
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Buttons
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck-button_with-text {
 | 
			
		||||
    --ck-color-text: var(--cmd-button-text-color);
 | 
			
		||||
 | 
			
		||||
    min-width: 60px;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Text boxes
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.ck.ck-labeled-field-view {
 | 
			
		||||
    padding-top: var(--ck-input-label-height) !important; /* Create space for the label */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck.ck-labeled-field-view > .ck.ck-labeled-field-view__input-wrapper > label.ck.ck-label {
 | 
			
		||||
    /* Move the label above the text box regardless of the text box state */
 | 
			
		||||
    transform: translate(0, calc(-.2em - var(--ck-input-label-height))) !important;
 | 
			
		||||
    
 | 
			
		||||
    padding-left: 0 !important;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    font-size: .85em;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root input.ck.ck-input-text[readonly="true"] {
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
    background: var(--input-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Forms */
 | 
			
		||||
 | 
			
		||||
:root .ck.ck-form__row.ck-form__row_with-submit > :not(:first-child) {
 | 
			
		||||
    margin-inline-start: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck.ck-form__row_with-submit button {
 | 
			
		||||
    margin-top: var(--ck-input-label-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ck.ck-form__header {
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * EDITOR'S CONTENT
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,7 @@
 | 
			
		||||
    background: var(--background) !important;
 | 
			
		||||
    color: var(--color) !important;
 | 
			
		||||
    line-height: unset;
 | 
			
		||||
    cursor: help;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
 | 
			
		||||
@@ -105,6 +106,18 @@
 | 
			
		||||
    --color: var(--main-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Tooltip */
 | 
			
		||||
 | 
			
		||||
.tooltip .table-schema {
 | 
			
		||||
    font-family: var(--monospace-font-family);
 | 
			
		||||
    font-size: .85em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Data type */
 | 
			
		||||
.tooltip .table-schema td:nth-child(2) {
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * NOTE MAP
 | 
			
		||||
 */
 | 
			
		||||
@@ -169,6 +182,8 @@ div.note-detail-empty {
 | 
			
		||||
 | 
			
		||||
.options-section:not(.tn-no-card) {
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    min-width: var(--options-card-min-width);
 | 
			
		||||
    max-width: var(--options-card-max-width);
 | 
			
		||||
    border-radius: 12px;
 | 
			
		||||
    border: 1px solid var(--card-border-color) !important;
 | 
			
		||||
    box-shadow: var(--card-box-shadow);
 | 
			
		||||
@@ -177,11 +192,6 @@ div.note-detail-empty {
 | 
			
		||||
    margin-bottom: calc(var(--options-title-offset) + 26px) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.desktop .option-section:not(.tn-no-card) {
 | 
			
		||||
    min-width: var(--options-card-min-width);
 | 
			
		||||
    max-width: var(--options-card-max-width);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-detail-content-widget-content.options {
 | 
			
		||||
    --default-padding: 15px;
 | 
			
		||||
    padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
 | 
			
		||||
@@ -223,6 +233,11 @@ body.desktop .option-section:not(.tn-no-card) {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.options-section .options-mime-types {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.options-section .form-group {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,23 +36,32 @@ body.mobile {
 | 
			
		||||
 | 
			
		||||
/* #region Mica */
 | 
			
		||||
body.background-effects.platform-win32 {
 | 
			
		||||
    --background-material: tabbed;
 | 
			
		||||
    --launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
 | 
			
		||||
    --launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
 | 
			
		||||
    --launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
 | 
			
		||||
    --tab-background-color: var(--window-background-color-bgfx);
 | 
			
		||||
    --new-tab-button-background: var(--window-background-color-bgfx);
 | 
			
		||||
    --launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.15);
 | 
			
		||||
    --launcher-pane-horiz-background-color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
    --launcher-pane-vert-background-color: rgba(255, 255, 255, 0.055);
 | 
			
		||||
    --tab-background-color: transparent;
 | 
			
		||||
    --new-tab-button-background: transparent;
 | 
			
		||||
    --active-tab-background-color: var(--launcher-pane-horiz-background-color);
 | 
			
		||||
    --background-material: tabbed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    body.background-effects.platform-win32 {
 | 
			
		||||
        --launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
        --launcher-pane-horiz-background-color: rgba(255, 255, 255, 0.09);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.background-effects.platform-win32.layout-vertical {
 | 
			
		||||
    --left-pane-background-color: var(--window-background-color-bgfx);
 | 
			
		||||
    --left-pane-background-color: transparent;
 | 
			
		||||
    --left-pane-item-hover-background: rgba(127, 127, 127, 0.05);
 | 
			
		||||
    --background-material: mica;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.background-effects.platform-win32,
 | 
			
		||||
body.background-effects.platform-win32 #root-widget {
 | 
			
		||||
    background: var(--window-background-color-bgfx) !important;
 | 
			
		||||
body.background-effects.platform-win32 #root-widget,
 | 
			
		||||
body.background-effects.platform-win32 #launcher-pane .launcher-button {
 | 
			
		||||
    background: transparent !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
 | 
			
		||||
@@ -81,7 +90,7 @@ body.background-effects.zen #root-widget {
 | 
			
		||||
 * Gutter
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.gutter {
 | 
			
		||||
 .gutter {
 | 
			
		||||
    background: var(--gutter-color) !important;
 | 
			
		||||
    transition: background 150ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
@@ -321,6 +330,7 @@ body.layout-horizontal > .horizontal {
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
 .calendar-dropdown-widget {
 | 
			
		||||
    width: unset !important;
 | 
			
		||||
    padding: 12px;
 | 
			
		||||
    color: var(--calendar-color);
 | 
			
		||||
    user-select: none;
 | 
			
		||||
@@ -566,18 +576,25 @@ div.quick-search .search-button.show {
 | 
			
		||||
 * Quick search results
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
div.quick-search .dropdown-menu {
 | 
			
		||||
    --quick-search-item-delimiter-color: transparent;
 | 
			
		||||
    --menu-item-icon-vert-offset: -.065em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Item */
 | 
			
		||||
.quick-search .dropdown-menu *.dropdown-item {
 | 
			
		||||
    padding: 8px 12px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.quick-search .quick-search-item-icon {
 | 
			
		||||
    vertical-align: text-bottom;
 | 
			
		||||
/* Note icon */
 | 
			
		||||
.quick-search .dropdown-menu .dropdown-item > .bx {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Note title */
 | 
			
		||||
.quick-search .dropdown-menu .dropdown-item > a {
 | 
			
		||||
    color: var(--menu-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.quick-search .dropdown-menu .dropdown-item > a:hover {
 | 
			
		||||
    --hover-item-background-color: transparent;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Note path */
 | 
			
		||||
@@ -588,24 +605,6 @@ div.quick-search .dropdown-menu {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Note content snippet */
 | 
			
		||||
:root .quick-search .search-result-content {
 | 
			
		||||
    background-color: var(--quick-search-result-content-background);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Highlighted search terms */
 | 
			
		||||
:root .quick-search .search-result-title b,
 | 
			
		||||
:root .quick-search .search-result-content b,
 | 
			
		||||
:root .quick-search .search-result-attributes b {
 | 
			
		||||
    color: var(--quick-search-result-highlight-color);
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.quick-search div.dropdown-divider {
 | 
			
		||||
    margin: 8px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * TREE PANE
 | 
			
		||||
 */
 | 
			
		||||
@@ -878,80 +877,6 @@ body.layout-horizontal .tab-row-container {
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container {
 | 
			
		||||
    border-bottom: unset !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .note-tab-wrapper {
 | 
			
		||||
    top: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .toggle-button {
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .toggle-button:after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: -10px;
 | 
			
		||||
    right: -10px;
 | 
			
		||||
    top: 29px;
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left:after,
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right:after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0px;
 | 
			
		||||
    right: 0px;
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-tab[active]:before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: -32768px;
 | 
			
		||||
    top: var(--tab-height);
 | 
			
		||||
    right: calc(100% - 1px);
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-tab[active]:after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 100%;
 | 
			
		||||
    top: var(--tab-height);
 | 
			
		||||
    right: 0;
 | 
			
		||||
    width: 100vw;
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-new-tab:before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: -4px;
 | 
			
		||||
    top: calc(var(--tab-height), -1);
 | 
			
		||||
    right: 0;
 | 
			
		||||
    width: 100vw;
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.layout-vertical.electron.platform-darwin .tab-row-container {
 | 
			
		||||
    border-bottom: 1px solid var(--subtle-border-color);
 | 
			
		||||
}
 | 
			
		||||
@@ -1167,11 +1092,6 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
 | 
			
		||||
    /* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.split-note-container-widget > .gutter {
 | 
			
		||||
    background: var(--root-background) !important;
 | 
			
		||||
    transition: background 150ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Ribbon & note header
 | 
			
		||||
 */
 | 
			
		||||
@@ -1180,6 +1100,10 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
 | 
			
		||||
    margin-bottom: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-split:not(.hidden-ext) + .note-split:not(.hidden-ext) {
 | 
			
		||||
    border-left: 4px solid var(--root-background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes note-entrance {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
@@ -1451,7 +1375,7 @@ div.floating-buttons-children .floating-button:active {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* The first visible floating button */
 | 
			
		||||
div.floating-buttons-children > *:first-child {
 | 
			
		||||
div.floating-buttons-children > *:nth-child(1 of .visible) {
 | 
			
		||||
    --border-radius: var(--border-radius-size) 0 0 var(--border-radius-size);
 | 
			
		||||
    border-radius: var(--border-radius);
 | 
			
		||||
}
 | 
			
		||||
@@ -1553,6 +1477,13 @@ div.floating-buttons-children .close-floating-buttons:has(.close-floating-button
 | 
			
		||||
    padding-inline-start: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Copy image reference */
 | 
			
		||||
 | 
			
		||||
.floating-buttons .copy-image-reference-button .hidden-image-copy {
 | 
			
		||||
    /* Take out of the the hidden image from flexbox to prevent the layout being affected */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Code, relation map buttons */
 | 
			
		||||
 | 
			
		||||
.floating-buttons .code-buttons-widget,
 | 
			
		||||
@@ -1748,41 +1679,3 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    transition: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Canvas **/
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
    --border-radius-lg: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .Island {
 | 
			
		||||
    backdrop-filter: var(--dropdown-backdrop-filter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .Island.App-toolbar {
 | 
			
		||||
    --island-bg-color: var(--floating-button-background-color);
 | 
			
		||||
    --shadow-island: 1px 1px 1px var(--floating-button-shadow-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .dropdown-menu {
 | 
			
		||||
    border: unset !important;
 | 
			
		||||
    box-shadow: unset !important;    
 | 
			
		||||
    background-color: transparent !important;
 | 
			
		||||
    --island-bg-color: var(--menu-background-color);
 | 
			
		||||
    --shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
 | 
			
		||||
    --default-border-color: var(--bs-dropdown-divider-bg);
 | 
			
		||||
    --button-hover-bg: var(--hover-item-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .dropdown-menu .dropdown-menu-container {
 | 
			
		||||
    border-radius: var(--dropdown-border-radius);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .dropdown-menu .dropdown-menu-container > div:not([class]):not(:last-child) {
 | 
			
		||||
    margin-left: calc(var(--padding) * var(--space-factor) * -1) !important;
 | 
			
		||||
    margin-right: calc(var(--padding) * var(--space-factor) * -1) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.excalidraw .dropdown-menu:before {
 | 
			
		||||
    content: unset !important;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,185 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "about": {
 | 
			
		||||
        "title": "Sobre Trilium Notes",
 | 
			
		||||
        "homepage": "Pàgina principal:"
 | 
			
		||||
    },
 | 
			
		||||
    "add_link": {
 | 
			
		||||
        "note": "Nota"
 | 
			
		||||
    },
 | 
			
		||||
    "branch_prefix": {
 | 
			
		||||
        "prefix": "Prefix: ",
 | 
			
		||||
        "save": "Desa"
 | 
			
		||||
    },
 | 
			
		||||
    "bulk_actions": {
 | 
			
		||||
        "labels": "Etiquetes",
 | 
			
		||||
        "relations": "Relacions",
 | 
			
		||||
        "notes": "Notes",
 | 
			
		||||
        "other": "Altres"
 | 
			
		||||
    },
 | 
			
		||||
    "confirm": {
 | 
			
		||||
        "confirmation": "Confirmació",
 | 
			
		||||
        "cancel": "Cancel·la",
 | 
			
		||||
        "ok": "OK"
 | 
			
		||||
    },
 | 
			
		||||
    "delete_notes": {
 | 
			
		||||
        "close": "Tanca",
 | 
			
		||||
        "cancel": "Cancel·la",
 | 
			
		||||
        "ok": "OK"
 | 
			
		||||
    },
 | 
			
		||||
    "export": {
 | 
			
		||||
        "close": "Tanca",
 | 
			
		||||
        "export": "Exporta"
 | 
			
		||||
    },
 | 
			
		||||
    "help": {
 | 
			
		||||
        "troubleshooting": "Solució de problemes",
 | 
			
		||||
        "other": "Altres"
 | 
			
		||||
    },
 | 
			
		||||
    "import": {
 | 
			
		||||
        "options": "Opcions",
 | 
			
		||||
        "import": "Importa"
 | 
			
		||||
    },
 | 
			
		||||
    "include_note": {
 | 
			
		||||
        "label_note": "Nota"
 | 
			
		||||
    },
 | 
			
		||||
    "info": {
 | 
			
		||||
        "closeButton": "Tanca",
 | 
			
		||||
        "okButton": "OK"
 | 
			
		||||
    },
 | 
			
		||||
    "note_type_chooser": {
 | 
			
		||||
        "templates": "Plantilles:"
 | 
			
		||||
    },
 | 
			
		||||
    "prompt": {
 | 
			
		||||
        "title": "Sol·licitud",
 | 
			
		||||
        "defaultTitle": "Sol·licitud"
 | 
			
		||||
    },
 | 
			
		||||
    "protected_session_password": {
 | 
			
		||||
        "close_label": "Tanca"
 | 
			
		||||
    },
 | 
			
		||||
    "recent_changes": {
 | 
			
		||||
        "undelete_link": "recuperar"
 | 
			
		||||
    },
 | 
			
		||||
    "revisions": {
 | 
			
		||||
        "restore_button": "Restaura",
 | 
			
		||||
        "delete_button": "Suprimeix",
 | 
			
		||||
        "download_button": "Descarrega",
 | 
			
		||||
        "mime": "MIME: ",
 | 
			
		||||
        "preview": "Vista prèvia:"
 | 
			
		||||
    },
 | 
			
		||||
    "sort_child_notes": {
 | 
			
		||||
        "title": "títol",
 | 
			
		||||
        "ascending": "ascendent",
 | 
			
		||||
        "descending": "descendent",
 | 
			
		||||
        "folders": "Carpetes"
 | 
			
		||||
    },
 | 
			
		||||
    "upload_attachments": {
 | 
			
		||||
        "options": "Opcions",
 | 
			
		||||
        "upload": "Puja"
 | 
			
		||||
    },
 | 
			
		||||
    "attribute_detail": {
 | 
			
		||||
        "name": "Nom",
 | 
			
		||||
        "value": "Valor",
 | 
			
		||||
        "promoted": "Destacat",
 | 
			
		||||
        "promoted_alias": "Àlies",
 | 
			
		||||
        "multiplicity": "Multiplicitat",
 | 
			
		||||
        "label_type": "Tipus",
 | 
			
		||||
        "text": "Text",
 | 
			
		||||
        "number": "Número",
 | 
			
		||||
        "boolean": "Booleà",
 | 
			
		||||
        "date": "Data",
 | 
			
		||||
        "time": "Hora",
 | 
			
		||||
        "url": "URL",
 | 
			
		||||
        "precision": "Precisió",
 | 
			
		||||
        "digits": "dígits",
 | 
			
		||||
        "inheritable": "Heretable",
 | 
			
		||||
        "delete": "Suprimeix",
 | 
			
		||||
        "color_type": "Color"
 | 
			
		||||
    },
 | 
			
		||||
    "rename_label": {
 | 
			
		||||
        "to": "Per"
 | 
			
		||||
    },
 | 
			
		||||
    "move_note": {
 | 
			
		||||
        "to": "a"
 | 
			
		||||
    },
 | 
			
		||||
    "add_relation": {
 | 
			
		||||
        "to": "a"
 | 
			
		||||
    },
 | 
			
		||||
    "rename_relation": {
 | 
			
		||||
        "to": "Per"
 | 
			
		||||
    },
 | 
			
		||||
    "update_relation_target": {
 | 
			
		||||
        "to": "a"
 | 
			
		||||
    },
 | 
			
		||||
    "attachments_actions": {
 | 
			
		||||
        "download": "Descarrega"
 | 
			
		||||
    },
 | 
			
		||||
    "calendar": {
 | 
			
		||||
        "mon": "Dl",
 | 
			
		||||
        "tue": "Dt",
 | 
			
		||||
        "wed": "dc",
 | 
			
		||||
        "thu": "Dj",
 | 
			
		||||
        "fri": "Dv",
 | 
			
		||||
        "sat": "Ds",
 | 
			
		||||
        "sun": "Dg",
 | 
			
		||||
        "january": "Gener",
 | 
			
		||||
        "febuary": "Febrer",
 | 
			
		||||
        "march": "Març",
 | 
			
		||||
        "april": "Abril",
 | 
			
		||||
        "may": "Maig",
 | 
			
		||||
        "june": "Juny",
 | 
			
		||||
        "july": "Juliol",
 | 
			
		||||
        "august": "Agost",
 | 
			
		||||
        "september": "Setembre",
 | 
			
		||||
        "october": "Octubre",
 | 
			
		||||
        "november": "Novembre",
 | 
			
		||||
        "december": "Desembre"
 | 
			
		||||
    },
 | 
			
		||||
    "global_menu": {
 | 
			
		||||
        "menu": "Menú",
 | 
			
		||||
        "options": "Opcions",
 | 
			
		||||
        "zoom": "Zoom",
 | 
			
		||||
        "advanced": "Avançat",
 | 
			
		||||
        "logout": "Tanca la sessió"
 | 
			
		||||
    },
 | 
			
		||||
    "zpetne_odkazy": {
 | 
			
		||||
        "relation": "relació"
 | 
			
		||||
    },
 | 
			
		||||
    "note_icon": {
 | 
			
		||||
        "category": "Categoria:",
 | 
			
		||||
        "search": "Cerca:"
 | 
			
		||||
    },
 | 
			
		||||
    "basic_properties": {
 | 
			
		||||
        "editable": "Editable",
 | 
			
		||||
        "language": "Llengua"
 | 
			
		||||
    },
 | 
			
		||||
    "book_properties": {
 | 
			
		||||
        "grid": "Graella",
 | 
			
		||||
        "list": "Llista",
 | 
			
		||||
        "collapse": "Replega",
 | 
			
		||||
        "expand": "Desplega",
 | 
			
		||||
        "calendar": "Calendari",
 | 
			
		||||
        "table": "Taula",
 | 
			
		||||
        "board": "Tauler"
 | 
			
		||||
    },
 | 
			
		||||
    "edited_notes": {
 | 
			
		||||
        "deleted": "(suprimit)"
 | 
			
		||||
    },
 | 
			
		||||
    "file_properties": {
 | 
			
		||||
        "download": "Descarrega",
 | 
			
		||||
        "open": "Obre",
 | 
			
		||||
        "title": "Fitxer"
 | 
			
		||||
    },
 | 
			
		||||
    "image_properties": {
 | 
			
		||||
        "download": "Descarrega",
 | 
			
		||||
        "open": "Obre",
 | 
			
		||||
        "title": "Imatge"
 | 
			
		||||
    },
 | 
			
		||||
    "note_info_widget": {
 | 
			
		||||
        "created": "Creat",
 | 
			
		||||
        "modified": "Modificat",
 | 
			
		||||
        "type": "Tipus",
 | 
			
		||||
        "calculate": "calcula"
 | 
			
		||||
    },
 | 
			
		||||
    "note_paths": {
 | 
			
		||||
        "archived": "Arxivat"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user