diff --git a/.github/actions/build-server/action.yml b/.github/actions/build-server/action.yml index 7e15f1e20..cc7eb0e87 100644 --- a/.github/actions/build-server/action.yml +++ b/.github/actions/build-server/action.yml @@ -10,7 +10,7 @@ runs: steps: - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 cache: "pnpm" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..6558788c8 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,183 @@ +# 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' + + # 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' + +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" + + # 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 + }); + } diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index c4789cb2c..ceed464c8 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -28,12 +28,15 @@ jobs: - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 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 diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 7b2ee4ecd..a1b38782d 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -44,7 +44,7 @@ jobs: - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 cache: "pnpm" @@ -144,7 +144,7 @@ jobs: uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 cache: 'pnpm' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d9edb2cc2..ab913d402 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 cache: 'pnpm' @@ -78,7 +78,7 @@ jobs: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }} - name: Publish release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@v2.3.3 if: ${{ github.event_name != 'pull_request' }} with: make_latest: false @@ -119,7 +119,7 @@ jobs: arch: ${{ matrix.arch }} - name: Publish release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@v2.3.3 if: ${{ github.event_name != 'pull_request' }} with: make_latest: false diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 711aaaaeb..07ad94fd7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22 cache: 'pnpm' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 133d40622..16dec7492 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - name: Set up node & dependencies - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 cache: 'pnpm' @@ -114,7 +114,7 @@ jobs: path: upload - name: Publish stable release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@v2.3.3 with: draft: false body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md diff --git a/.gitignore b/.gitignore index 66e9781f9..b2c4e3c46 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ upload *.tsbuildinfo /result -.svelte-kit \ No newline at end of file +.svelte-kit + +# docs +site/ diff --git a/_regroup/package.json b/_regroup/package.json index 01eeb1bf0..b61ea536a 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -38,10 +38,10 @@ "@playwright/test": "1.55.0", "@stylistic/eslint-plugin": "5.3.1", "@types/express": "5.0.3", - "@types/node": "22.18.0", + "@types/node": "22.18.1", "@types/yargs": "17.0.33", "@vitest/coverage-v8": "3.2.4", - "eslint": "9.34.0", + "eslint": "9.35.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", "jsdoc": "4.0.4", diff --git a/apps/client/package.json b/apps/client/package.json index ca26c0b29..b2abba919 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -15,7 +15,7 @@ "circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular" }, "dependencies": { - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@excalidraw/excalidraw": "0.18.0", "@fullcalendar/core": "6.1.19", "@fullcalendar/daygrid": "6.1.19", @@ -24,7 +24,7 @@ "@fullcalendar/multimonth": "6.1.19", "@fullcalendar/timegrid": "6.1.19", "@maplibre/maplibre-gl-leaflet": "0.1.3", - "@mermaid-js/layout-elk": "0.1.9", + "@mermaid-js/layout-elk": "0.2.0", "@mind-elixir/node-menu": "5.0.0", "@popperjs/core": "2.11.8", "@triliumnext/ckeditor5": "workspace:*", @@ -39,9 +39,9 @@ "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", "draggabilly": "3.0.0", - "force-graph": "1.50.1", + "force-graph": "1.51.0", "globals": "16.3.0", - "i18next": "25.4.2", + "i18next": "25.5.2", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", "jquery.fancytree": "2.38.5", @@ -52,8 +52,8 @@ "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "16.2.1", - "mermaid": "11.10.1", - "mind-elixir": "5.0.6", + "mermaid": "11.11.0", + "mind-elixir": "5.1.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", "preact": "10.27.1", @@ -69,7 +69,7 @@ "@types/bootstrap": "5.2.10", "@types/jquery": "3.5.33", "@types/leaflet": "1.9.20", - "@types/leaflet-gpx": "1.3.7", + "@types/leaflet-gpx": "1.3.8", "@types/mark.js": "8.11.12", "@types/tabulator-tables": "6.2.10", "copy-webpack-plugin": "13.0.1", diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index b7eceffa2..a324152c9 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -22,6 +22,7 @@ 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 = ` @@ -131,30 +132,33 @@ 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", "column") + new ScreenContainer("detail", "row") .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 FlexContainer("row") - .contentSized() - .css("font-size", "larger") - .css("align-items", "center") - .child() - .child() - .child() + new NoteWrapperWidget() + .child( + new FlexContainer("row") + .contentSized() + .css("font-size", "larger") + .css("align-items", "center") + .child() + .child() + .child() + ) + .child() + .child() + .child(new PromotedAttributesWidget()) + .child( + new ScrollingContainer() + .filling() + .contentSized() + .child(new NoteDetailWidget()) + .child(new NoteListWidget(false)) + .child() + ) + .child() ) - .child() - .child() - .child(new PromotedAttributesWidget()) - .child( - new ScrollingContainer() - .filling() - .contentSized() - .child(new NoteDetailWidget()) - .child(new NoteListWidget(false)) - .child() - ) - .child() ) ) .child( diff --git a/apps/client/src/services/resizer.ts b/apps/client/src/services/resizer.ts index e0dc40995..54a11e801 100644 --- a/apps/client/src/services/resizer.ts +++ b/apps/client/src/services/resizer.ts @@ -10,6 +10,10 @@ let leftInstance: ReturnType | null; let rightPaneWidth: number; let rightInstance: ReturnType | null; +const noteSplitMap = new Map | undefined>(); // key: a group of ntxIds, value: the corresponding Split instance +const noteSplitRafMap = new Map(); +let splitNoteContainer: HTMLElement | undefined; + function setupLeftPaneResizer(leftPaneVisible: boolean) { if (leftInstance) { leftInstance.destroy(); @@ -83,7 +87,86 @@ 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(':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 + setupRightPaneResizer, + setupNoteSplitResizer, + delNoteSplitResizer, + moveNoteSplitResizer }; diff --git a/apps/client/src/services/shortcuts.spec.ts b/apps/client/src/services/shortcuts.spec.ts index 1a20f9a84..ec9a0a581 100644 --- a/apps/client/src/services/shortcuts.spec.ts +++ b/apps/client/src/services/shortcuts.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js"; +import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js"; // Mock utils module vi.mock("./utils.js", () => ({ @@ -320,4 +320,36 @@ describe("shortcuts", () => { 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); + }); + }); }); diff --git a/apps/client/src/services/shortcuts.ts b/apps/client/src/services/shortcuts.ts index c0e136c6c..a2aca5d80 100644 --- a/apps/client/src/services/shortcuts.ts +++ b/apps/client/src/services/shortcuts.ts @@ -40,6 +40,24 @@ 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; +} + function removeGlobalShortcut(namespace: string) { bindGlobalShortcut("", null, namespace); } @@ -68,6 +86,13 @@ function bindElShortcut($el: JQuery, keyboardShortcut: st } 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(); diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 77fec1366..a8f4f567f 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -297,6 +297,54 @@ 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("
([\\s\\S]+?)?
"), 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("") 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("
", "
\n").replace("
", pre[i].indent + "
")); + } + + return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html; +} + export async function clearBrowserCache() { if (isElectron()) { const win = dynamicRequire("@electron/remote").getCurrentWindow(); @@ -855,6 +903,7 @@ export default { getNoteTypeClass, getMimeTypeClass, isHtmlEmpty, + formatHtml, clearBrowserCache, copySelectionToClipboard, dynamicRequire, diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 2aefbbc01..d3f3f4690 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1243,6 +1243,10 @@ 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; @@ -1772,7 +1776,6 @@ 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; } @@ -2375,4 +2378,13 @@ footer.webview-footer button { 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; } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 4edfd6e38..9b1bafaa8 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -13,6 +13,7 @@ --theme-style: dark; --native-titlebar-background: #00000000; + --window-background-color-bgfx: transparent; /* When background effects enabled */ --main-background-color: #272727; --main-text-color: #ccc; @@ -147,6 +148,7 @@ --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; @@ -155,6 +157,8 @@ --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; diff --git a/apps/client/src/stylesheets/theme-next-light.css b/apps/client/src/stylesheets/theme-next-light.css index 331de6d94..6456f2797 100644 --- a/apps/client/src/stylesheets/theme-next-light.css +++ b/apps/client/src/stylesheets/theme-next-light.css @@ -13,6 +13,7 @@ --theme-style: light; --native-titlebar-background: #ffffff00; + --window-background-color-bgfx: transparent; /* When background effects enabled */ --main-background-color: white; --main-text-color: black; @@ -121,11 +122,11 @@ --left-pane-collapsed-border-color: #0000000d; --left-pane-background-color: #f2f2f2; --left-pane-text-color: #383838; - --left-pane-item-hover-background: #eaeaea; + --left-pane-item-hover-background: rgba(0, 0, 0, 0.032); --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: #d7d7d7; + --left-pane-item-action-button-background: rgba(0, 0, 0, 0.11); --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); @@ -141,6 +142,7 @@ --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; @@ -148,6 +150,8 @@ --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; diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index c709e457a..c4c0b6d4c 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -36,31 +36,23 @@ body.mobile { /* #region Mica */ body.background-effects.platform-win32 { - --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); - } + --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); + --active-tab-background-color: var(--launcher-pane-horiz-background-color); } body.background-effects.platform-win32.layout-vertical { - --left-pane-background-color: transparent; - --left-pane-item-hover-background: rgba(127, 127, 127, 0.05); + --left-pane-background-color: var(--window-background-color-bgfx); --background-material: mica; } body.background-effects.platform-win32, body.background-effects.platform-win32 #root-widget { - background: transparent !important; + background: var(--window-background-color-bgfx) !important; } body.background-effects.platform-win32.layout-horizontal #horizontal-main-container, @@ -89,7 +81,7 @@ body.background-effects.zen #root-widget { * Gutter */ - .gutter { +.gutter { background: var(--gutter-color) !important; transition: background 150ms ease-out; } @@ -1175,6 +1167,11 @@ 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 */ @@ -1183,10 +1180,6 @@ 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; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index d76843a27..638501c45 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -263,6 +263,11 @@ "confirm_delete_all": "Do you want to delete all revisions of this note?", "no_revisions": "No revisions for this note yet...", "restore_button": "Restore", + "diff_on": "Show diff", + "diff_off": "Show content", + "diff_on_hint": "Click to show note source diff", + "diff_off_hint": "Click to show note content", + "diff_not_available": "Diff isn't available.", "confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.", "delete_button": "Delete", "confirm_delete": "Do you want to delete this revision?", @@ -1118,7 +1123,9 @@ "title": "Performance", "enable-motion": "Enable transitions and animations", "enable-shadows": "Enable shadows", - "enable-backdrop-effects": "Enable background effects for menus, popups and panels" + "enable-backdrop-effects": "Enable background effects for menus, popups and panels", + "enable-smooth-scroll": "Enable smooth scrolling", + "app-restart-required": "(a restart of the application is required for the change to take effect)" }, "ai_llm": { "not_started": "Not started", diff --git a/apps/client/src/translations/pl/translation.json b/apps/client/src/translations/pl/translation.json index 980ba7fa7..2d6e92e20 100644 --- a/apps/client/src/translations/pl/translation.json +++ b/apps/client/src/translations/pl/translation.json @@ -30,13 +30,16 @@ "search_note": "Wyszukaj notatkę po nazwie", "link_title_arbitrary": "Tytuł linku można dowolnie zmieniać", "link_title": "Tytuł linku", - "button_add_link": "Dodaj link" + "button_add_link": "Dodaj link", + "help_on_links": "Pomoc dotycząca linków", + "link_title_mirrors": "tytuł linku odzwierciedla tytuł obecnej notatki" }, "branch_prefix": { "save": "Zapisz", "edit_branch_prefix": "Edytuj prefiks gałęzi", "prefix": "Prefiks: ", - "branch_prefix_saved": "Zapisano prefiks gałęzi." + "branch_prefix_saved": "Zapisano prefiks gałęzi.", + "help_on_tree_prefix": "Pomoc dotycząca prefiksu drzewa" }, "bulk_actions": { "labels": "Etykiety", @@ -98,7 +101,8 @@ "prefix_optional": "Prefiks (opcjonalne)", "clone_to_selected_note": "Sklonuj do wybranej notatki", "no_path_to_clone_to": "Brak ścieżki do sklonowania.", - "note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\"" + "note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\"", + "help_on_links": "Pomoc dotycząca linków" }, "help": { "title": "Ściągawka", diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index cc068831a..69dc904b9 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -2013,7 +2013,9 @@ "title": "Setări de performanță", "enable-motion": "Activează tranzițiile și animațiile", "enable-shadows": "Activează umbrirea elementelor", - "enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri" + "enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri", + "enable-smooth-scroll": "Activează derularea lină", + "app-restart-required": "(este necesară repornirea aplicației pentru ca modificarea să aibă efect)" }, "settings": { "related_settings": "Setări similare" diff --git a/apps/client/src/widgets/containers/split_note_container.ts b/apps/client/src/widgets/containers/split_note_container.ts index 12d417973..8298d5989 100644 --- a/apps/client/src/widgets/containers/split_note_container.ts +++ b/apps/client/src/widgets/containers/split_note_container.ts @@ -3,7 +3,7 @@ import appContext, { type CommandData, type CommandListenerData, type EventData, import type BasicWidget from "../basic_widget.js"; import type NoteContext from "../../components/note_context.js"; import Component from "../../components/component.js"; - +import splitService from "../../services/resizer.js"; interface NoteContextEvent { noteContext: NoteContext; } @@ -52,6 +52,10 @@ export default class SplitNoteContainer extends FlexContainer { await widget.handleEvent("setNoteContext", { noteContext }); this.child(widget); + + if (noteContext.mainNtxId && noteContext.ntxId) { + splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]); + } } async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: EventData<"openNewNoteSplit">) { @@ -95,9 +99,9 @@ export default class SplitNoteContainer extends FlexContainer { } } - closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) { + async closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) { if (ntxId) { - appContext.tabManager.removeNoteContext(ntxId); + await appContext.tabManager.removeNoteContext(ntxId); } } @@ -137,6 +141,8 @@ export default class SplitNoteContainer extends FlexContainer { // activate context that now contains the original note await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]); + + splitService.moveNoteSplitResizer(ntxIds[leftIndex]); } activeContextChangedEvent() { @@ -157,6 +163,8 @@ export default class SplitNoteContainer extends FlexContainer { recursiveCleanup(widget); delete this.widgets[ntxId]; } + + splitService.delNoteSplitResizer(ntxIds); } contextsReopenedEvent({ ntxId, afterNtxId }: EventData<"contextsReopened">) { diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx index 0fa4f956e..65c7dfd2c 100644 --- a/apps/client/src/widgets/dialogs/revisions.tsx +++ b/apps/client/src/widgets/dialogs/revisions.tsx @@ -7,6 +7,7 @@ import { t } from "../../services/i18n"; import server from "../../services/server"; import toast from "../../services/toast"; import Button from "../react/Button"; +import FormToggle from "../react/FormToggle"; import Modal from "../react/Modal"; import FormList, { FormListItem } from "../react/FormList"; import utils from "../../services/utils"; @@ -18,12 +19,15 @@ import open from "../../services/open"; import ActionButton from "../react/ActionButton"; import options from "../../services/options"; import { useTriliumEvent } from "../react/hooks"; +import { diffWords } from "diff"; export default function RevisionsDialog() { const [ note, setNote ] = useState(); + const [ noteContent, setNoteContent ] = useState(); const [ revisions, setRevisions ] = useState(); const [ currentRevision, setCurrentRevision ] = useState(); const [ shown, setShown ] = useState(false); + const [ showDiff, setShowDiff ] = useState(false); const [ refreshCounter, setRefreshCounter ] = useState(0); useTriliumEvent("showRevisions", async ({ noteId }) => { @@ -37,8 +41,10 @@ export default function RevisionsDialog() { useEffect(() => { if (note?.noteId) { server.get(`notes/${note.noteId}/revisions`).then(setRevisions); + note.getContent().then(setNoteContent); } else { setRevisions(undefined); + setNoteContent(undefined); } }, [ note?.noteId, refreshCounter ]); @@ -54,22 +60,42 @@ export default function RevisionsDialog() { helpPageId="vZWERwf8U3nx" bodyStyle={{ display: "flex", height: "80vh" }} header={ - (!!revisions?.length &&