Compare commits

...

81 Commits

Author SHA1 Message Date
Ammar Ahmed
5d03d3033f Merge pull request #10281 from streetwriters/mobile-release/3.4.10
mobile: release v3.4.10
2026-08-25 08:48:03 +05:00
Ammar Ahmed
db8f02a585 mobile: release v3.4.10 2026-08-24 13:39:23 +05:00
Ammar Ahmed
0fd305a259 Merge pull request #10220 from streetwriters/mobile-release/3.4.9
mobile: release v3.4.9
2026-08-24 13:26:15 +05:00
Ammar Ahmed
b855266293 Merge pull request #10208 from kashaf-ansari-dev/fix-compress-checkbox-not-toggling
mobile: re-check fullQualityImages availability on every compress tog…
2026-08-24 13:23:48 +05:00
Ammar Ahmed
b4227a8c43 Merge pull request #10222 from kashaf-ansari-dev/fix-9053-restore-editor-scroll-position
Fix 9053 restore editor scroll position
2026-08-24 13:23:02 +05:00
Ammar Ahmed
39887f81a2 Merge pull request #10257 from streetwriters/fix/performance
mobile: fix groupOptions causes unnecessary rerenders on first load
2026-08-24 13:22:15 +05:00
Ammar Ahmed
e47ef061fd Merge pull request #10240 from kashaf-ansari-dev/fix-9014-respect-reduce-animations-setting
mobile: respect device reduce animations system setting for sheets
2026-08-24 13:20:52 +05:00
Ammar Ahmed
360cd5431d Merge pull request #10258 from streetwriters/cleanup/master
global: cleanup
2026-08-19 13:25:40 +05:00
Ammar Ahmed
c3aff365f1 global: cleanup 2026-08-19 13:24:34 +05:00
kashaf-ansari-dev
dd3ced17de mobile: re-check fullQualityImages availability on every compress toggle tap
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-18 18:56:58 +05:00
Abdullah Atta
ee419e514c web: fix Requiring @playwright/test second time when running tests (#10252) 2026-08-18 10:29:06 +05:00
01zulfi
db72716234 Merge pull request #10248 from streetwriters/release/3.4.6
web: bump version to 3.4.6
2026-08-17 17:17:05 +05:00
01zulfi
72351960fa web: fix trailing slash when saving cors proxy url
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-17 14:44:58 +05:00
01zulfi
85458932de web: bump version to 3.4.6
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-17 14:07:02 +05:00
Abdullah Atta
4e1acc292d web: Improve auth ui on web/desktop (#10218)
* web: improve auth ui

* web: fix password show/hide icons

* web: fix auth form width

* web: fix minor ui issues with back button & recovery view
2026-08-17 11:41:18 +05:00
01zulfi
20b4585361 editor: fix multiple link hover popups from opening (#10233)
* also fix the popup not opening when quickly hovering one link after another
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-17 10:11:46 +05:00
01zulfi
dc3f38f67d ui: truncate long menu titles (#10237)
* On web, long tags in menu items in the editor header would take a lot of width. The fix in the ui package will fix it for all usages of long menu items on web

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-17 10:11:17 +05:00
01zulfi
a40e11f2d4 web: preserve port in cors proxy url (#10234)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-17 10:10:54 +05:00
Abdullah Atta
ccc152b561 docs(help): fix links ignoring baseUrl on docs index page (#10243) 2026-08-16 21:46:39 +05:00
kashaf-ansari-dev
279c776e45 mobile: respect device reduce animations system setting for sheets
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-14 17:56:22 +05:00
Abdullah Atta
87aa98b85e global: move everything to notesnook.com/help/* (#10238) 2026-08-14 12:34:07 +05:00
Ammar Ahmed
b7777a2a19 mobile: fix groupOptions causes unnecessary rerenders on first load 2026-08-14 10:55:53 +05:00
Ammar Ahmed
52d6044e73 docs(help): rebuild the help site with VitePress (#10169)
* docs: new help built with vitepress.

* docs: improve docs home ui

Added a Go to Docs button on homepage
Added a searchbar for directly searching what you are looking for in the docs

* docs(help): wire up with build system & setup proper ci

* Clean up new documentation (#10174)

* Docs: Cleanup pass 1

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: cleanup pass 2

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Fix vscode's manglement that I missed.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/plans-and-limits.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/rich-text-editor/outline-lists.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Remove self-hosting guide from sidebar, and comments for reviewer.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>

* docs: some fixes

* ci: do not publish help on push

* docs(help): improve regional pricing & free trials

* docs: improve sidebar

* docs: some more fixes after re-review

* docs: improve search and nav

* docs: a few more fixes

* docs: fix tabs formatting compat with prettier

* docs: fix tabs formatting in various places

* docs: show correct plus button image for mobile

* docs: disable notesnook self hosting docs

* docs: update help docs with fixes

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Co-authored-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-08-14 08:57:51 +05:00
01zulfi
8d56564221 web: fix notes not exporting from archive list multiple selection (#10230)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-14 08:28:42 +05:00
01zulfi
d647eb8187 web: show error when unlock note password is empty (#10231)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-13 08:36:12 +05:00
kashaf-ansari-dev
316951c2bf editor: guard scroll persistence until note session is restored
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-11 09:36:50 +05:00
kashaf-ansari-dev
9ce5b76265 mobile: restore latest tab scroll position
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-10 20:03:22 +05:00
Ammar Ahmed
a82bb4cc32 mobile: release v3.4.9 2026-08-10 14:29:00 +05:00
Ammar Ahmed
b181655edc Merge pull request #10147 from kashaf-ansari-dev/fix/10011-recovery-key-font
mobile: improve recovery key readability
2026-08-10 14:12:46 +05:00
Ammar Ahmed
67ffcb163c Merge pull request #10183 from kashaf-ansari-dev/fix-10165-empty-trash-note
mobile: allow opening empty trashed notes without contentId
2026-08-10 14:12:12 +05:00
Ammar Ahmed
982cea7aa6 Merge pull request #10182 from kashaf-ansari-dev/mobile/improve-skip-wording
mobile: improve skip & go directly to app
2026-08-10 14:11:32 +05:00
Ammar Ahmed
adab66f285 Merge pull request #10198 from kashaf-ansari-dev/fix-rniap-play-billing
mobile: bump Play Billing Library to 8.0.0 via patch-package for reac…
2026-08-10 14:10:44 +05:00
Ammar Ahmed
1740ba9e10 mobile: bump Play Billing Library to 8.0.0 via patch-package for react-native-iap
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-10 14:09:22 +05:00
Ammar Ahmed
9e5ff04692 Merge pull request #10137 from streetwriters/fix/task-list-touch-drag
editor: drag & drop task list items with pointer events
2026-08-10 14:00:26 +05:00
Ammar Ahmed
08bb216334 editor: apply text direction to a whole list, not just the cursor's item 2026-08-09 18:00:53 +05:00
Ammar Ahmed
01da7cbb19 editor: render the drag preview in the editor's own context, 1:1 2026-08-05 19:55:23 +05:00
Ammar Ahmed
e326b912fd editor: RTL nesting, cheaper move and scroller lookup 2026-08-05 19:55:23 +05:00
Abdullah Atta
8bc072a543 editor: remove unnecessary comments
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
2026-08-05 19:55:23 +05:00
Ammar Ahmed
79086108a5 editor: drop placeholder on first item on task list should not hide when moving the dragged item above it or above the task list header. it should safely drop as the first list item.
- Improve drag/drop reliability be increasing the hit slop area for starting the drag.

- Ensure drag survives between rerenders
- Add a solid background to dragged item so it doesn't conflict with items underneath it
2026-08-05 19:55:23 +05:00
Ammar Ahmed
1070831b63 editor: only drop a task item into a task list & fix crash 2026-08-05 19:55:23 +05:00
Ammar Ahmed
efaffc8729 editor: drag task list items with pointer events 2026-08-05 19:55:23 +05:00
Ammar Ahmed
b8a02640d6 Merge pull request #10193 from streetwriters/mobile/release-3.4.8
mobile: release v3.4.8
2026-08-04 19:24:16 +05:00
Ammar Ahmed
31dbbe5e9e mobile: fix rn 82 build on xcode 26.5 2026-08-04 12:11:54 +05:00
kashaf-ansari-dev
696f57423b mobile: improve recovery key readability
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-04 10:57:47 +05:00
Ammar Ahmed
427dcbbb20 mobile: release v3.4.8 2026-08-03 19:30:12 +05:00
Ammar Ahmed
d3d21aa835 Merge pull request #10161 from streetwriters/fix/android-widgets
Fix android widget bugs.
2026-08-03 19:29:33 +05:00
Ammar Ahmed
d60c8be423 Merge pull request #10019 from kashaf-ansari-dev/feat/9694-new-reminder-shortcut
mobile: add app shortcut for creating reminders
2026-08-03 12:28:09 +05:00
kashaf-ansari-dev
4d97f7033f mobile: remove accent color on dialogue text and estrics from the string
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-08-03 10:49:31 +05:00
Ammar Ahmed
516a598b2b Merge pull request #10166 from kashaf-ansari-dev/fix-10135-image-note-save
mobile: fix images sometimes not saving after insert
2026-08-03 09:58:06 +05:00
kashaf-ansari-dev
966d57dfa2 mobile: allow opening empty trashed notes without contentId
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-31 18:04:59 +05:00
kashaf-ansari-dev
7a3111c6b3 mobile: improve skip & go directly to app
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-31 13:07:16 +05:00
Abdullah Atta
f858e3471f desktop: set snap base to core24 (#10180) 2026-07-31 12:41:56 +05:00
kashaf-ansari-dev
57113e0896 mobile: remove AddReminder from stack on navigation away to prevent stale form state
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-31 09:49:36 +05:00
kashaf-ansari-dev
28f2ffdbf7 mobile: remove unused ignoreEdit handling
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:32 +05:00
kashaf-ansari-dev
02c60d14c7 editor: skip ignoreEdit updates before scheduling save
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:00 +05:00
kashaf-ansari-dev
41cdc882c9 editor: fix ignoreEdit flag overwritten during debounced save
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:00 +05:00
Suyadi
a1879dae17 mobile: fix error on android's search query (#10106)
* mobile: pass value for items when searching on main screen to match other screens' pattern
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: guard notesWithHighlighting against an undefined notes selector.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: add regression test for notesWithHighlighting crash.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: revert changes after feedback
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* mobile: ensure type: "note" will make items become mandatory.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: remove tests after feedback
Signed-off-by: Suyadi <afsuyadi@gmail.com>
2026-07-30 23:01:21 +05:00
Ammar Ahmed
4765657423 mobile: fix editor cross contamination (#10157)
1. Use noteId defined in editorMessage when saving content, never use tabId since it can point to a different note since a tab can load a different note while a note save message is coming across the bridge.
2. If saving fails and editor saves save payloads in localStorage, add the noteId to them so we know which note the content belongs to.
3. Pending content saves must keep noteId and edit time so when we save them later, they save into the correct note and if the content is newer, we skip saving.
4. Fix the debounce key so cross contamination can never occur in new notes between two tabs.
2026-07-30 22:55:06 +05:00
Abdullah Atta
f7c755f2d6 editor: sort code block languages by recently used (#10144) 2026-07-30 08:57:38 +05:00
Kashaf Ansari
10a08d5719 mobile: fix add notes to notebook selection behavior (#10104)
* mobile: fix add notes to notebook selection behavior

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: improve move notes keyboard interactions

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix bottom inset when keyboard is open

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:56:18 +05:00
Kashaf Ansari
6e10ce8d8e mobile: add sync status button to side menu (#10112)
* mobile: add sync status button to side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: minor fixes

* mobile: hide sync status during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: skipped sync during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:54:05 +05:00
Abdullah Atta
b5140d9947 Merge pull request #10159 from streetwriters/release/3.4.5
web: bump version to 3.4.5
2026-07-28 21:09:08 +05:00
Abdullah Atta
2288b946ad desktop: launch app with --hidden flag for tests 2026-07-28 19:53:37 +05:00
Abdullah Atta
5fd43ae08e desktop: fix auto start with minimized window on macos 2026-07-28 19:53:37 +05:00
Abdullah Atta
729c410602 global: update intl & lockfiles 2026-07-28 19:53:37 +05:00
Abdullah Atta
bb30336f5c desktop: add tests for #9942 2026-07-28 19:52:48 +05:00
01zulfi
0dc89ba945 web: bump version to 3.4.5
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:52:47 +05:00
Abdullah Atta
bb51244fa3 editor: update prosemirror packages (#10158)
* editor: update prosemirror packages

* editor: fix `__serializeForClipboard` usage
2026-07-28 19:46:51 +05:00
Abdullah Atta
b5ac41c29b web: improve "skip & go directly to app" wording (#10167) 2026-07-28 19:46:23 +05:00
01zulfi
aafb6049b2 core: delete all vaults when user triggers their vault deletion (#9939)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:45:30 +05:00
Abdullah Atta
32070327f9 core: add logs to diagnose why decryption fails after changing password (#10139) 2026-07-27 10:20:25 +05:00
Ammar Ahmed
e4fa0e9f0b mobile: fix ios app does not load if awaited RNBootSplash.hide 2026-07-25 08:54:44 +05:00
kashaf-ansari-dev
97749729ad mobile: improve startup loading shortcuts
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
Ammar Ahmed
ca19fd9230 mobile: fix new note loading from shortcut on cold launch 2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
c435ab46d1 mobile: simplified new editor load logic
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
62ecc5f7fb mobile: resolve app shortcut navigation and add reminder feature gate
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
3e2847f844 mobile: add fluid panels initialRoute to simplified navigation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
c3c9b1fbf4 mobile: fix app shortcut navigation, flash, and back-nav issues
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
a40814a3ab mobile: revise app shortcut handling implementation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
180c202c4e mobile: handle app shortcuts correctly during cold and warm app launches
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
64943647f8 mobile: add app shortcut for creating reminders
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
473 changed files with 17042 additions and 3725 deletions

79
.github/workflows/help.preview.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Notesnook Help PR Preview
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "docs/help/**"
# re-run workflow if workflow file changes
- ".github/workflows/help.preview.yml"
jobs:
build-and-deploy:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=help
- name: Build help
run: npm run build:help
- name: Deploy to Cloudflare
id: deploy
working-directory: ./docs/help
run: |
set -euo pipefail
DEPLOY_OUT=$(npx --yes wrangler versions upload 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
echo "$DEPLOY_OUT"
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
if [ -z "$PREVIEW_URL" ]; then
echo "WARNING: could not parse preview URL from wrangler output"
fi
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ env.preview_url }}
with:
script: |
const marker = '<!-- docs-pages-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Cloudflare Pages Docs Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -2,11 +2,6 @@ name: Publish Notesnook Help
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "docs/help/**"
jobs:
build:
@@ -14,17 +9,21 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: actions-rs/toolchain@v1
uses: actions/checkout@v4
with:
toolchain: stable
# VitePress reads git history to show the last updated date per page.
fetch-depth: 0
- name: Install docgen
run: cargo install --git https://github.com/thecodrr/docgen
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Build site
run: docgen build --release
working-directory: docs/help
- name: Install dependencies
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=help
- name: Build help
run: npm run build:help
- name: Setup environment
run: |
@@ -32,4 +31,4 @@ jobs:
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Publish on Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main
run: npx --yes wrangler deploy

View File

@@ -27,7 +27,9 @@ jobs:
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: npm ci
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Build web
run: npm run build:web

View File

@@ -65,7 +65,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
## Additional Resources
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
- [Migrating & Importing your data from other apps — Importer](https://notesnook.com/help/importing-notes)
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)

View File

@@ -42,6 +42,7 @@ export interface AppContext {
export interface TestOptions {
version: string;
args?: string[];
config?: Record<string, unknown>;
}
@@ -72,7 +73,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
const ctx: AppContext = {
app,
@@ -82,7 +84,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
ctx.app = app;
ctx.userDataDir = userDataDir;
@@ -94,11 +97,12 @@ export async function buildAndLaunchApp(
async function launchApp(
executablePath: string,
userDataDir: string,
version?: string
version?: string,
args: string[] = []
) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
baseURL: "https://app.notesnook.com",
acceptDownloads: true,
env: {
@@ -154,6 +158,7 @@ export async function buildApp(version?: string) {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
CSC_IDENTITY_AUTO_DISCOVERY: "false",
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,

View File

@@ -17,7 +17,65 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "@nn/test";
import { test, expect } from "@nn/test";
import type { ElectronApplication } from "@playwright/test";
async function getMainWindowState(app: ElectronApplication) {
return await app.evaluate((window) => {
const { BrowserWindow } = window;
const mainWindow = BrowserWindow.getAllWindows()[0];
if (!mainWindow) throw new Error("Main window not found");
return {
isMinimized: mainWindow.isMinimized(),
isVisible: mainWindow.isVisible()
};
});
}
test("make sure app loads", async ({ page }) => {
await page.waitForSelector(".ProseMirror");
});
test("hidden launch minimizes when tray is disabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: false
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(true);
});
test("hidden launch does not minimize when close-to-tray is enabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: true
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(false);
});

View File

@@ -203,11 +203,12 @@ module.exports = {
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
snapcraft: {
base: "core24",
core24: {
confinement: "strict",
autoStart: false
}
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.4.4",
"version": "3.4.6",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -92,6 +92,17 @@ async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
// this workaround is necessary because macos doesn't support
// the --hidden flag when launching the app on startup
if (
process.platform === "darwin" &&
app.getLoginItemSettings().wasOpenedAtLogin &&
config.desktopSettings.autoStart &&
config.desktopSettings.startMinimized
) {
cliOptions.hidden = true;
}
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
@@ -141,8 +152,13 @@ async function createWindow() {
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !(config.desktopSettings.minimizeToSystemTray
|| config.desktopSettings.closeToSystemTray))
if (
cliOptions.hidden &&
!(
config.desktopSettings.minimizeToSystemTray ||
config.desktopSettings.closeToSystemTray
)
)
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3113
versionCode 3116
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -13,6 +13,7 @@ buildscript {
androidXCore = "1.7.0"
androidXBrowser = "1.0.0"
ndkVersion = "27.1.12297006"
playBillingSdkVersion = "8.0.0"
}
repositories {
@@ -41,3 +42,12 @@ allprojects {
maven { url 'https://www.jitpack.io' }
}
}
// Pin the ndkVersion for all subprojects
subprojects { subproject ->
["com.android.application", "com.android.library"].each { pluginId ->
subproject.plugins.withId(pluginId) {
subproject.android.ndkVersion = rootProject.ext.ndkVersion
}
}
}

View File

@@ -1,4 +1,3 @@
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { PropsWithChildren, useEffect } from "react";
import React, { PropsWithChildren, useEffect, useState } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -44,26 +44,37 @@ import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
import {
initShortcutListener,
launchNewNoteTab,
registerAppShortcuts
} from "./hooks/use-shortcut-manager";
import Shortcuts from "react-native-actions-shortcuts";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
RNBootSplash.hide({
fade: true
});
Linking.getInitialURL().then((url) => {
useSettingStore.setState({
initialUrl: url
});
});
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
useEffect(() => {
if (introCompleted) {
registerAppShortcuts();
}
}, [introCompleted]);
useEffect(() => {
RNBootSplash.hide({ fade: true });
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
@@ -176,4 +187,41 @@ export const withTheme = (
};
};
export default withTheme(withErrorBoundry(App, "App"));
export const withStartupBoundry = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithStartupBoundary(props: PropsWithChildren) {
const [ready, setReady] = useState(false);
useEffect(() => {
async function init() {
try {
const [url, shortcut] = await Promise.all([
Linking.getInitialURL(),
Shortcuts.getInitialShortcut()
]);
console.log(url, shortcut);
if (shortcut?.type === "notesnook.action.newnote") {
launchNewNoteTab();
}
useSettingStore.setState({
initialUrl: url,
pendingShortcut: shortcut ?? null
});
initShortcutListener();
} finally {
setReady(true);
}
}
init();
}, []);
if (!ready) return null;
return <Element {...props} />;
};
};
export default withStartupBoundry(withTheme(withErrorBoundry(App, "App")));

View File

@@ -24,6 +24,8 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
import { AuthParams } from "../../stores/use-navigation-store";
import { strings } from "@notesnook/intl";
import { presentDialog } from "../dialog/functions";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const route = useRoute();
@@ -56,18 +58,17 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
{!props.welcome ? null : (
<Button
title="Skip"
title={strings.skipAndGoToApp()}
onPress={() => {
hideAuth();
presentDialog({
title: strings.offlineMode(),
paragraph: strings.offlineModeDesc(),
positiveText: strings.understand(),
positivePress: hideAuth as any
});
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}

View File

@@ -36,6 +36,7 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
interface FloatingButtonProps {
onPress: () => void;
@@ -47,6 +48,7 @@ interface FloatingButtonProps {
position?: "left" | "right";
size?: "small" | "large";
style?: ViewStyle;
hideOnKeyboard?: boolean;
}
const FloatingButton = ({
@@ -57,26 +59,28 @@ const FloatingButton = ({
testID,
position = "right",
size = "large",
style
style,
hideOnKeyboard = true
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const route = useRoute();
const insets = useGlobalSafeAreaInsets();
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translate.value
},
{
translateY: translate.value
}
]
};
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: hideOnKeyboard ? translate.value : 0
},
{
translateY: hideOnKeyboard ? translate.value : 0
}
],
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
}));
const animate = useCallback(
(toValue: number) => {
@@ -97,12 +101,22 @@ const FloatingButton = ({
editorState().keyboardState = false;
if (deviceMode !== "mobile") return;
animate(0);
keyboardHeight.value = withTiming(0);
};
const onKeyboardShow = () => {
const onKeyboardShow = (e: any) => {
editorState().keyboardState = true;
if (deviceMode !== "mobile") return;
animate(150);
if (hideOnKeyboard) {
animate(150);
} else {
keyboardHeight.value = withTiming(
e.endCoordinates.height - insets.bottom,
{
duration: 250
}
);
}
};
const sub = [
@@ -110,9 +124,9 @@ const FloatingButton = ({
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
];
return () => {
sub.forEach((sub) => sub?.remove?.());
sub.forEach((sub) => sub.remove());
};
}, [deviceMode, animate]);
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
return deviceMode !== "mobile" && !alwaysVisible ? null : (
<Animated.View
@@ -161,10 +175,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

View File

@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};

View File

@@ -51,9 +51,10 @@ interface TabProps extends ViewProps {
onScroll: (offset: number) => void;
enabled: boolean;
onDrawerStateChange: (state: boolean) => void;
initialPage?: FluidTabPage;
}
type FluidTabPage = "home" | "editor";
export type FluidTabPage = "home" | "editor";
export interface TabsRef {
goToPage: (page: FluidTabPage, animated?: boolean) => void;
@@ -77,15 +78,21 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
onChangeTab,
onScroll,
enabled,
onDrawerStateChange
onDrawerStateChange,
initialPage
}: TabProps,
ref
) {
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const translateX = useSharedValue(widths ? widths.sidebar : 0);
const editorStartPosition = widths.sidebar + widths.list;
const translateX = useSharedValue(
initialPage === "editor" ? editorStartPosition : widths ? widths.sidebar : 0
);
const startX = useSharedValue(0);
const currentTab = useSharedValue(1);
const currentTab = useSharedValue(
initialPage === "editor" && deviceMode !== "tablet" ? 2 : 1
);
const previousTab = useSharedValue(1);
const isDrawerOpen = useSharedValue(false);
const gestureStartValue = useSharedValue({

View File

@@ -58,9 +58,9 @@ export const openNote = async (
}
if (isTrash) {
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
presentSheet({
component: <NotePreview note={item} content={content} />
});

View File

@@ -249,7 +249,7 @@ export default function NoteHistory({
<Text
onPress={() => {
openLinkInBrowser(
"https://help.notesnook.com/note-version-history"
"https://notesnook.com/help/note-version-history"
);
}}
style={{

View File

@@ -198,7 +198,7 @@ export default function NotePreview({
<View
style={{
width: "100%",
height: 100,
flex: 1,
justifyContent: "center",
alignItems: "center"
}}

View File

@@ -50,7 +50,7 @@ export const Synced = ({ item, close }) => {
close();
await sleep(300);
await openLinkInBrowser(
"https://help.notesnook.com/how-is-my-data-encrypted",
"https://notesnook.com/help/how-is-my-data-encrypted",
colors
);
} catch (e) {

View File

@@ -509,7 +509,7 @@ const PublishNoteSheet = ({
onPress={async () => {
try {
await openLinkInBrowser(
"https://help.notesnook.com/publish-notes-with-monographs"
"https://notesnook.com/help/publish-notes-with-monographs"
);
} catch (e) {
console.error(e);

View File

@@ -230,7 +230,7 @@ class RecoveryKeySheet extends React.Component {
>
<Paragraph
color={colors.primary.paragraph}
size={AppFontSize.sm}
size={AppFontSize.md}
numberOfLines={2}
selectable
style={{
@@ -238,7 +238,9 @@ class RecoveryKeySheet extends React.Component {
maxWidth: "100%",
paddingRight: 10,
textAlign: "center",
textDecorationLine: "underline"
textDecorationLine: "underline",
letterSpacing: 0.5,
fontFamily: "monospace"
}}
>
{this.state.key}

View File

@@ -33,7 +33,6 @@ import {
setGroupOptionsById
} from "../../../hooks/use-group-options";
import { eSendEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { RouteName } from "../../../stores/use-navigation-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useTagStore } from "../../../stores/use-tag-store";
@@ -46,6 +45,7 @@ import { Button } from "../../ui/button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import Navigation from "../../../services/navigation";
const Sort = ({
dataType,
screen,
@@ -87,8 +87,7 @@ const Sort = ({
};
const updateGroupOptions = async (_groupOptions: GroupOptions) => {
console.log(groupId, type);
setGroupOptionsById(groupType, _groupOptions, groupId, type);
await setGroupOptionsById(groupType, _groupOptions, groupId, type);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen) Navigation.queueRoutesForUpdate(screen);

View File

@@ -30,6 +30,7 @@ import { Pressable } from "../ui/pressable";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import { useSideBarDraggingStore } from "./dragging-store";
import SyncStatusButton from "./sync-status-button";
const SettingsIcon = () => {
const { colors } = useThemeColors();
@@ -123,7 +124,7 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => {
size={AppFontSize.lg}
/>
))}
<SyncStatusButton />
<SettingsIcon />
</View>
</View>

View File

@@ -0,0 +1,161 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useTimeAgo } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import { View } from "react-native";
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming
} from "react-native-reanimated";
import Sync from "../../services/sync";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { AppFontSize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
const SyncStatusButton = () => {
const { colors } = useThemeColors();
const [user, syncing, lastSyncStatus, lastSynced, isLoggingOut] =
useUserStore((state) => [
state.user,
state.syncing,
state.lastSyncStatus,
state.lastSynced,
state.isLoggingOut
]);
const { isInternetReachable } = useNetInfo();
const isOffline = !isInternetReachable;
const hasSyncedBefore = lastSynced && lastSynced !== "Never";
const isFailed = lastSyncStatus === SyncStatus.Failed;
const isSynced = lastSyncStatus === SyncStatus.Passed;
const lastSyncedTimeAgo = useTimeAgo(lastSynced, {
interval: 5000,
live: true
});
const getIconColor = (): string => {
if (syncing) return colors.primary.accent;
if (isSynced) return colors.primary.accent;
return colors.secondary.icon;
};
const rotation = useSharedValue(0);
React.useEffect(() => {
if (syncing && !isOffline) {
rotation.value = 0;
rotation.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1,
false
);
} else {
cancelAnimation(rotation);
rotation.value = withTiming(360, {
duration: 1000 * (1 - (rotation.value % 360) / 360),
easing: Easing.linear
});
}
}, [syncing, rotation, isOffline]);
const rotationStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }]
}));
const tooltipText = React.useMemo(() => {
const offlineSuffix = isOffline ? ` (${strings.offline()})` : "";
if (syncing) return strings.syncing();
if (!hasSyncedBefore) return `${strings.never()}${offlineSuffix}`;
if (isFailed) return `${strings.syncFailed()}${offlineSuffix}`;
return `${strings.synced()}${lastSyncedTimeAgo}${offlineSuffix ? `${offlineSuffix}` : ""}`;
}, [isOffline, syncing, hasSyncedBefore, isFailed, lastSyncedTimeAgo]);
const onPress = () => {
if (syncing) return;
Sync.run();
};
if (!user || isLoggingOut) return null;
return (
<View
style={{
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
{syncing ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "center",
alignItems: "center"
}}
>
<Animated.View style={rotationStyle}>
<AppIcon name="sync" size={AppFontSize.lg} color={getIconColor()} />
</Animated.View>
</View>
) : (
<IconButton
name="sync"
onPress={onPress}
tooltipText={tooltipText}
tooltipPosition={NativeTooltip.POSITIONS.BOTTOM}
size={AppFontSize.lg}
color={getIconColor()}
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border
}}
/>
)}
{!syncing && (isFailed || isOffline) ? (
<AppIcon
name="information"
color={isOffline ? colors.static.yellow : colors.error.icon}
size={AppFontSize.xxs}
style={{ position: "absolute", bottom: -3, right: -3 }}
/>
) : null}
</View>
);
};
export default SyncStatusButton;

View File

@@ -27,7 +27,7 @@ import { useUserStore } from "../../../stores/use-user-store";
import { getContainerBorder } from "../../../utils/colors";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { Toast } from "../../toast";
import { useReduceMotion } from "../../../hooks/use-reduce-motion";
/**
*
* @param {any} param0
@@ -53,6 +53,9 @@ const SheetWrapper = ({
const sheetKeyboardHandler = useSettingStore(
(state) => state.sheetKeyboardHandler
);
const isReduceMotionEnabled = useReduceMotion();
const isAnimated = !isReduceMotionEnabled;
const largeTablet = deviceMode === "tablet";
const smallTablet = deviceMode === "smallTablet";
const dimensions = useSettingStore((state) => state.dimensions);
@@ -125,6 +128,7 @@ const SheetWrapper = ({
<ScopedThemeProvider value="sheet">
<ActionSheet
ref={fwdRef || localRef}
animated={isAnimated}
testIDs={{
backdrop: "sheet-backdrop"
}}

View File

@@ -346,6 +346,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
useUserStore.getState().setIsLoggingOut(false);
eSendEvent(eAfterSync);
};

View File

@@ -54,15 +54,13 @@ export function useGroupOptions(
const [groupOptions, setGroupOptions] = useState(
getGroupOptions(groupingKey, id, type)
);
console.log(groupingKey, id, type, groupOptions, "options");
const groupOptionsRef = useRef(groupOptions);
groupOptionsRef.current = groupOptions;
useEffect(() => {
const onUpdate = (_groupingKey: string, _id?: string, _type?: string) => {
if (_groupingKey !== groupingKey) return;
if (_id && _type && _id !== id && _type !== type) return;
if (_groupingKey !== groupingKey || _type !== type) return;
if (_id && _type && _id !== id) return;
const options = getGroupOptions(groupingKey, id, type);
if (!options) return;
if (
@@ -70,9 +68,7 @@ export function useGroupOptions(
groupOptionsRef.current?.sortBy !== options.sortBy ||
groupOptionsRef.current?.sortDirection !== options?.sortDirection
) {
console.log("onUpdate", _id, _type);
setGroupOptions({ ...options });
Navigation.queueRoutesForUpdate();
}
};

View File

@@ -0,0 +1,50 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { AccessibilityInfo } from "react-native";
export function useReduceMotion(): boolean {
const [isReduceMotionEnabled, setIsReduceMotionEnabled] =
useState<boolean>(false);
useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled()
.then(setIsReduceMotionEnabled)
.catch(() => {});
const subscription = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
setIsReduceMotionEnabled
);
return () => {
if (subscription?.remove) {
subscription.remove();
} else if ((AccessibilityInfo as any).removeEventListener) {
(AccessibilityInfo as any).removeEventListener(
"reduceMotionChanged",
setIsReduceMotionEnabled
);
}
};
}, []);
return isReduceMotionEnabled;
}

View File

@@ -17,54 +17,67 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts";
import { useEffect } from "react";
import { NativeEventEmitter, NativeModule } from "react-native";
import { useRef } from "react";
import { Platform } from "react-native";
import { NativeEventEmitter, NativeModule, Platform } from "react-native";
import deviceInfoModule from "react-native-device-info";
import { strings } from "@notesnook/intl";
import { useSettingStore } from "../stores/use-setting-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
const ShortcutsEmitter = new NativeEventEmitter(
Shortcuts as unknown as NativeModule
);
function isSupported() {
export function isShortcutsSupported() {
return Platform.OS !== "android" || deviceInfoModule.getApiLevelSync() > 25;
}
const defaultShortcuts: ShortcutItem[] = [
{
type: "notesnook.action.newnote",
title: strings.createNewNote(),
shortTitle: strings.newNote(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
},
{
type: "notesnook.action.newreminder",
title: strings.setReminder(),
shortTitle: strings.newReminder(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
}
];
export const useShortcutManager = ({
onShortcutPressed,
shortcuts = defaultShortcuts
}: {
onShortcutPressed: (shortcut: ShortcutItem | null) => void;
shortcuts?: ShortcutItem[];
}) => {
const initialShortcutRecieved = useRef(false);
useEffect(() => {
if (!isSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}, [shortcuts]);
export function registerAppShortcuts(
shortcuts: ShortcutItem[] = defaultShortcuts
) {
if (!isShortcutsSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}
useEffect(() => {
if (!isSupported()) return;
Shortcuts.getInitialShortcut().then((shortcut) => {
if (initialShortcutRecieved.current || !shortcut) return;
onShortcutPressed(shortcut);
initialShortcutRecieved.current = true;
});
const subscription = ShortcutsEmitter.addListener(
"onShortcutItemPressed",
onShortcutPressed
);
return () => {
subscription?.remove();
};
}, [onShortcutPressed]);
};
let listenerInitialized = false;
export function initShortcutListener() {
if (!isShortcutsSupported() || listenerInitialized) return;
listenerInitialized = true;
ShortcutsEmitter.addListener(
"onShortcutItemPressed",
(shortcut: ShortcutItem) => {
console.time("shortcut");
useSettingStore.setState({ pendingShortcut: shortcut });
}
);
}
export function launchNewNoteTab() {
let tabId;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
if (useTabStore.getState().getTab(tabId)?.session?.noteId) {
useTabStore.getState().newTabSession(tabId, {});
}
}
}

View File

@@ -41,10 +41,9 @@ import Animated, {
} from "react-native-reanimated";
import { notesnook } from "../../e2e/test.ids";
import { db } from "../common/database";
import { FluidPanels } from "../components/fluid-panels";
import { FluidPanels, FluidTabPage } from "../components/fluid-panels";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
@@ -59,7 +58,6 @@ import {
eCloseFullscreenEditor,
eOnEnterEditor,
eOnExitEditor,
eOnLoadNote,
eOpenFullscreenEditor,
eUnlockNote
} from "../utils/events";
@@ -67,6 +65,7 @@ import { valueLimiter } from "../utils/functions";
import { fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import type { PaneWidths } from "../screens/editor/wrapper";
import { NavigationProps } from "../services/navigation";
const MOBILE_SIDEBAR_SIZE = 0.85;
@@ -74,7 +73,7 @@ let SideMenu: any = null;
let EditorWrapper: any = null;
export const FluidPanelsView = React.memo(
() => {
({ route }: NavigationProps<"FluidPanelsView">) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const setFullscreen = useSettingStore((state) => state.setFullscreen);
@@ -102,6 +101,15 @@ export const FluidPanelsView = React.memo(
setOrientation(o);
}
});
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newnote") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (!appLoading) {
@@ -111,29 +119,6 @@ export const FluidPanelsView = React.memo(
}
}, [appLoading]);
useShortcutManager({
onShortcutPressed: async (item) => {
if (!item) return;
if (item?.type === "notesnook.action.newnote") {
if (!fluidTabsRef.current) {
setTimeout(() => {
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current?.goToPage("editor", false);
}, 1000);
return;
}
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
setTimeout(
() => fluidTabsRef.current?.goToPage("editor", false),
300
);
}
}
});
const showFullScreenEditor = useCallback(() => {
setFullscreen(true);
if (deviceMode === "smallTablet") {
@@ -356,6 +341,7 @@ export const FluidPanelsView = React.memo(
dimensions={dimensions}
widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]}
enabled={deviceMode !== "tablet" && !fullscreen}
initialPage={route.params?.initialPage}
onScroll={onScroll}
onChangeTab={onChangeTab}
onDrawerStateChange={(state) => {

View File

@@ -27,10 +27,17 @@ import useNavigationStore, {
} from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { fluidTabsRef, rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { isFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { isInternalLink, parseInternalLink } from "@notesnook/core";
import { eSendEvent } from "../services/event-manager";
import { editorState } from "../screens/editor/tiptap/utils";
import { eOnLoadNote } from "../utils/events";
import { strings } from "@notesnook/intl";
import PaywallSheet from "../components/sheets/paywall";
import { presentDialog } from "../components/dialog/functions";
import { launchNewNoteTab } from "../hooks/use-shortcut-manager";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -300,8 +307,15 @@ export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const initialShortcut = React.useRef(
useSettingStore.getState().pendingShortcut
).current;
const reminderFeature = useIsFeatureAvailable("activeReminders");
const clearSelection = useSelectionStore((state) => state.clearSelection);
const resetTimer = React.useRef<NodeJS.Timeout>(undefined);
const onStateChange = React.useCallback(
(state: any) => {
if (useSelectionStore.getState().selectionMode) {
@@ -316,13 +330,66 @@ export const RootNavigation = () => {
[clearSelection]
);
React.useEffect(() => {
const unsubscribe = useSettingStore.subscribe((state, prevState) => {
const pendingShortcut = state.pendingShortcut;
if (pendingShortcut === prevState.pendingShortcut || !pendingShortcut) {
return;
}
if (pendingShortcut.type === "notesnook.action.newreminder") {
if (reminderFeature === undefined) return;
if (!reminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: reminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(reminderFeature);
}
});
useSettingStore.setState({
pendingShortcut: null
});
return;
}
rootNavigatorRef.current?.navigate("AddReminder" as any);
} else if (pendingShortcut.type === "notesnook.action.newnote") {
if (fluidTabsRef.current) {
rootNavigatorRef.current?.navigate("FluidPanelsView" as any);
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current.goToPage("editor", true);
} else {
launchNewNoteTab();
rootNavigatorRef.current?.navigate("FluidPanelsView" as any, {
initialPage: "editor"
});
}
}
});
return unsubscribe;
}, [reminderFeature]);
const initialRouteName = !introCompleted
? "Welcome"
: initialShortcut?.type === "notesnook.action.newreminder"
? "AddReminder"
: "FluidPanelsView";
return (
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
<RootStack.Navigator
screenOptions={{
headerShown: false
}}
initialRouteName={introCompleted ? "FluidPanelsView" : "Welcome"}
initialRouteName={initialRouteName}
>
<RootStack.Screen
name="Welcome"
@@ -347,6 +414,12 @@ export const RootNavigation = () => {
require("../navigation/fluid-panels-view").default;
return FluidPanelsView;
}}
initialParams={{
initialPage:
initialShortcut?.type === "notesnook.action.newnote"
? "editor"
: undefined
}}
/>
<RootStack.Screen

View File

@@ -20,8 +20,9 @@ import { Note, Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
BackHandler,
KeyboardAvoidingView,
Platform,
ScrollView,
@@ -63,6 +64,7 @@ import FormInput, {
validators
} from "../../components/ui/input/form-input";
import AppIcon from "../../components/ui/AppIcon";
import { presentDialog } from "../../components/dialog/functions";
const ReminderModes =
Platform.OS === "ios"
@@ -94,7 +96,7 @@ const ReminderNotificationModes = {
};
export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
const { reminder, reference } = props.route.params ?? {};
useNavigationFocus(props.navigation, {
focusOnInit: true,
onFocus: () => {
@@ -106,6 +108,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
return false;
}
});
const handleBackNavigation = useCallback(() => {
const routes = props.navigation.getState()?.routes;
if (routes && routes.length <= 1) {
props.navigation.navigate("FluidPanelsView" as any);
return true;
}
Navigation.goBack();
return true;
}, [props.navigation]);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
return handleBackNavigation();
});
return () => sub.remove();
}, [handleBackNavigation]);
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
@@ -127,6 +146,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? (reference as Note) : null;
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
const activeReminderFeature = useIsFeatureAvailable("activeReminders");
const formRef = useRef(
createFormRef({
title: reminder?.title || referencedItem?.title || "",
@@ -153,6 +173,31 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
);
const [dateError, setDateError] = useState<string>();
const [selectDayError, setSelectDayError] = useState<string>();
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newreminder") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (activeReminderFeature === undefined) return;
if (!activeReminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: activeReminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(activeReminderFeature);
},
onClose: () => {
props.navigation.navigate("FluidPanelsView" as any);
}
});
}
}, [activeReminderFeature]);
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -243,7 +288,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
Notifications.scheduleNotification(_reminder as Reminder);
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
Navigation.goBack();
handleBackNavigation();
} catch (e) {
ToastManager.error(e as Error, undefined);
}
@@ -267,12 +312,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Header
title={reminder ? strings.editReminder() : strings.newReminder()}
canGoBack
onLeftMenuButtonPress={handleBackNavigation}
rightButton={{
name: "check",
onPress: saveReminder
}}
/>
<Dialog context="local" />
<ScrollView
style={{
marginBottom: DDS.isTab ? 25 : undefined,

View File

@@ -90,9 +90,10 @@ export type SavePayload = {
data?: string;
type?: "tiptap";
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
};
export type AppState = {

View File

@@ -149,7 +149,7 @@ const showActionsheet = async () => {
}
};
type ContentMessage = { html: string; ignoreEdit: boolean };
type ContentMessage = { html: string };
export const useEditorEvents = (
editor: useEditorType,
@@ -411,16 +411,19 @@ export const useEditorEvents = (
.getState()
.getNoteIdForTab(editorMessage.tabId);
const saveNoteId = editorMessage.noteId || noteId;
switch (editorMessage.type) {
case EditorEvents.content:
DatabaseLogger.log("EditorEvents.content");
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value.html as string,
noteId: noteId,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
});
break;
case EditorEvents.title:
@@ -428,10 +431,11 @@ export const useEditorEvents = (
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value?.title as string,
noteId: noteId,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
tabId: editorMessage.tabId,
ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
});
break;
case EditorEvents.logger:

View File

@@ -260,29 +260,65 @@ export const useEditor = (
id,
data,
type,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
}: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return;
if (sourceNoteId && id && sourceNoteId !== id) {
DatabaseLogger.error(
new Error(
`Refused to save content of note ${sourceNoteId} into note ${id}`
)
);
return;
}
try {
if (id && !(await db.notes?.note(id))) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
if (useTabStore.getState().getNoteIdForTab(tabId) === id) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
}
return;
}
let note = id ? await db.notes?.note(id) : undefined;
// A restored pending change can be older than what is already in the
// db (it was saved on another device, or the save actually went
// through and only the acknowledgement was lost). Applying it would
// roll the note back, so verify it is still the newest edit. Content
// and title are compared separately so that a newer title doesn't
// discard pending content, and vice versa.
if (pendingChanges && pendingChangesAt && note) {
const dateEdited = data
? note.contentId
? (await db.content?.get(note.contentId))?.dateEdited
: undefined
: note.dateEdited;
if (dateEdited && dateEdited > pendingChangesAt) {
DatabaseLogger.log(
`Discarding stale pending ${
data ? "content" : "title"
} for note ${id}: edited at ${dateEdited}, change captured at ${pendingChangesAt}`
);
return id;
}
}
const locked = note && (await db.vaults.itemExists(note));
if (note?.conflicted) {
@@ -306,11 +342,6 @@ export const useEditor = (
noteData.title = title;
if (ignoreEdit) {
DatabaseLogger.log("Ignoring edits...");
noteData.dateEdited = note?.dateEdited;
}
if (data) {
noteData.content = {
data: data,
@@ -321,6 +352,9 @@ export const useEditor = (
let saved = false;
setTimeout(() => {
if (saved) return;
// Don't report progress on a tab that has moved on to another note.
if (id && useTabStore.getState().getNoteIdForTab(tabId) !== id)
return;
commands.setStatus(
getFormattedDate(note ? note.dateEdited : Date.now(), "date-time"),
strings.saving(),
@@ -436,14 +470,26 @@ export const useEditor = (
}
}
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
currentNotes.current[id] = note;
if (id && pendingChanges) {
if (data) {
currentContents.current[id] = {
data: data,
type: "tiptap",
noteId: id
};
}
lastContentChangeTime.current[id] = Date.now();
// Push the restored change into the editor only if the note is
// actually open in a tab, and only into that tab.
const noteTabId = useTabStore.getState().getTabForNote(id);
if (noteTabId !== undefined) {
postMessage(NativeEvents.title, title || note?.title, noteTabId);
if (data) {
postMessage(NativeEvents.html, { data: data }, noteTabId);
}
currentNotes.current[id] = note;
}
}
if (!saveCount.current[tabId]) {
@@ -703,12 +749,13 @@ export const useEditor = (
await postMessage(NativeEvents.title, item.title, tabId);
overlay(false);
const updatedTab = useTabStore.getState().getTab(tabId!);
await postMessage(
NativeEvents.html,
{
data: currentContents.current[item.id]?.data || "",
scrollTop: tab?.session?.scrollTop,
selection: tab?.session?.selection,
scrollTop: updatedTab?.session?.scrollTop,
selection: updatedTab?.session?.selection,
searchResultIndex: event.searchResultIndex
},
tabId,
@@ -932,24 +979,25 @@ export const useEditor = (
title,
content,
type,
ignoreEdit,
noteId,
tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
}: {
noteId?: string;
title?: string;
content?: string;
type: string;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
}) => {
DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if (
ignoreEdit ||
lock.current ||
(currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId)
@@ -958,7 +1006,6 @@ export const useEditor = (
lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
ignoreEdit: ${ignoreEdit}
`);
if (lock.current) {
setTimeout(() => {
@@ -971,7 +1018,10 @@ export const useEditor = (
return;
}
if (noteId) {
// A restored pending change is not a live edit: it may still be
// discarded as stale by saveNote, so it must not claim to be the newest
// content until it is actually written.
if (noteId && !pendingChanges) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
@@ -979,7 +1029,7 @@ export const useEditor = (
});
}
if (type === EditorEvents.content && noteId) {
if (type === EditorEvents.content && noteId && !pendingChanges) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -992,15 +1042,17 @@ export const useEditor = (
data: content,
type: "tiptap",
id: noteId,
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
};
withTimer(
noteId || "newnote",
`${noteId || tabId}:${type}`,
() => {
if (!params.id) {
if (!params.id && !params.sourceNoteId) {
params.id = useTabStore.getState().getNoteIdForTab(tabId);
}
if (onChange && params.data) {
@@ -1018,7 +1070,7 @@ export const useEditor = (
saveNote(params);
}
},
ignoreEdit ? 0 : 150
150
);
},
[editorSessionHistory, withTimer, onChange, saveNote]

View File

@@ -30,6 +30,7 @@ import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNotes } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common";
import { db } from "../../common/database";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
@@ -59,7 +60,8 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
placeholder: strings.searchInRoute(route.name),
type: "note",
title: route.name,
route: route.name
route: route.name,
items: db.notes.all
});
}}
id={route.name}

View File

@@ -35,7 +35,10 @@ import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import { createItemSelectionStore } from "../../stores/item-selection-store";
import {
createItemSelectionStore,
SelectionState
} from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
@@ -47,14 +50,12 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
const currentNotebook = props.route.params.notebook;
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const selectionCount = useItemSelectionStore(
(state) =>
Object.keys(state.selection).filter(
(k) =>
state.selection?.[k] === "selected" ||
state.selection[k] === "deselected"
)?.length > 0
);
const hasPendingChanges = useItemSelectionStore((state) => {
return Object.keys(state.selection).some((id) => {
return state.selection[id] !== state.initialState[id];
});
});
useNavigationFocus(props.navigation, { focusOnInit: true });
@@ -76,20 +77,6 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
.catch(() => {
setLoading(false);
});
db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
const selection: { [name: string]: any } = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection: selection,
initialState: selection
});
});
},
[currentNotebook]
);
@@ -97,6 +84,25 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
useEffect(() => {
loadNotes();
const loadSelection = async () => {
const existingNotes = await db.relations
.from(currentNotebook, "note")
.get();
const selection: Record<string, SelectionState> = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection,
initialState: selection
});
};
loadSelection();
return () => {
useItemSelectionStore.getState().reset();
};
@@ -159,6 +165,8 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
/>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View
style={{
@@ -176,17 +184,19 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
)}
</View>
}
style={{
flexGrow: 1
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: hasPendingChanges ? 80 : 0
}}
data={loading ? [] : notes?.placeholders}
renderItem={renderItem}
/>
{selectionCount ? (
{hasPendingChanges ? (
<FloatingButton
icon="check"
alwaysVisible
hideOnKeyboard={false}
onPress={async () => {
await db.notes?.addToNotebook(
currentNotebook.id,

View File

@@ -46,7 +46,7 @@ export function toCamelCase(title: string) {
export function openMonographsWebpage() {
try {
openLinkInBrowser(
"https://help.notesnook.com/publish-notes-with-monographs"
"https://notesnook.com/help/publish-notes-with-monographs"
);
} catch (e) {
console.error(e);

View File

@@ -28,6 +28,7 @@ import {
} from "../../components/dialogs/progress";
import Navigation from "../../services/navigation";
import BackupService from "../../services/backup";
import { useUserStore } from "../../stores/use-user-store";
export async function logoutUser() {
const hasUnsyncedChanges = await db.hasUnsyncedChanges();
@@ -47,6 +48,7 @@ export async function logoutUser() {
: undefined,
positivePress: async (_, takeBackup) => {
eSendEvent(eCloseSimpleDialog);
useUserStore.getState().setIsLoggingOut(true);
setTimeout(async () => {
try {
startProgress({
@@ -104,6 +106,7 @@ export async function logoutUser() {
DatabaseLogger.error(e);
ToastManager.error(e as Error, strings.logoutError());
endProgress();
useUserStore.getState().setIsLoggingOut(false);
}
}, 300);
}

View File

@@ -208,15 +208,15 @@ export const settingsGroups: SettingSection[] = [
return status === SubscriptionStatus.TRIAL
? strings.trialOnGoing(trialEndDate)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
}
return strings.neverHesitate();
@@ -971,7 +971,7 @@ export const settingsGroups: SettingSection[] = [
name: strings.keepScreenOn(),
description: strings.keepScreenOnDesc(),
property: "keepScreenOn",
icon: "cellphone-screenshot",
icon: "cellphone-screenshot"
},
{
id: "image-compression",
@@ -1679,7 +1679,7 @@ export const settingsGroups: SettingSection[] = [
id: "docs-link",
name: strings.documentation(),
modifer: async () => {
Linking.openURL("https://help.notesnook.com/");
Linking.openURL("https://notesnook.com/help/");
},
description: strings.documentationDesc(),
icon: "file-document"

View File

@@ -472,7 +472,7 @@ function ThemeSelector() {
actionText: strings.learnMore(),
func: () => {
openLinkInBrowser(
"https://help.notesnook.com/custom-themes/introduction"
"https://notesnook.com/help/custom-themes/introduction"
);
}
});

View File

@@ -177,9 +177,11 @@ function resetRootState(
if (state.routes.length < 2) return;
const routes = state.routes.filter(
let routes = state.routes.filter(
(route) =>
(route.name !== "Auth" && route.name !== "Welcome") ||
(route.name !== "Auth" &&
route.name !== "Welcome" &&
route.name !== "AddReminder") ||
route.key === focusedRoute.key
);

View File

@@ -63,6 +63,11 @@ const run = async (
clearTimeout(syncTimer);
syncTimer = setTimeout(async () => {
if (useUserStore.getState().isLoggingOut) {
DatabaseLogger.info("Sync skipped — user is logging out");
return;
}
const userstore = useUserStore.getState();
userstore.setSyncing(true);
const user = await db.user.getUser();

View File

@@ -711,9 +711,10 @@ const ShareView = () => {
width: "100%",
marginTop: 6
}}
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
onPress={async () => {
const feature = await isFeatureAvailable("fullQualityImages");
if (feature?.isAllowed) {
setCompress((prev) => !prev);
}
}}
>

View File

@@ -56,17 +56,14 @@ export function createItemSelectionStore(
canEnableMultiSelectMode: multiSelectMode,
initialState: {},
markAs: (item, state) => {
set({
selection: {
...get().selection,
[item.id]:
state === "deselected"
? get().initialState === undefined
? undefined
: "deselected"
: state
}
});
const selection = { ...get().selection };
const initial = get().initialState[item.id];
if (state === "deselected" && initial === undefined) {
delete selection[item.id];
} else {
selection[item.id] = state;
}
set({ selection });
},
multiSelect: false,
toggleMultiSelect: () => {

View File

@@ -74,13 +74,21 @@ export interface RouteParams extends ParamListBase {
Tags: GenericRouteParam;
Favorites: GenericRouteParam;
Trash: GenericRouteParam;
Search: {
placeholder: string;
type: ItemType;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
Search:
| {
placeholder: string;
type: "note";
title: string;
route: RouteName;
items: FilteredSelector<Note>;
}
| {
placeholder: string;
type: Exclude<ItemType, "note">;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
TaggedNotes: NotesScreenParams;
ColoredNotes: NotesScreenParams;
TopicNotes: NotesScreenParams;
@@ -88,7 +96,7 @@ export interface RouteParams extends ParamListBase {
Monographs: NotesScreenParams;
Reminders: GenericRouteParam;
SettingsGroup: GenericRouteParam;
FluidPanelsView: GenericRouteParam;
FluidPanelsView: { initialPage?: "editor" | "home" };
AppLock: GenericRouteParam;
Settings: GenericRouteParam;
Auth: AuthParams;

View File

@@ -27,6 +27,7 @@ import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
import { db } from "../common/database";
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
import { ShortcutItem } from "react-native-actions-shortcuts";
export const HostIds = [
"API_HOST",
"AUTH_HOST",
@@ -149,6 +150,7 @@ export interface SettingStore {
refresh: () => void;
inboxEnabled: boolean;
setInboxEnabled: (inboxEnabled: boolean) => void;
pendingShortcut: ShortcutItem | null;
}
const { width, height } = Dimensions.get("window");
@@ -269,5 +271,6 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
});
},
inboxEnabled: false,
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled })
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }),
pendingShortcut: null
}));

View File

@@ -48,6 +48,8 @@ export interface UserStore {
disableAppLockRequests: boolean;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
isLoggingOut: boolean;
setIsLoggingOut: (value: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -95,5 +97,7 @@ export const useUserStore = create<UserStore>((set) => ({
set({ disableAppLockRequests: false });
}, 1000);
},
profile: undefined
profile: undefined,
isLoggingOut: false,
setIsLoggingOut: (value) => set({ isLoggingOut: value })
}));

View File

@@ -5,6 +5,8 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip
require_relative 'scripts/patch_fmt_consteval'
platform :ios, min_ios_version_supported
prepare_react_native_project!
@@ -66,6 +68,10 @@ post_install do |installer|
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
# Keep fmt buildable on Xcode >= 26.2. See ios/scripts/patch_fmt_consteval.rb.
PatchFmtConsteval.apply!(installer.sandbox.root)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'

View File

@@ -4092,6 +4092,6 @@ SPEC CHECKSUMS:
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
PODFILE CHECKSUM: 3fe13efa8356dcc061862bfa9f453dcd12ede70a
PODFILE CHECKSUM: 30b2045c0f4fc91402a43a9e2a872af803f2d6c3
COCOAPODS: 1.16.2

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -0,0 +1,77 @@
# Xcode >= 26.2 rejects fmt's compile-time format-string check with
#
# call to consteval function 'fmt::fstring<...>::fstring<char[N]>' is not a
# constant expression
#
# React Native 0.81/0.82 vendor fmt 11.0.2, which hits this. It is an upstream
# incompatibility (reproducible in a stock RN app), but left alone it makes those
# RN versions permanently un-buildable on a modern Xcode.
#
# fmt's own escape hatch is FMT_USE_CONSTEVAL: 0 downgrades the format-string
# check from compile-time to run-time. fmt 11.0.2 does not guard its detection
# block with #ifndef, so we cannot simply predefine the macro -- and doing it
# through the build settings is worse anyway:
#
# * a command-line GCC_PREPROCESSOR_DEFINITIONS outranks every per-target
# value, silently dropping COCOAPODS=1, RCT_METRO_PORT, ...
# * a second `post_install` block in the Podfile REPLACES React Native's own.
#
# So we patch the header itself, right after fmt has made up its mind and before
# the first use of the macro. Idempotent, and safe to run on every pod install.
module PatchFmtConsteval
MARKER = 'NOTESNOOK_FMT_CONSTEVAL_PATCH'.freeze
# The line that first consumes the macro; our override goes immediately above
# it, i.e. after the whole detection cascade.
ANCHOR = "#if FMT_USE_CONSTEVAL\n".freeze
OVERRIDE = <<~PATCH.freeze
// #{MARKER}: Xcode >= 26.2 rejects fmt's consteval format-string check
// (see ios/scripts/patch_fmt_consteval.rb). Applied automatically by
// `pod install`; downgrades the check to run-time.
#undef FMT_USE_CONSTEVAL
#define FMT_USE_CONSTEVAL 0
PATCH
# pods_root: the Pods directory (installer.sandbox.root).
def self.apply!(pods_root)
header = File.join(pods_root.to_s, 'fmt', 'include', 'fmt', 'base.h')
unless File.exist?(header)
Pod::UI.warn "fmt: #{header} not found, skipping consteval patch."
return
end
contents = File.read(header)
if contents.include?(MARKER)
Pod::UI.puts 'fmt: consteval patch already applied.'
return
end
index = contents.index(ANCHOR)
if index.nil?
Pod::UI.warn 'fmt: could not find `#if FMT_USE_CONSTEVAL` in base.h; ' \
'the consteval patch was NOT applied. If this fmt version ' \
'still uses a consteval format-string check, builds on ' \
'Xcode >= 26.2 will fail -- update ' \
'ios/scripts/patch_fmt_consteval.rb.'
return
end
contents.insert(index, OVERRIDE)
# CocoaPods checks pod sources out read-only (0444), so make the header
# writable for the write and restore the original mode afterwards.
mode = File.stat(header).mode & 0o7777
begin
File.chmod(mode | 0o200, header)
File.write(header, contents)
ensure
File.chmod(mode, header)
end
Pod::UI.puts 'fmt: patched base.h to set FMT_USE_CONSTEVAL=0 (Xcode >= 26.2 compatibility).'
end
end

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.4.5",
"version": "3.4.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.4.5",
"version": "3.4.10",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -191,6 +191,7 @@
"version": "2.1.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/common": "^2.1.3",
"@notesnook/core": "file:../core",
"@readme/data-urls": "^3.0.0",
"dayjs": "1.11.13",
@@ -333,7 +334,10 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.4.7",
"version": "3.4.10",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {

View File

@@ -1,8 +1,98 @@
diff --git a/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
index 70149d2..fc917f3 100644
index 70149d2..cc2b272 100644
--- a/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
+++ b/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
@@ -604,7 +604,7 @@ class RNIapModule(
@@ -15,10 +15,8 @@ import com.android.billingclient.api.GetBillingConfigParams
import com.android.billingclient.api.GetBillingConfigParams.Builder
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
-import com.android.billingclient.api.PurchaseHistoryRecord
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
-import com.android.billingclient.api.QueryPurchaseHistoryParams
import com.android.billingclient.api.QueryPurchasesParams
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.LifecycleEventListener
@@ -38,11 +36,14 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
+import com.android.billingclient.api.PendingPurchasesParams
@ReactModule(name = RNIapModule.TAG)
class RNIapModule(
private val reactContext: ReactApplicationContext,
- private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(),
+ private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(
+ PendingPurchasesParams.newBuilder().enableOneTimeProducts().build(),
+),
private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance(),
) : ReactContextBaseJavaModule(reactContext),
PurchasesUpdatedListener {
@@ -275,8 +276,10 @@ class RNIapModule(
.setProductList(skuList)
.build()
- billingClient.queryProductDetailsAsync(params) { billingResult, skuDetailsList ->
- if (!isValidResult(billingResult, promise)) return@queryProductDetailsAsync
+ billingClient.queryProductDetailsAsync(params) { billingResult, queryProductDetailsResult ->
+ if (!isValidResult(billingResult, promise)) return@queryProductDetailsAsync
+
+ val skuDetailsList = queryProductDetailsResult.productDetailsList
val items = Arguments.createArray()
for (skuDetails in skuDetailsList) {
@@ -553,43 +556,12 @@ class RNIapModule(
type: String,
promise: Promise,
) {
- ensureConnection(
- promise,
- ) { billingClient ->
- billingClient.queryPurchaseHistoryAsync(
- QueryPurchaseHistoryParams
- .newBuilder()
- .setProductType(
- if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
- ).build(),
- ) { billingResult: BillingResult, purchaseHistoryRecordList: MutableList<PurchaseHistoryRecord>? ->
-
- if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
-
- Log.d(TAG, purchaseHistoryRecordList.toString())
- val items = Arguments.createArray()
- purchaseHistoryRecordList?.forEach { purchase ->
- val item = Arguments.createMap()
- // Add both field names for compatibility
- item.putString("productId", purchase.products[0])
- item.putString("id", purchase.products[0])
- val products = Arguments.createArray()
- purchase.products.forEach { products.pushString(it) }
- item.putArray("productIds", products)
- item.putArray("ids", products)
- item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
- item.putString("transactionReceipt", purchase.originalJson)
- item.putString("purchaseToken", purchase.purchaseToken)
- item.putString("purchaseTokenAndroid", purchase.purchaseToken)
- item.putString("dataAndroid", purchase.originalJson)
- item.putString("signatureAndroid", purchase.signature)
- item.putString("developerPayload", purchase.developerPayload.orEmpty())
- item.putString("platform", "android")
- items.pushMap(item)
- }
- promise.safeResolve(items)
- }
- }
+ promise.safeReject(
+ "E_UNSUPPORTED",
+ "getPurchaseHistoryByType is no longer supported since Play Billing Library 8 " +
+ "removed queryPurchaseHistoryAsync. Use getAvailableItemsByType for active " +
+ "purchases, or reconstruct history server-side.",
+ )
}
@ReactMethod
@@ -604,7 +576,7 @@ class RNIapModule(
isOfferPersonalized: Boolean, // New parameter in V5
promise: Promise,
) {

View File

@@ -128,7 +128,8 @@ const EXTRA_ICON_NAMES = [
"identifier",
"image-area",
"clock-outline",
"delete-sweep-outline"
"delete-sweep-outline",
"sync"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -141,7 +141,7 @@ export default function Monograph() {
bg: "background-secondary",
border: "1px solid var(--border)"
}}
href="https://help.notesnook.com/publish-notes-with-monographs"
href="https://notesnook.com/help/publish-notes-with-monographs"
target="_blank"
>
How it works

View File

@@ -33,7 +33,6 @@ import {
} from "./utils";
import { NavigationMenuModel } from "./navigation-menu.model";
import { AppModel } from "./app.model";
import { getAppFromPage } from "../../../desktop/__tests__/electron-test/utils";
import { readFile } from "node:fs/promises";
export class SettingsViewModel {
@@ -135,6 +134,10 @@ export class SettingsViewModel {
};
if (IS_DESKTOP_TESTS) {
const { getAppFromPage } = await import(
"../../../desktop/__tests__/electron-test/utils"
);
await saveBackup();
const toast = new AppModel(this.page).toasts.toasts.locator(
getTestId("toast-message")

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.4.4",
"version": "3.4.6",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useMemo } from "react";
import { Box, Button, Flex, Image, Link, Text } from "@theme-ui/components";
import { Button, Flex, Image, Text } from "@theme-ui/components";
import { getRandom, usePromise } from "@notesnook/common";
import Holenstein from "../../assets/testimonials/holenstein.jpg";
import Jason from "../../assets/testimonials/jason.jpg";
@@ -26,6 +26,7 @@ import Cameron from "../../assets/testimonials/cameron.jpg";
import { hosts } from "@notesnook/core";
import { SettingsDialog } from "../../dialogs/settings";
import { strings } from "@notesnook/intl";
import { FixedColorSchemeThemeProvider } from "../theme-provider";
const testimonials = [
{
@@ -80,62 +81,18 @@ function AuthContainer(props) {
bg: "background"
}}
>
<Box
<FixedColorSchemeThemeProvider
colorScheme="dark"
sx={{
position: "relative",
overflow: "hidden",
flexDirection: "column",
display: ["none", "none", "flex"],
flex: 1
flex: 1,
background:
"radial-gradient(1200px 700px at 82% 18%, color-mix(in srgb, var(--accent) 14%, transparent) 0%, transparent 62%), var(--background-secondary)"
}}
>
<Box
as="svg"
version="1.1"
viewBox="0 0 1920 1080"
preserveAspectRatio="xMinYMin slice"
sx={{
position: "absolute",
top: -100,
left: 0,
height: "100%"
// opacity: 0.7,
}}
>
<g mask='url("#SvgjsMask1017")' fill="none">
<path
d="M1184.21-85.14C1033.8-60.27 964.89 302.42 717.38 307.22 469.87 312.02 483.97 244.72 250.55 244.72 17.13 244.72-98.53 307.08-216.28 307.22"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M641.38-10.43C534.57 43 590.55 387.5 384.53 392.38 178.52 397.26 2.17 282.99-129.16 282.38"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M1136.18-29.24C957.53-5.77 852.26 404.49 561.01 405.07 269.76 405.65 142.54 160.4-14.16 155.07"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M508.47-71.88C398.16-66.29 333.42 117.75 114.38 127.84-104.65 137.93-170.96 308.31-279.7 312.84"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M1104.88-26.74C976.63-19.04 883.5 217.2 653.03 218.11 422.55 219.02 427.1 155.61 201.17 155.61-24.75 155.61-136.64 217.96-250.68 218.11"
stroke="var(--icon)"
strokeWidth="2"
></path>
</g>
<defs>
<mask id="SvgjsMask1017">
<rect width="1440" height="500" fill="#ffffff"></rect>
</mask>
</defs>
</Box>
<Flex
p={50}
sx={{
@@ -162,17 +119,9 @@ function AuthContainer(props) {
<Text
variant="body"
mt={10}
sx={{ fontSize: 14, color: "paragraph-secondary" }}
sx={{ fontSize: 16, color: "paragraph-secondary" }}
>
{testimonial.text} {" "}
<Link
sx={{ fontStyle: "italic", color: "paragraph-secondary" }}
href={testimonial.link}
target="_blank"
rel="noopener noreferrer"
>
source
</Link>
{testimonial.text}
</Text>
<Flex mt={2} sx={{ alignItems: "center", justifyContent: "center" }}>
<Image
@@ -180,10 +129,12 @@ function AuthContainer(props) {
sx={{ borderRadius: 50, width: 40 }}
/>
<Flex ml={2} sx={{ flexDirection: "column" }}>
<Text variant="body" sx={{ fontSize: 14, fontWeight: "bold" }}>
<Text variant="body" sx={{ fontSize: 16, fontWeight: "bold" }}>
{testimonial.name}
</Text>
<Text variant="subBody">@{testimonial.username}</Text>
<Text variant="subBody" sx={{ fontSize: 13 }}>
@{testimonial.username}
</Text>
</Flex>
</Flex>
@@ -218,52 +169,18 @@ function AuthContainer(props) {
</Button>
</Flex>
</Flex>
</Box>
<Flex
</FixedColorSchemeThemeProvider>
<FixedColorSchemeThemeProvider
colorScheme="light"
sx={{
display: "flex",
position: "relative",
flex: 1.5,
flexDirection: "column"
background: "var(--background-secondary)"
}}
>
<Box
as="svg"
version="1.1"
viewBox="0 0 1920 1080"
preserveAspectRatio="xMinYMin slice"
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
width: "130%",
height: "100%"
}}
>
<path
d="M0 336L29.2 316.2C58.3 296.3 116.7 256.7 174.8 267.5C233 278.3 291 339.7 349.2 361.3C407.3 383 465.7 365 523.8 359.5C582 354 640 361 698.2 346.5C756.3 332 814.7 296 872.8 267.2C931 238.3 989 216.7 1047.2 202.3C1105.3 188 1163.7 181 1221.8 202.7C1280 224.3 1338 274.7 1396.2 298C1454.3 321.3 1512.7 317.7 1570.8 332C1629 346.3 1687 378.7 1745.2 366.2C1803.3 353.7 1861.7 296.3 1890.8 267.7L1920 239L1920 0L1890.8 0C1861.7 0 1803.3 0 1745.2 0C1687 0 1629 0 1570.8 0C1512.7 0 1454.3 0 1396.2 0C1338 0 1280 0 1221.8 0C1163.7 0 1105.3 0 1047.2 0C989 0 931 0 872.8 0C814.7 0 756.3 0 698.2 0C640 0 582 0 523.8 0C465.7 0 407.3 0 349.2 0C291 0 233 0 174.8 0C116.7 0 58.3 0 29.2 0L0 0Z"
fill="var(--background-secondary)"
></path>
<path
d="M0 627L29.2 607.3C58.3 587.7 116.7 548.3 174.8 564.7C233 581 291 653 349.2 683.5C407.3 714 465.7 703 523.8 703C582 703 640 714 698.2 724.8C756.3 735.7 814.7 746.3 872.8 742.7C931 739 989 721 1047.2 670.7C1105.3 620.3 1163.7 537.7 1221.8 528.7C1280 519.7 1338 584.3 1396.2 623.8C1454.3 663.3 1512.7 677.7 1570.8 666.8C1629 656 1687 620 1745.2 602C1803.3 584 1861.7 584 1890.8 584L1920 584L1920 237L1890.8 265.7C1861.7 294.3 1803.3 351.7 1745.2 364.2C1687 376.7 1629 344.3 1570.8 330C1512.7 315.7 1454.3 319.3 1396.2 296C1338 272.7 1280 222.3 1221.8 200.7C1163.7 179 1105.3 186 1047.2 200.3C989 214.7 931 236.3 872.8 265.2C814.7 294 756.3 330 698.2 344.5C640 359 582 352 523.8 357.5C465.7 363 407.3 381 349.2 359.3C291 337.7 233 276.3 174.8 265.5C116.7 254.7 58.3 294.3 29.2 314.2L0 334Z"
fill="var(--hover)"
></path>
<path
d="M0 735L29.2 731.5C58.3 728 116.7 721 174.8 739C233 757 291 800 349.2 832.3C407.3 864.7 465.7 886.3 523.8 886.3C582 886.3 640 864.7 698.2 859.3C756.3 854 814.7 865 872.8 870.5C931 876 989 876 1047.2 845.3C1105.3 814.7 1163.7 753.3 1221.8 729.8C1280 706.3 1338 720.7 1396.2 738.7C1454.3 756.7 1512.7 778.3 1570.8 789.2C1629 800 1687 800 1745.2 814.5C1803.3 829 1861.7 858 1890.8 872.5L1920 887L1920 582L1890.8 582C1861.7 582 1803.3 582 1745.2 600C1687 618 1629 654 1570.8 664.8C1512.7 675.7 1454.3 661.3 1396.2 621.8C1338 582.3 1280 517.7 1221.8 526.7C1163.7 535.7 1105.3 618.3 1047.2 668.7C989 719 931 737 872.8 740.7C814.7 744.3 756.3 733.7 698.2 722.8C640 712 582 701 523.8 701C465.7 701 407.3 712 349.2 681.5C291 651 233 579 174.8 562.7C116.7 546.3 58.3 585.7 29.2 605.3L0 625Z"
fill="var(--border)"
></path>
<path
d="M0 897L29.2 895.3C58.3 893.7 116.7 890.3 174.8 908.3C233 926.3 291 965.7 349.2 985.3C407.3 1005 465.7 1005 523.8 1003.3C582 1001.7 640 998.3 698.2 996.7C756.3 995 814.7 995 872.8 986C931 977 989 959 1047.2 939.2C1105.3 919.3 1163.7 897.7 1221.8 894C1280 890.3 1338 904.7 1396.2 911.8C1454.3 919 1512.7 919 1570.8 928C1629 937 1687 955 1745.2 960.3C1803.3 965.7 1861.7 958.3 1890.8 954.7L1920 951L1920 885L1890.8 870.5C1861.7 856 1803.3 827 1745.2 812.5C1687 798 1629 798 1570.8 787.2C1512.7 776.3 1454.3 754.7 1396.2 736.7C1338 718.7 1280 704.3 1221.8 727.8C1163.7 751.3 1105.3 812.7 1047.2 843.3C989 874 931 874 872.8 868.5C814.7 863 756.3 852 698.2 857.3C640 862.7 582 884.3 523.8 884.3C465.7 884.3 407.3 862.7 349.2 830.3C291 798 233 755 174.8 737C116.7 719 58.3 726 29.2 729.5L0 733Z"
fill="var(--hover)"
></path>
<path
d="M0 1081L29.2 1081C58.3 1081 116.7 1081 174.8 1081C233 1081 291 1081 349.2 1081C407.3 1081 465.7 1081 523.8 1081C582 1081 640 1081 698.2 1081C756.3 1081 814.7 1081 872.8 1081C931 1081 989 1081 1047.2 1081C1105.3 1081 1163.7 1081 1221.8 1081C1280 1081 1338 1081 1396.2 1081C1454.3 1081 1512.7 1081 1570.8 1081C1629 1081 1687 1081 1745.2 1081C1803.3 1081 1861.7 1081 1890.8 1081L1920 1081L1920 949L1890.8 952.7C1861.7 956.3 1803.3 963.7 1745.2 958.3C1687 953 1629 935 1570.8 926C1512.7 917 1454.3 917 1396.2 909.8C1338 902.7 1280 888.3 1221.8 892C1163.7 895.7 1105.3 917.3 1047.2 937.2C989 957 931 975 872.8 984C814.7 993 756.3 993 698.2 994.7C640 996.3 582 999.7 523.8 1001.3C465.7 1003 407.3 1003 349.2 983.3C291 963.7 233 924.3 174.8 906.3C116.7 888.3 58.3 891.7 29.2 893.3L0 895Z"
fill="var(--border)"
></path>
</Box>
{props.children}
</Flex>
</FixedColorSchemeThemeProvider>
</Flex>
);
}

View File

@@ -918,7 +918,7 @@ function NavigationDropdown() {
icon: Documentation.path,
key: "help-and-support",
onClick: () => {
window.open("https://help.notesnook.com/", "_blank");
window.open("https://notesnook.com/help/", "_blank");
}
},
{

View File

@@ -550,7 +550,7 @@ export const noteMenuItems: (
await exportNotes(
format.type,
db.notes.all.where((eb) => eb("id", "in", ids))
db.notes.exportable.where((eb) => eb("id", "in", ids))
);
}
}))

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
EmotionThemeProvider,
FixedThemeProvider,
ThemeScopes,
themeToCSS,
useThemeEngineStore
@@ -94,4 +95,25 @@ export function BaseThemeProvider(
);
}
export function FixedColorSchemeThemeProvider(
props: PropsWithChildren<
{
injectCssVars?: boolean;
scope?: keyof ThemeScopes;
colorScheme: "light" | "dark";
} & Omit<BoxProps, "variant">
>
) {
const { children, scope = "base", ...restProps } = props;
const theme = useThemeStore((store) =>
props.colorScheme === "dark" ? store.darkTheme : store.lightTheme
);
return (
<FixedThemeProvider {...restProps} scope={scope} theme={theme}>
{children}
</FixedThemeProvider>
);
}
export { EmotionThemeProvider as ScopedThemeProvider };

View File

@@ -33,15 +33,21 @@ type UnlockViewProps = {
};
export function UnlockView(props: UnlockViewProps) {
const { title, subtitle, buttonTitle, unlock } = props;
const [isWrong, setIsWrong] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined
);
const [isUnlocking, setIsUnlocking] = useState(false);
const passwordRef = useRef<HTMLInputElement>(null);
const submit = useCallback(async () => {
if (!passwordRef.current?.value) return;
const password = passwordRef?.current?.value;
if (!password) {
setErrorMessage(strings.passwordRequired());
return;
}
setIsUnlocking(true);
const password = passwordRef.current.value;
try {
await unlock(password);
} catch (e) {
@@ -49,7 +55,7 @@ export function UnlockView(props: UnlockViewProps) {
e instanceof Error &&
e.message.includes("ciphertext cannot be decrypted using that key")
) {
setIsWrong(true);
setErrorMessage(strings.passwordIncorrect());
} else {
showToast("error", `${strings.couldNotUnlock()}: ` + e);
console.error(e);
@@ -57,7 +63,7 @@ export function UnlockView(props: UnlockViewProps) {
} finally {
setIsUnlocking(false);
}
}, [setIsWrong, unlock]);
}, [setErrorMessage, unlock]);
return (
<Flex
@@ -110,12 +116,12 @@ export function UnlockView(props: UnlockViewProps) {
onKeyUp={async (e) => {
if (e.key === "Enter") {
await submit();
} else if (isWrong) {
setIsWrong(false);
} else if (errorMessage) {
setErrorMessage(undefined);
}
}}
/>
{isWrong && <ErrorText sx={{ mt: 1 }} error="Wrong password" />}
{errorMessage && <ErrorText sx={{ mt: 1 }} error={errorMessage} />}
<Button
mt={3}
variant="accent"

View File

@@ -179,7 +179,7 @@ const staticCommands: Command[] = [
id: "help",
title: strings.helpAndSupport(),
icon: ArrowTopRight,
action: () => (window.location.href = "https://help.notesnook.com"),
action: () => (window.location.href = "https://notesnook.com/help"),
group: strings.navigate(),
type: "command"
},

View File

@@ -386,7 +386,7 @@ export const SupportSettings: SettingsGroup[] = [
{
type: "button",
action: () =>
void window.open("https://help.notesnook.com/", "_blank"),
void window.open("https://notesnook.com/help/", "_blank"),
title: strings.open(),
variant: "secondary"
}

View File

@@ -124,7 +124,7 @@ export const PrivacySettings: SettingsGroup[] = [
if (!result) return;
try {
const url = new URL(result);
Config.set("corsProxy", `${url.protocol}//${url.hostname}`);
Config.set("corsProxy", url.href.replace(/\/$/, ""));
} catch (e) {
console.error(e);
showToast("error", strings.invalidCors());

View File

@@ -181,7 +181,7 @@ const DEFAULT_TIPS: Record<TipContext, Omit<Tip, "contexts">> = {
icon: ArrowTopRight,
onClick() {
window.open(
"https://help.notesnook.com/publish-notes-with-monographs",
"https://notesnook.com/help/publish-notes-with-monographs",
"_blank"
);
}

View File

@@ -40,7 +40,7 @@ import AuthContainer from "../components/auth-container";
import { useTimer } from "../hooks/use-timer";
import { ErrorText } from "../components/error-text";
import { AuthenticatorType, User } from "@notesnook/core";
import { showLogoutConfirmation } from "../dialogs/confirm";
import { ConfirmDialog, showLogoutConfirmation } from "../dialogs/confirm";
import { TaskManager } from "../common/task-manager";
import { strings } from "@notesnook/intl";
import { ScrollContainer } from "@notesnook/ui";
@@ -389,7 +389,7 @@ function Signup(props: BaseAuthComponentProps<"signup">) {
<Text
mt={4}
variant="subBody"
sx={{ fontSize: 13, textAlign: "center" }}
sx={{ fontSize: "subBody", textAlign: "center" }}
>
{strings.signupAgreement[0]()}{" "}
<Link
@@ -843,9 +843,6 @@ export function AuthForm<T extends AuthRoutes>(props: AuthFormProps<T>) {
const formRef = useRef<HTMLFormElement>(null);
const [form, setForm] = useState<AuthFormData[T] | undefined>();
if (isSubmitting)
return <Loader title={props.loading.title} text={props.loading.subtitle} />;
return (
<Flex
ref={formRef}
@@ -876,48 +873,74 @@ export function AuthForm<T extends AuthRoutes>(props: AuthFormProps<T>) {
}
}}
sx={{
flex: 1,
flexDirection: "column",
size: "100%",
alignItems: "center",
justifyContent: "center",
width: ["95%", "95%", "45%"],
alignSelf: "center"
justifyContent: "center"
}}
>
<Text variant={"heading"} sx={{ fontSize: 32, textAlign: "center" }}>
{title}
</Text>
<Text
variant="body"
mt={2}
mb={35}
<Flex
sx={{
fontSize: "title",
textAlign: "center",
color: "var(--paragraph-secondary)"
flexDirection: "column",
width: ["95%", "95%", "550px"],
background: "var(--background)",
p: 6,
my: 10,
borderRadius: "15px",
border: "1px solid var(--border)",
boxShadow: "0px 0px 10px 0px #00000019"
}}
>
{subtitle}
</Text>
{typeof children === "function" ? children(form) : children}
{canSkip && (
<Button
type="button"
variant="anchor"
<Text variant={"heading"} sx={{ fontSize: 32 }}>
{title}
</Text>
<Text
variant="body"
mt={2}
mb={2}
sx={{
mt: 5,
color: "paragraph",
textDecoration: "none"
}}
onClick={() => {
openURL("/notes/", { authenticated: false });
fontSize: "title",
color: "var(--paragraph-secondary)"
}}
>
{strings.skipAndGoToApp()}
</Button>
)}
{subtitle}
</Text>
{canSkip && (
<Button
type="button"
variant="secondary"
sx={{
position: "absolute",
top: 4,
right: 4,
bg: "transparent",
border: "2px solid var(--border)",
borderRadius: "default",
px: 2
}}
onClick={async () => {
const result = await ConfirmDialog.show({
title: strings.offlineMode(),
message: strings.offlineModeDesc(),
negativeButtonText: strings.cancel(),
positiveButtonText: strings.understand()
});
if (result) openURL("/notes/", { authenticated: false });
}}
>
{strings.skipAndGoToApp()}
</Button>
)}
{isSubmitting ? (
<Loader title={props.loading.title} text={props.loading.subtitle} />
) : typeof children === "function" ? (
children(form)
) : (
children
)}
<ErrorText error={error} mt={5} />
<ErrorText error={error} mt={5} />
</Flex>
</Flex>
);
}
@@ -939,8 +962,7 @@ function SubtitleWithAction(props: SubtitleWithActionProps) {
sx={{
textDecoration: "underline",
fontWeight: "bold",
fontSize: "subtitle",
color: "paragraph",
fontSize: "title",
cursor: "pointer"
}}
onClick={props.action.onClick}
@@ -960,7 +982,11 @@ export function AuthField(props: FieldProps) {
data-test-id={props["data-test-id"] || props.id}
sx={{ mt: 2, width: "100%" }}
styles={{
// label: { fontWeight: "normal" },
label: { fontWeight: "normal", fontSize: "subtitle" },
helpText: {
fontSize: "body",
my: "2px"
},
input: {
p: "12px",
borderRadius: "default",
@@ -983,21 +1009,20 @@ type SubmitButtonProps = {
text: string;
disabled?: boolean;
loading?: boolean;
sx?: Record<string, unknown>;
};
export function SubmitButton(props: SubmitButtonProps) {
return (
<Button
data-test-id="submitButton"
type="submit"
mt={50}
variant="accent"
px={50}
sx={{
borderRadius: 50,
alignItems: "center",
justifyContent: "center",
alignSelf: "center",
display: "flex"
alignSelf: "stretch",
py: 2,
mt: 3,
fontSize: "subtitle",
...props.sx
}}
disabled={props.disabled}
>

View File

@@ -356,16 +356,16 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
autoFocus
defaultValue={formData?.recoveryKey || ""}
/>
<Flex sx={{ gap: 1 }}>
<Flex sx={{ gap: 1, mt: 3 }}>
<Button
variant="secondary"
type="button"
sx={{ mt: 50, borderRadius: 50 }}
sx={{ flex: 1, py: 2, fontSize: "subtitle" }}
onClick={() => navigate("methods")}
>
{strings.back()}
</Button>
<SubmitButton text={strings.startAccountRecovery()} />
<SubmitButton text={strings.startAccountRecovery()} sx={{ flex: 1, mt: 0 }} />
</Flex>
<Button
@@ -447,11 +447,11 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
label={strings.confirmPassword()}
defaultValue={form?.confirmPassword}
/>
<Flex sx={{ gap: 1 }}>
<Flex sx={{ gap: 1, mt: 3 }}>
<Button
variant="secondary"
type="button"
sx={{ mt: 50, borderRadius: 50 }}
sx={{ flex: 1, py: 2, fontSize: "subtitle" }}
onClick={() =>
navigate(
formData?.userResetRequired ? "methods" : "method:key",
@@ -461,7 +461,7 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
>
{strings.back()}
</Button>
<SubmitButton text={strings.continue()} />
<SubmitButton text={strings.continue()} sx={{ flex: 1, mt: 0 }} />
</Flex>
</>
)}
@@ -539,31 +539,41 @@ export function RecoveryForm<T extends RecoveryRoutes>(
}
}}
sx={{
flex: 1,
flexDirection: "column",
size: "100%",
alignItems: "center",
justifyContent: "center",
width: ["95%", 420],
alignSelf: "center"
justifyContent: "center"
}}
>
<Text variant={"heading"} sx={{ fontSize: 32, textAlign: "center" }}>
{title}
</Text>
<Text
variant="body"
mt={2}
mb={35}
<Flex
sx={{
fontSize: "title",
textAlign: "center",
color: "var(--paragraph-secondary)"
flexDirection: "column",
width: ["95%", "95%", "550px"],
background: "var(--background)",
p: 6,
my: 10,
borderRadius: "15px",
border: "1px solid var(--border)",
boxShadow: "0px 0px 10px 0px #00000019"
}}
>
{subtitle}
</Text>
{typeof children === "function" ? children(form) : children}
<ErrorText error={error} sx={{ mt: 2 }} />
<Text variant={"heading"} sx={{ fontSize: 32 }}>
{title}
</Text>
<Text
variant="body"
mt={2}
mb={2}
sx={{
fontSize: "title",
color: "var(--paragraph-secondary)"
}}
>
{subtitle}
</Text>
{typeof children === "function" ? children(form) : children}
<ErrorText error={error} sx={{ mt: 2 }} />
</Flex>
</Flex>
);
}

5
docs/help/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vitepress/dist
.vitepress/cache
contents/v*/
.vitepress/sidebars/generated.mjs
.wrangler

View File

@@ -0,0 +1,222 @@
import { defineConfig } from "vitepress";
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs";
import taskLists from "markdown-it-task-lists";
import { sidebar } from "./sidebar.mjs";
import {
LATEST,
isArchivedPath,
versionOfPath,
versionsNavItem
} from "./versions.mjs";
// Latest docs live at the root; the /v<version>/ trees and their sidebars are
// composed from contents/_versions/ by scripts/build-versions.mjs, which runs
// before dev and build.
import { archivedSidebars } from "./sidebars/generated.mjs";
import { seoHead, seoTitle } from "./seo.mjs";
import { stringsMarkdownPlugin } from "./strings.mjs";
export default defineConfig({
title: "Notesnook Help",
description:
"Your complete and free resource to using Notesnook as a daily note taking app to organize your work and life while safeguarding your privacy.",
lang: "en-US",
srcDir: "./contents",
base: "/help/",
outDir: "./.vitepress/dist/help",
// Version overrides are source material for build-versions.mjs, not pages.
// The Standard Notes importer is unpublished for now — the page is kept in
// the repo but is not built, linked or listed in the sitemap. Delete the
// second entry (and restore the sidebar link) to publish it again.
srcExclude: [
"_versions/**",
"importing-notes/import-notes-from-standardnotes.md",
"self-hosting.md"
],
cleanUrls: true,
lastUpdated: true,
metaChunk: true,
sitemap: {
hostname: "https://notesnook.com/help",
// Only the latest docs belong in the sitemap.
transformItems: (items) =>
items.filter(
(i) => !isArchivedPath(`/${i.url}`) && !i.url.startsWith("404")
)
},
transformPageData(pageData, ctx) {
const path = `/${pageData.relativePath}`;
pageData.frontmatter.head ??= [];
// Archived pages are kept out of search engines so they don't compete with
// the latest docs, and are tagged so the layout can show a version banner.
if (isArchivedPath(path)) {
pageData.frontmatter.archivedVersion = versionOfPath(path);
pageData.frontmatter.latestVersion = LATEST;
pageData.frontmatter.head.push([
"meta",
{ name: "robots", content: "noindex,follow" }
]);
return;
}
// Canonical, Open Graph, Twitter cards and JSON-LD for the live docs.
seoTitle(pageData);
pageData.frontmatter.head.push(...seoHead(pageData, ctx));
},
head: [
["link", { rel: "icon", href: "/favicon.ico" }],
// The two weights that render above the fold on every page.
[
"link",
{
rel: "preload",
href: "/help/fonts/Inter-Regular.woff2",
as: "font",
type: "font/woff2",
crossorigin: ""
}
],
[
"link",
{
rel: "preload",
href: "/help/fonts/Inter-SemiBold.woff2",
as: "font",
type: "font/woff2",
crossorigin: ""
}
],
["meta", { name: "theme-color", content: "#008837" }],
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:site_name", content: "Notesnook Help" }],
["meta", { property: "og:image", content: "/logo.png" }],
[
"script",
{
async: "",
defer: "",
"data-website-id": "676a7449-2151-44f7-a8c7-3b0691cade30",
src: "https://aas.streetwriters.co/script.js",
"data-domains": "notesnook.com"
}
]
],
markdown: {
config(md) {
md.use(tabsMarkdownPlugin);
// `- [x] item` renders as a real checkbox instead of literal "[x]".
md.use(taskLists, { label: true, labelAfter: true });
// `{{archive}}` becomes the live label from packages/intl.
md.use(stringsMarkdownPlugin);
// An image that shares a line with text is a UI glyph ("press the ⋯
// button"), not a figure. Tag those so CSS can keep them in the line —
// :only-child can't be used for this because it ignores text nodes.
md.core.ruler.push("nn_inline_glyphs", (state) => {
for (const token of state.tokens) {
if (token.type !== "inline" || !token.children) continue;
// Line breaks split the inline token into segments. A screenshot on
// its own line inside a numbered step lives in the same inline token
// as the step's text, so "does this token contain text?" would wrongly
// shrink it — the question is whether text sits on *its* line.
let segment: typeof token.children = [];
const segments = [segment];
for (const child of token.children) {
if (child.type === "softbreak" || child.type === "hardbreak") {
segment = [];
segments.push(segment);
} else segment.push(child);
}
for (const line of segments) {
const sharesLineWithText = line.some(
(child) =>
(child.type === "text" && child.content.trim()) ||
child.type === "code_inline"
);
if (!sharesLineWithText) continue;
for (const child of line) {
if (child.type === "image")
child.attrJoin("class", "inline-glyph");
}
}
}
return true;
});
},
image: { lazyLoading: true }
},
themeConfig: {
logo: "/logo.png",
siteTitle: "Help",
nav: [
versionsNavItem,
{ text: "Downloads", link: "https://notesnook.com/downloads" },
{ text: "Pricing", link: "https://notesnook.com/pricing" },
{
text: "More",
items: [
{ text: "Notesnook", link: "https://notesnook.com" },
{ text: "Blog", link: "https://blog.notesnook.com" },
{ text: "Roadmap", link: "https://notesnook.com/roadmap" },
{ text: "Contact us", link: "https://notesnook.com/contact-us" },
{
text: "Report an issue",
link: "https://github.com/streetwriters/notesnook/issues/new/choose"
}
]
}
],
sidebar: { ...archivedSidebars, "/": sidebar },
search: {
provider: "local",
options: {
detailedView: true,
// Archived versions are excluded so a search for "archive a note" does
// not return the same article once per version.
_render(src, env, md) {
if (isArchivedPath(`/${env.relativePath}`)) return "";
return md.render(src, env);
}
}
},
outline: { level: [2, 3], label: "On this page" },
editLink: {
pattern:
"https://github.com/streetwriters/notesnook/edit/master/docs/help/contents/:path",
text: "Suggest an edit to this page"
},
lastUpdated: {
text: "Last updated",
formatOptions: { dateStyle: "medium", forceLocale: false }
},
socialLinks: [
{ icon: "github", link: "https://github.com/streetwriters/notesnook" },
{ icon: "mastodon", link: "https://mastodon.social/@notesnook" },
{ icon: "discord", link: "https://discord.com/invite/zQBK97EE22" },
{ icon: "x", link: "https://x.com/notesnook" }
],
footer: {
message:
'Made with care by <a href="https://streetwriters.co">Streetwriters</a>. Notesnook is <a href="https://github.com/streetwriters/notesnook">open source</a>.',
copyright: "Copyright © 2026 Streetwriters (Private) Limited"
},
docFooter: { prev: "Previous", next: "Next" },
externalLinkIcon: true,
returnToTopLabel: "Back to top",
darkModeSwitchLabel: "Appearance"
}
});

View File

@@ -0,0 +1,206 @@
/**
* Per-page SEO: canonical URL, Open Graph, Twitter cards and JSON-LD.
*
* The help site ranks #1 for high-intent queries like "import enex", so every
* page needs to be individually addressable, individually described, and
* eligible for rich results. Driven from each page's frontmatter:
*
* ---
* title: Import from Evernote # sidebar label
* description: One sentence… # meta description + search snippet
* pageTitle: How to import Evernote… # optional: overrides the <title> only
* keywords: [import enex, evernote…] # optional
* schema: howto | faq | article # optional, default article
* faqs: # required when schema: faq
* - q: …
* a: …
* ---
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { HeadConfig, TransformPageContext, PageData } from "vitepress";
import { resolveString } from "./strings.mjs";
const SITE = "https://notesnook.com/help";
const OG_IMAGE = `${SITE}/logo.png`;
const url = (relativePath: string) =>
`${SITE}/${relativePath
.replace(/(index)?\.md$/, "")
.replace(/\/$/, "")}`.replace(/\/$/, "") || SITE;
/** "organizing-notes/archive-notes.md" -> ["Organizing notes", "Archive notes"] */
function breadcrumbs(relativePath: string, title: string) {
const parts = relativePath.split("/").slice(0, -1);
const crumbs = [{ name: "Notesnook Help", item: SITE }];
let path = "";
for (const part of parts) {
path += `/${part}`;
crumbs.push({
name: part.replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase()),
item: `${SITE}${path}`
});
}
crumbs.push({ name: title, item: url(relativePath) });
return crumbs;
}
/**
* The page's markdown. `transformPageData`'s context does not carry the source,
* so it is read back off disk.
*/
function pageSource(relativePath: string) {
try {
return readFileSync(join(process.cwd(), "contents", relativePath), "utf8");
} catch {
return "";
}
}
/** Numbered list items in the first tab of a page become HowTo steps. */
const STRING_TOKEN = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)(?::(\d+))?\s*\}\}/g;
function howToSteps(src: string) {
const steps: { name: string; text: string }[] = [];
for (const line of src.split("\n")) {
const m = line.match(/^\s*\d+\.\s+(.*\S)\s*$/);
if (!m) continue;
const text = m[1]
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
// These steps come from the raw markdown, before the markdown-it plugin
// has swapped `{{key}}` for the app's label — resolve them here too, or
// the structured data Google reads ships the raw tokens.
.replace(STRING_TOKEN, (_m, key: string, count?: string) =>
resolveString(key, count ? Number(count) : undefined)
)
.replace(/[`*_]/g, "")
.trim();
if (text.length > 3) steps.push({ name: text.slice(0, 110), text });
if (steps.length >= 12) break;
}
return steps;
}
function jsonLd(pageData: PageData, ctx: TransformPageContext) {
const fm = pageData.frontmatter;
const title = (fm.pageTitle || fm.title || pageData.title) as string;
const description = (fm.description || "") as string;
const pageUrl = url(pageData.relativePath);
const graph: Record<string, unknown>[] = [];
graph.push({
"@type": "BreadcrumbList",
itemListElement: breadcrumbs(pageData.relativePath, title).map((c, i) => ({
"@type": "ListItem",
position: i + 1,
name: c.name,
item: c.item
}))
});
const publisher = {
"@type": "Organization",
name: "Notesnook",
url: "https://notesnook.com",
logo: OG_IMAGE
};
if (fm.schema === "faq" && Array.isArray(fm.faqs) && fm.faqs.length) {
graph.push({
"@type": "FAQPage",
mainEntity: fm.faqs.map((f: { q: string; a: string }) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a }
}))
});
} else if (fm.schema === "howto") {
const steps = howToSteps(pageSource(pageData.relativePath));
if (steps.length)
graph.push({
"@type": "HowTo",
name: title,
description,
url: pageUrl,
step: steps.map((s, i) => ({
"@type": "HowToStep",
position: i + 1,
name: s.name,
text: s.text,
url: `${pageUrl}#${i + 1}`
})),
tool: [{ "@type": "HowToTool", name: "Notesnook" }],
totalTime: fm.totalTime || undefined
});
}
graph.push({
"@type": "TechArticle",
headline: title,
description,
url: pageUrl,
inLanguage: "en",
isPartOf: {
"@type": "WebSite",
name: "Notesnook Help",
url: SITE
},
about: {
"@type": "SoftwareApplication",
name: "Notesnook",
applicationCategory: "ProductivityApplication",
operatingSystem: "Windows, macOS, Linux, Android, iOS, Web"
},
author: publisher,
publisher,
dateModified: pageData.lastUpdated
? new Date(pageData.lastUpdated).toISOString()
: undefined
});
return JSON.stringify({ "@context": "https://schema.org", "@graph": graph });
}
/**
* Head tags for one page. Returned as frontmatter `head` entries so VitePress
* merges them into the rendered <head>.
*/
export function seoHead(
pageData: PageData,
ctx: TransformPageContext
): HeadConfig[] {
const fm = pageData.frontmatter;
if (fm.layout === "home" && !fm.description) return [];
const title = (fm.pageTitle || fm.title || pageData.title) as string;
const description = (fm.description || "") as string;
const pageUrl = url(pageData.relativePath);
const fullTitle = fm.pageTitle
? `${fm.pageTitle} | Notesnook Help`
: `${title} | Notesnook Help`;
const head: HeadConfig[] = [
["link", { rel: "canonical", href: pageUrl }],
["meta", { property: "og:title", content: fullTitle }],
["meta", { property: "og:description", content: description }],
["meta", { property: "og:url", content: pageUrl }],
["meta", { property: "og:image", content: OG_IMAGE }],
["meta", { name: "twitter:card", content: "summary" }],
["meta", { name: "twitter:title", content: fullTitle }],
["meta", { name: "twitter:description", content: description }]
];
if (Array.isArray(fm.keywords) && fm.keywords.length)
head.push(["meta", { name: "keywords", content: fm.keywords.join(", ") }]);
head.push(["script", { type: "application/ld+json" }, jsonLd(pageData, ctx)]);
return head;
}
/** The <title> tag: prefer an SEO-shaped `pageTitle` when the page defines one. */
export function seoTitle(pageData: PageData) {
if (pageData.frontmatter.pageTitle)
pageData.title = pageData.frontmatter.pageTitle as string;
}

View File

@@ -0,0 +1,320 @@
/**
* Help site navigation.
*
* A page that is not listed here is unreachable from the sidebar, so every new
* article needs an entry. `link` values are extensionless and root-absolute —
* they mirror the file path under `contents/`, which is also the public URL.
*/
export const sidebar = [
{
text: "Getting started",
collapsed: false,
items: [
{ text: "All help topics", link: "/docs" },
{ text: "Create your first note", link: "/create-a-note-in-notesnook" },
{ text: "Search & navigation", link: "/search-and-navigation" },
{ text: "Keyboard shortcuts", link: "/keyboard-shortcuts" },
{ text: "Plans & limits", link: "/plans-and-limits" }
]
},
{
text: "Organizing notes",
collapsed: false,
items: [
{
text: "Notebooks",
link: "/organizing-notes/organize-notes-using-notebooks"
},
{ text: "Tags", link: "/organizing-notes/organize-notes-using-tags" },
{ text: "Colors", link: "/organizing-notes/organize-notes-using-colors" },
{
text: "Favorites",
link: "/organizing-notes/organize-notes-using-favorites"
},
{ text: "Pins", link: "/organizing-notes/pin-notes" },
{ text: "Archive", link: "/organizing-notes/archive-notes" },
{
text: "Side menu shortcuts",
link: "/organizing-notes/side-menu-shortcuts"
},
{ text: "Reminders", link: "/reminders" }
]
},
{
text: "Working with notes",
collapsed: false,
items: [
{ text: "Note actions", link: "/notes/note-actions" },
{ text: "Note links", link: "/note-links-and-backlinks" },
{ text: "Expiring notes", link: "/notes/note-expiry" },
{ text: "Version history", link: "/note-version-history" },
{ text: "Trash", link: "/trash" }
]
},
{
text: "Editor",
collapsed: false,
items: [
{
text: "Editor toolbar",
link: "/rich-text-editor/rich-text-editor-toolbar"
},
{ text: "Tabs & panes", link: "/rich-text-editor/editor-tabs-and-panes" },
{
text: "Personalizing the editor",
link: "/rich-text-editor/personalizing-rich-text-editor"
},
{
text: "Markdown shortcuts",
link: "/rich-text-editor/markdown-notes-editing"
},
{
text: "Headings",
link: "/rich-text-editor/headings-and-collapsible-sections"
},
{ text: "Tables", link: "/rich-text-editor/tables" },
{ text: "Task lists", link: "/rich-text-editor/task-and-todo-lists" },
{ text: "Outline lists", link: "/rich-text-editor/outline-lists" },
{ text: "Callouts", link: "/rich-text-editor/callouts" },
{ text: "Code blocks", link: "/rich-text-editor/code-blocks" },
{ text: "Math & formulas", link: "/rich-text-editor/math-and-formulas" },
{
text: "Images & embeds",
link: "/rich-text-editor/images-attachments-and-embeds"
},
{ text: "Find & replace", link: "/rich-text-editor/search-and-replace" }
]
},
{
text: "Importing notes",
collapsed: false,
items: [
{ text: "Overview", link: "/importing-notes/" },
{ text: "Evernote", link: "/importing-notes/import-notes-from-evernote" },
{
text: "Google Keep",
link: "/importing-notes/import-notes-from-googlekeep"
},
{ text: "Joplin", link: "/importing-notes/import-notes-from-joplin" },
{ text: "Obsidian", link: "/importing-notes/import-notes-from-obsidian" },
{
text: "Simplenote",
link: "/importing-notes/import-notes-from-simplenote"
},
// Standard Notes is unpublished for now; the page is excluded from the
// build in config.mts. Restore this entry when it goes live again.
// {
// text: "Standard Notes",
// link: "/importing-notes/import-notes-from-standardnotes"
// },
{
text: "ColorNote",
link: "/importing-notes/import-notes-from-colornote"
},
{ text: "UpNote", link: "/importing-notes/import-notes-from-upnote" },
{
text: "Skiff Pages",
link: "/importing-notes/import-notes-from-skiff-pages"
},
{
text: "Zoho Notebook",
link: "/importing-notes/import-notes-from-zoho-notebook"
},
{
text: "Fusebase (Nimbus Note)",
link: "/importing-notes/import-notes-from-fusebase"
},
{
text: "Markdown files",
link: "/importing-notes/import-notes-from-markdown-files"
},
{
text: "HTML files",
link: "/importing-notes/import-notes-from-html-files"
},
{
text: "Plaintext files",
link: "/importing-notes/import-notes-from-plaintext-files"
},
{
text: "TextBundle files",
link: "/importing-notes/import-notes-from-textbundle-files"
}
]
},
{
text: "Backup & export",
collapsed: false,
items: [
{
text: "Backup and restore",
link: "/backup-and-restore-notes-in-notesnook"
},
{ text: "Exporting notes", link: "/export-notes-from-notesnook" },
{ text: "Attachments & files", link: "/attachments-and-files" }
]
},
{
text: "Sync",
collapsed: false,
items: [
{ text: "How sync works", link: "/sync/how-sync-works" },
{ text: "Sync settings", link: "/sync/sync-settings" },
{ text: "Troubleshooting sync", link: "/sync/troubleshooting-sync" }
]
},
{
text: "Privacy & security",
collapsed: false,
items: [
{ text: "How is my data encrypted?", link: "/how-is-my-data-encrypted" },
{ text: "Private vault", link: "/lock-notes-with-private-vault" },
{ text: "App lock", link: "/app-lock" },
{ text: "Two-factor authentication", link: "/two-factor-authentication" },
{ text: "Privacy mode", link: "/privacy-mode" }
]
},
{
text: "Publishing",
collapsed: false,
items: [{ text: "Monographs", link: "/publish-notes-with-monographs" }]
},
{
text: "Web clipper",
collapsed: false,
items: [
{ text: "Installation", link: "/web-clipper/installation" },
{
text: "Clipping your first page",
link: "/web-clipper/clipping-your-first-web-page-with-web-clipper"
},
{ text: "Troubleshooting", link: "/web-clipper/troubleshooting" }
]
},
{
text: "Mobile",
collapsed: false,
items: [
{
text: "Home screen widgets",
link: "/mobile-integration/home-screen-widgets"
},
{
text: "Android quick actions",
link: "/mobile-integration/android-quick-actions"
},
{
text: "Pin to notifications",
link: "/mobile-integration/pin-notes-to-notifications"
},
{
text: "Quick notes",
link: "/mobile-integration/quick-note-from-notification"
},
{
text: "Share from other apps",
link: "/mobile-integration/share-things-from-other-apps"
}
]
},
{
text: "Desktop",
collapsed: false,
items: [
{
text: "Auto start",
link: "/desktop-integration/auto-start-on-system-startup"
},
{
text: "System tray menu",
link: "/desktop-integration/system-tray-menu"
},
{
text: "Jumplist & dock menu",
link: "/desktop-integration/jumplist-and-dock-menu"
},
{ text: "Spell checker", link: "/desktop-integration/spell-checker" },
{
text: "Updates & advanced",
link: "/desktop-integration/updates-and-advanced-settings"
}
]
},
{
text: "Appearance & themes",
collapsed: false,
items: [
{ text: "Customizing the app", link: "/customizing-notesnook" },
{
text: "Using themes",
link: "/custom-themes/using-themes",
items: [
{ text: "How themes work", link: "/custom-themes/introduction" },
{
text: "Theme Builder",
link: "/custom-themes/create-a-theme-with-theme-builder"
},
{
text: "Install from file",
link: "/custom-themes/install-a-theme-from-file"
},
{
text: "Publish a new theme",
link: "/custom-themes/publish-a-theme"
}
]
}
]
},
{
text: "Your account",
collapsed: false,
items: [
{ text: "Account settings", link: "/account-settings" },
{ text: "Notesnook Circle", link: "/notesnook-circle" },
{ text: "Recovering your account", link: "/recovering-your-account" },
{ text: "Deleting your account", link: "/deleting-your-account" },
{ text: "Gift cards", link: "/gift-cards" },
{ text: "Notesnook Wrapped", link: "/notesnook-wrapped" }
]
},
{
text: "Advanced",
collapsed: false,
items: [
{
text: "Inbox API",
items: [
{
text: "Getting started",
link: "/inbox-api/getting-started"
},
{
text: "Self-hosting the Inbox API",
link: "/inbox-api/self-hosting-inbox-api"
}
]
}
// { text: "Self-hosting Notesnook", link: "/self-hosting" }
]
},
{
text: "FAQs",
collapsed: false,
items: [
{
text: "What are merge conflicts?",
link: "/faqs/what-are-merge-conflicts"
},
{ text: "Is there an ETA for X feature?", link: "/faqs/is-there-an-eta" },
{
text: "Why login is needed to upload attachments",
link: "/faqs/login-to-upload-attachments"
},
{
text: "Why login is needed to restore attachments",
link: "/faqs/login-to-restore-attachments-in-backup"
}
]
}
];

View File

@@ -0,0 +1,131 @@
/**
* Live UI strings, straight from the app.
*
* The docs quote hundreds of button and menu labels. Typing them by hand means
* they rot the moment someone renames a string, so pages write a key instead:
*
* Click on `{{archive}}` -> Click on `Archive`
*
* The key is resolved at build time from `@notesnook/intl` — the same catalogue
* the apps render from — so renaming a string in the app updates every page that
* quotes it on the next build. An unknown key fails the build rather than
* shipping a placeholder.
*
* This only *reads* the catalogue. Never add strings to `packages/intl` for the
* docs' sake: if a label has no string, write it as plain text and say why.
*/
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { i18n } from "@lingui/core";
import { strings, setI18nGlobal } from "@notesnook/intl";
const require = createRequire(import.meta.url);
// The compiled English catalogue lives beside the package's dist output.
const localePath = require.resolve("@notesnook/intl/locales/$en.json");
const locale = JSON.parse(readFileSync(localePath, "utf8"));
i18n.load({ en: locale.messages });
i18n.activate("en");
setI18nGlobal(i18n);
export type StringKey = keyof typeof strings;
const cache = new Map<string, string>();
/**
* Resolve one key to the English text the app shows.
*
* A few catalogue entries are plural forms that take a count — quote those as
* `{{notebooks:2}}` and the number is passed through.
*/
export function resolveString(key: string, count?: number): string {
const cacheKey = count === undefined ? key : `${key}:${count}`;
const cached = cache.get(cacheKey);
if (cached !== undefined) return cached;
const entry = (strings as Record<string, unknown>)[key];
if (typeof entry !== "function")
throw new Error(
`Unknown UI string "${key}". It must be an existing key in packages/intl ` +
`(see strings.ts). Do not invent one — write the label as plain text instead.`
);
let value: unknown;
try {
value =
count === undefined
? (entry as () => unknown)()
: (entry as (n: number) => unknown)(count);
} catch {
throw new Error(
`UI string "${key}" needs arguments. If it is a plural, quote it as ` +
`{{${key}:2}}; otherwise write the label as plain text.`
);
}
if (typeof value !== "string" || !value.trim())
throw new Error(`UI string "${key}" did not resolve to text.`);
cache.set(cacheKey, value);
return value;
}
/**
* Reverse index: rendered text -> the key(s) that produce it. Used by
* `scripts/check-strings.mjs` to find hardcoded labels that could be keys.
*/
export function buildReverseIndex(): Map<string, string[]> {
const index = new Map<string, string[]>();
for (const key of Object.keys(strings)) {
let value: unknown;
try {
value = (strings as Record<string, () => unknown>)[key]();
} catch {
continue; // needs arguments
}
if (typeof value !== "string" || !value.trim()) continue;
const existing = index.get(value);
if (existing) existing.push(key);
else index.set(value, [key]);
}
return index;
}
/** Every key used across the docs this build, for reporting. */
export const usedKeys = new Set<string>();
const TOKEN = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)(?::(\d+))?\s*\}\}/g;
/**
* markdown-it rule: swap `{{key}}` for the live string while parsing, so the
* rendered HTML contains real text and Vue never sees a moustache.
*/
export function stringsMarkdownPlugin(md: any) {
md.core.ruler.push("nn_ui_strings", (state: any) => {
const where = state.env?.relativePath ? ` in ${state.env.relativePath}` : "";
const swap = (text: string) =>
text.replace(TOKEN, (_match: string, key: string, count?: string) => {
try {
const value = resolveString(key, count ? Number(count) : undefined);
usedKeys.add(key);
return value;
} catch (error) {
throw new Error((error as Error).message + where);
}
});
for (const token of state.tokens) {
if (token.type === "inline" && token.children) {
for (const child of token.children) {
if (child.type === "text" || child.type === "code_inline")
child.content = swap(child.content);
}
} else if (token.type === "fence" || token.type === "html_block") {
// Leave code fences alone; a doc may legitimately show `{{ }}` syntax.
continue;
}
if (token.type === "inline") token.content = swap(token.content);
}
return true;
});
}

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
/**
* Every page on the site, grouped exactly as the sidebar groups them.
*
* The home page has no sidebar, so without this there is no way to see what the
* documentation actually covers. Reads the same sidebar module the site is
* built from, so it can never drift from the navigation.
*/
import { sidebar } from "../../sidebar.mjs";
import { withBase } from "vitepress";
type Item = { text: string; link?: string; items?: Item[] };
// Drop this page's own entry — listing the index inside the index is noise.
const groups = (sidebar as Item[]).map((group) => ({
...group,
items: group.items?.filter((item) => item.link !== "/docs")
}));
const pageCount = groups.reduce(
(total, group) => total + (group.items?.filter((i) => i.link).length ?? 0),
0
);
</script>
<template>
<div class="nn-index">
<p class="nn-index__count">
{{ pageCount }} pages, grouped by what you're trying to do.
</p>
<div class="nn-index__grid">
<section
v-for="group in groups"
:key="group.text"
class="nn-index__group"
>
<h2 class="nn-index__heading">{{ group.text }}</h2>
<ul class="nn-index__list">
<li v-for="item in group.items" :key="item.link || item.text">
<a v-if="item.link" :href="withBase(item.link)">{{ item.text }}</a>
<span v-else>{{ item.text }}</span>
</li>
</ul>
</section>
</div>
</div>
</template>
<style scoped>
.nn-index__count {
margin: 0 0 28px;
color: var(--vp-c-text-2);
}
.nn-index__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 28px 32px;
}
.nn-index__group {
break-inside: avoid;
}
.nn-index__heading {
margin: 0 0 10px;
padding: 0 0 8px;
border: none;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--vp-c-text-3);
}
.nn-index__list {
margin: 0;
padding: 0;
list-style: none;
}
.nn-index__list li {
margin: 0 0 6px;
line-height: 1.5;
}
.nn-index__list a {
font-weight: 400;
text-decoration: none;
}
.nn-index__list a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
/**
* Conversion block for high-intent pages (importers, comparisons, "how do I…"
* pages that people land on from search). Renders real anchors so crawlers and
* no-JS clients follow them.
*/
withDefaults(
defineProps<{
title?: string;
text?: string;
/** Primary link target: "download" | "pricing" */
action?: string;
}>(),
{
title: "Ready to move your notes?",
text: "Notesnook is free to use, end-to-end encrypted by default, and open source. Install it on every device you own and your notes stay in sync — readable only by you.",
action: "download"
}
);
</script>
<template>
<aside class="nn-cta">
<p class="nn-cta__title">{{ title }}</p>
<p class="nn-cta__text">{{ text }}</p>
<p class="nn-cta__actions">
<a
v-if="action === 'download'"
class="nn-cta__button"
href="https://notesnook.com/downloads"
>Download Notesnook</a
>
<a
v-else
class="nn-cta__button"
href="https://notesnook.com/pricing"
>See plans and pricing</a
>
<a class="nn-cta__link" href="/plans-and-limits">What's included in each plan</a>
</p>
</aside>
</template>
<style scoped>
.nn-cta {
margin: 32px 0;
padding: 20px 24px;
border: 1px solid var(--vp-c-divider);
border-left: 3px solid var(--nn-accent);
border-radius: var(--nn-radius-large);
background-color: var(--vp-c-bg-alt);
}
.nn-cta__title {
margin: 0 0 6px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.nn-cta__text {
margin: 0 0 14px;
font-size: 15px;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.nn-cta__actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
margin: 0;
}
.nn-cta__button {
display: inline-block;
padding: 8px 16px;
border-radius: var(--nn-radius-button);
background-color: var(--nn-accent);
color: var(--nn-accent-foreground) !important;
font-size: 14px;
font-weight: 600;
text-decoration: none !important;
transition: background-color 100ms ease-out;
}
.nn-cta__button:hover {
background-color: #008837e6;
}
.nn-cta__link {
font-size: 14px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
const isMac = ref(false);
onMounted(() => {
isMac.value = /mac/i.test(navigator.platform || navigator.userAgent);
});
/**
* Open the site's own search modal. VitePress listens for a Cmd/Ctrl+K keydown
* on `window` and its nav button triggers search by dispatching exactly this
* synthetic event, so we reuse that path rather than reimplementing search.
*/
function openSearch() {
const event = new Event("keydown") as Event & { key: string; metaKey: boolean };
event.key = "k";
event.metaKey = true;
window.dispatchEvent(event);
}
</script>
<template>
<div class="nn-home-search">
<button
type="button"
class="nn-home-search__button"
aria-label="Search the documentation"
@click="openSearch"
>
<span class="nn-home-search__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
</span>
<span class="nn-home-search__placeholder">Search the docs</span>
<kbd class="nn-home-search__key">{{ isMac ? "⌘" : "Ctrl" }} K</kbd>
</button>
<p class="nn-home-search__hint">
Try import from Evernote, app lock, or why is my note not syncing.
</p>
</div>
</template>
<style scoped>
.nn-home-search {
max-width: 640px;
/* The hero's own bottom padding stops here, so the space below the search box
has to come from this margin — without it the features grid rides up over
the hint text. */
margin: 16px auto 56px;
padding: 0 24px;
}
@media (max-width: 640px) {
.nn-home-search {
margin: 8px auto 40px;
}
}
.nn-home-search__button {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 14px 16px;
border: 1.5px solid var(--vp-c-divider);
border-radius: var(--nn-radius-button, 10px);
background-color: var(--vp-c-bg);
color: var(--vp-c-text-3);
font-size: 16px;
text-align: left;
cursor: text;
transition: border-color 120ms ease-out, box-shadow 120ms ease-out;
}
.nn-home-search__button:hover,
.nn-home-search__button:focus-visible {
border-color: var(--nn-accent);
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
outline: none;
}
.nn-home-search__icon {
display: flex;
color: var(--vp-c-text-3);
}
.nn-home-search__placeholder {
flex: 1;
}
.nn-home-search__key {
flex-shrink: 0;
padding: 2px 6px;
border: 1px solid var(--vp-c-divider);
border-bottom-width: 2px;
border-radius: var(--nn-radius-default, 5px);
background-color: var(--vp-c-bg-alt);
font-family: var(--vp-font-family-mono);
font-size: 11px;
line-height: 1.6;
color: var(--vp-c-text-3);
}
.nn-home-search__hint {
margin: 10px 2px 0;
font-size: 13px;
color: var(--vp-c-text-3);
}
@media (max-width: 640px) {
.nn-home-search__key {
display: none;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
/** essential | pro | believer | free — the LOWEST plan that unlocks the feature. */
plan: string;
/** Set when the feature is limited to one platform, e.g. "Android only". */
note?: string;
}>();
const PLANS: Record<string, { label: string; title: string }> = {
free: {
label: "Free",
title: "Available on every plan, including Free"
},
essential: {
label: "Essential",
title: "Requires the Essential plan or higher (Essential, Pro, Believer)"
},
pro: {
label: "Pro",
title: "Requires the Pro plan or higher (Pro, Believer)"
},
believer: {
label: "Believer",
title: "Requires the Believer plan"
}
};
const tier = computed(() => PLANS[props.plan.toLowerCase()] ?? PLANS.pro);
</script>
<template>
<span class="nn-plan-tag ignore-header" :class="`nn-plan-tag--${plan.toLowerCase()}`" :title="tier.title">
{{ tier.label }}
<span v-if="note" class="nn-plan-tag__note">· {{ note }}</span>
</span>
</template>
<style scoped>
.nn-plan-tag {
display: inline-block;
vertical-align: middle;
margin-left: 6px;
padding: 1px 8px;
border-radius: 100px;
border: 1px solid transparent;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
line-height: 1.7;
white-space: nowrap;
text-transform: uppercase;
cursor: help;
}
.nn-plan-tag__note {
font-weight: 500;
text-transform: none;
opacity: 0.85;
}
.nn-plan-tag--free {
background-color: var(--vp-c-bg-alt);
border-color: var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.nn-plan-tag--essential,
.nn-plan-tag--pro,
.nn-plan-tag--believer {
background-color: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.nn-plan-tag--believer {
background-color: transparent;
border-color: var(--nn-accent);
}
h1 .nn-plan-tag,
h2 .nn-plan-tag,
h3 .nn-plan-tag {
position: relative;
top: -2px;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed } from "vue";
import { useData } from "vitepress";
const { frontmatter, page } = useData();
const archived = computed(() => frontmatter.value.archivedVersion as string | undefined);
const latest = computed(() => frontmatter.value.latestVersion as string | undefined);
// The same article in the latest docs, if it still exists there.
const latestLink = computed(() => {
const path = page.value.relativePath
.replace(/^v[\d.]+\//, "/")
.replace(/(index)?\.md$/, "");
return path.startsWith("/") ? path : `/${path}`;
});
</script>
<template>
<div v-if="archived" class="nn-version-banner">
<p>
You are reading the documentation for <strong>Notesnook v{{ archived }}</strong>.
The current version is v{{ latest }}.
</p>
<a :href="latestLink">Read the latest version of this page </a>
</div>
</template>
<style scoped>
.nn-version-banner {
margin-bottom: 24px;
padding: 15px 20px;
border: 1px solid var(--vp-c-warning-soft);
border-left: 3px solid var(--vp-c-warning-1);
border-radius: var(--nn-radius-large);
background-color: var(--vp-custom-block-warning-bg);
font-size: 14px;
line-height: 1.6;
}
.nn-version-banner p {
margin: 0;
color: var(--vp-c-text-1);
}
.nn-version-banner a {
display: inline-block;
margin-top: 6px;
color: var(--vp-c-brand-1);
font-weight: 500;
text-decoration: none;
}
.nn-version-banner a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,77 @@
/**
* Self-hosted webfonts, matching the Notesnook app.
* Inter is the app's UI font (apps/web/src/app.css), Fira Code its code font.
*/
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local(""), url("/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: local(""), url("/fonts/Inter-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: local(""), url("/fonts/Inter-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: local(""), url("/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 400;
font-display: swap;
src: local(""), url("/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 500;
font-display: swap;
src: local(""), url("/fonts/Inter-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 600;
font-display: swap;
src: local(""), url("/fonts/Inter-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 700;
font-display: swap;
src: local(""), url("/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Fira Code";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local(""), url("/fonts/fira-code-v21-latin-regular.woff2")
format("woff2");
}

View File

@@ -0,0 +1,31 @@
import type { Theme } from "vitepress";
// theme-without-fonts skips the default theme's own bundled Inter — we ship the
// exact Inter files the Notesnook app uses instead (see fonts.css).
import DefaultTheme from "vitepress/theme-without-fonts";
import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client";
import { h } from "vue";
import VersionBanner from "./components/VersionBanner.vue";
import PlanTag from "./components/PlanTag.vue";
import GetNotesnook from "./components/GetNotesnook.vue";
import HomeSearch from "./components/HomeSearch.vue";
import DocsIndex from "./components/DocsIndex.vue";
import "./fonts.css";
import "./notesnook.css";
export default {
extends: DefaultTheme,
Layout: () =>
h(DefaultTheme.Layout, null, {
// Renders only on pages under an archived /v<version>/ tree.
"doc-before": () => h(VersionBanner),
// The home page has no sidebar, so search is the primary way in.
"home-hero-after": () => h(HomeSearch)
}),
enhanceApp({ app }) {
enhanceAppWithTabs(app);
// Usable directly in markdown, no per-page import.
app.component("PlanTag", PlanTag);
app.component("GetNotesnook", GetNotesnook);
app.component("DocsIndex", DocsIndex);
}
} satisfies Theme;

View File

@@ -0,0 +1,628 @@
/**
* Notesnook design tokens applied to VitePress.
*
* Values come from @notesnook/theme (default-light / default-dark v2.1) and
* apps/web:
* accent #008837 packages/theme default themes
* radii 2.5 / 5 / 7 / 10 packages/theme/src/theme/index.ts
* space scale 6 / 10 / 15 / 20 packages/theme/src/theme/index.ts
* shadows menu / dialog packages/theme/src/theme/index.ts
* fonts Inter, Fira Code apps/web/src/app.css
*
* Notesnook's UI is flat: 5px radii, hairline borders, hover fills instead of
* cards, and a single accent used sparingly. This file keeps VitePress in that
* register rather than layering a second design language on top of it.
*/
/* ========================================================================== */
/* Tokens */
/* ========================================================================== */
:root {
/* Notesnook primitives */
--nn-accent: #008837;
--nn-accent-foreground: #ffffff;
--nn-shade: #0088371a; /* alpha(accent, .1) */
--nn-radius-small: 2.5px;
--nn-radius-default: 5px;
--nn-radius-large: 7px;
--nn-radius-dialog: 10px;
--nn-radius-button: 10px;
--nn-space-1: 6px;
--nn-space-2: 10px;
--nn-space-3: 15px;
--nn-space-4: 20px;
--nn-space-5: 25px;
--nn-static-orange: #ff9800;
/* Typography */
--vp-font-family-base: "Inter", "Noto Sans", Frutiger, Calibri, Myriad, Arial,
Ubuntu, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif;
--vp-font-family-mono: "Fira Code", "Fira Mono", Hack, Menlo, Consolas,
"Liberation Mono", "Courier New", monospace;
/* Layout — a help site reads better slightly narrower than VitePress' default */
--vp-layout-max-width: 1440px;
--vp-sidebar-width: 288px;
--vp-nav-height: 60px;
}
/* ------------------------------- light ---------------------------------- */
:root {
--vp-c-brand-1: #008837;
--vp-c-brand-2: #008837e6; /* app button hover — alpha(accent, .9) */
--vp-c-brand-3: #008837cc; /* app button active — alpha(accent, .8) */
--vp-c-brand-soft: var(--nn-shade);
--vp-c-bg: #ffffff;
--vp-c-bg-alt: #f7f7f7;
--vp-c-bg-soft: #f7f7f7;
--vp-c-bg-elv: #ffffff;
--vp-c-divider: #e8e8e8;
--vp-c-border: #e8e8e8;
--vp-c-gutter: #e8e8e8;
--vp-c-text-1: #202020; /* heading */
--vp-c-text-2: #505050; /* paragraph */
--vp-c-text-3: #777777; /* paragraph-secondary */
--nn-hover: #eeeeee;
--nn-selected-bg: #eeeeee;
--nn-selected-fg: #212121;
--nn-code-bg: #f7f7f7;
--nn-shadow-menu: 0px 0px 10px 0px #00000022;
--nn-shadow-dialog: 0px 0px 25px 5px #0000004e;
--vp-c-tip-1: #4f8a10;
--vp-c-tip-soft: #4f8a101a;
--vp-c-danger-1: #f54b42;
--vp-c-danger-soft: #f54b421a;
--vp-c-warning-1: #b26a00; /* darkened static orange for AA on white */
--vp-c-warning-soft: #ff98001f;
}
/* -------------------------------- dark ---------------------------------- */
.dark {
/* #008837 is only 3.06:1 on the app's #181818, so text-bearing brand steps
are lightened for readability. Solid accent fills stay exactly #008837. */
--vp-c-brand-1: #00b34a;
--vp-c-brand-2: #00c853;
--vp-c-brand-3: #008837;
--vp-c-brand-soft: #00883733;
--vp-c-bg: #181818;
--vp-c-bg-alt: #202020;
--vp-c-bg-soft: #202020;
--vp-c-bg-elv: #202020;
--vp-c-divider: #383838;
--vp-c-border: #2b2b2b;
--vp-c-gutter: #2b2b2b;
--vp-c-text-1: #e3e3e3;
--vp-c-text-2: #d3d3d3;
--vp-c-text-3: #818589;
--nn-hover: #2b2b2b;
--nn-selected-bg: #494949;
--nn-selected-fg: #fbfbfb;
--nn-code-bg: #202020;
--nn-shadow-menu: 0px 0px 10px 0px #00000078;
--nn-shadow-dialog: 0px 0px 25px 5px #000000aa;
--vp-c-tip-1: #7bb32e;
--vp-c-tip-soft: #4f8a1033;
--vp-c-danger-1: #f76b64;
--vp-c-danger-soft: #f54b4226;
--vp-c-warning-1: #ffa726;
--vp-c-warning-soft: #ff980026;
}
/* --------------------------- derived bindings ---------------------------- */
:root,
.dark {
--vp-button-brand-bg: var(--nn-accent);
--vp-button-brand-text: var(--nn-accent-foreground);
--vp-button-brand-border: transparent;
--vp-button-brand-hover-bg: #008837e6;
--vp-button-brand-hover-text: var(--nn-accent-foreground);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-active-bg: #008837cc;
--vp-button-brand-active-text: var(--nn-accent-foreground);
--vp-code-bg: var(--nn-code-bg);
--vp-code-block-bg: var(--nn-code-bg);
--vp-code-copy-code-bg: var(--nn-code-bg);
--vp-code-copy-code-hover-bg: var(--nn-hover);
--vp-shadow-1: var(--nn-shadow-menu);
--vp-shadow-2: var(--nn-shadow-menu);
--vp-shadow-3: var(--nn-shadow-menu);
--vp-shadow-4: var(--nn-shadow-dialog);
--vp-shadow-5: var(--nn-shadow-dialog);
--vp-custom-block-tip-bg: var(--vp-c-tip-soft);
--vp-custom-block-warning-bg: var(--vp-c-warning-soft);
--vp-custom-block-danger-bg: var(--vp-c-danger-soft);
--vp-custom-block-info-bg: var(--vp-c-bg-alt);
--vp-custom-block-details-bg: var(--vp-c-bg-alt);
}
/* ========================================================================== */
/* Base */
/* ========================================================================== */
html {
scroll-behavior: smooth;
}
::selection {
background-color: #00883766;
color: var(--nn-selected-fg);
}
.vp-doc h1,
.vp-doc h2,
.vp-doc h3,
.vp-doc h4 {
letter-spacing: -0.015em;
font-weight: 600;
}
.vp-doc h1 {
font-size: 32px;
line-height: 1.25;
margin-bottom: var(--nn-space-3);
}
/* The app never underlines its own separators heavily — keep section rules
hairline and give sections room to breathe. */
.vp-doc h2 {
margin-top: 44px;
padding-top: var(--nn-space-4);
border-top: 1px solid var(--vp-c-divider);
font-size: 22px;
letter-spacing: -0.01em;
}
.vp-doc h3 {
margin-top: 28px;
font-size: 17px;
}
.vp-doc p,
.vp-doc li {
line-height: 1.7;
}
.vp-doc a {
font-weight: 500;
text-decoration: none;
text-underline-offset: 3px;
}
.vp-doc a:hover {
text-decoration: underline;
}
/* ========================================================================== */
/* Navigation & sidebar — modelled on the app's navigation menu */
/* ========================================================================== */
.VPNav {
backdrop-filter: saturate(160%) blur(12px);
}
.VPNavBar:not(.home.top) {
background-color: color-mix(in srgb, var(--vp-c-bg) 88%, transparent);
}
.VPSidebar {
background-color: var(--vp-c-bg-alt) !important;
padding-top: var(--nn-space-3);
}
.VPSidebarItem.level-0 > .item > .text {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--vp-c-text-3);
}
/* Flat rows with a hover fill — same treatment as list items in the app. */
.VPSidebarItem.is-link > .item .link,
.VPSidebarItem .item > .link {
border-radius: var(--nn-radius-default);
}
/* The hover fill bleeds left of the label so item text keeps the same left edge
as the group headers and the nav logo (32px), instead of sitting 10px in. */
.VPSidebarItem.level-1 .item,
.VPSidebarItem.level-2 .item,
.VPSidebarItem.level-3 .item {
border-radius: var(--nn-radius-default);
margin-left: calc(var(--nn-space-2) * -1);
padding-left: var(--nn-space-2);
padding-right: var(--nn-space-1);
transition: background-color 100ms ease-out;
}
.VPSidebarItem.level-1:not(.is-active) .item:hover,
.VPSidebarItem.level-2:not(.is-active) .item:hover,
.VPSidebarItem.level-3:not(.is-active) .item:hover {
background-color: var(--nn-hover);
}
.VPSidebarItem.is-active > .item {
background-color: var(--nn-selected-bg);
}
.VPSidebarItem.is-active > .item > .indicator {
background-color: var(--nn-accent);
}
.VPSidebarItem.is-active > .item .link .text {
color: var(--nn-selected-fg);
font-weight: 600;
}
.VPSidebarItem .text {
font-size: 14px;
}
/* ========================================================================== */
/* Buttons */
/* ========================================================================== */
/* One radius for every button on the site. `!important` is needed because the
default theme's own button rules carry a scoped [data-v-*] attribute, which
outranks a plain class selector. */
.VPButton,
.VPButton.small,
.VPButton.medium,
.VPButton.big,
.VPNavBarSearchButton,
.DocSearch-Button,
.VPNavBarHamburger,
.VPLocalNav button,
.VPBackToTop,
.VPSidebarItem .caret,
.VPLocalSearchBox button,
.VPDocFooter button,
[class*="language-"] > button.copy,
[class*="language-"] > span.lang + button,
.vp-doc button {
border-radius: var(--nn-radius-button) !important;
}
/* ========================================================================== */
/* Home page */
/* ========================================================================== */
.VPHero .image-bg {
display: none;
}
/* The default theme nudges the hero image up and left by 32px to optically
centre it against the blurred `image-bg` circle behind it. We hide that
circle, so the nudge has nothing left to compensate for — it just lifts the
logo above the middle of the text column, leaving it centred on the heading
and tagline with the action buttons hanging below. Dropping the transform
centres it against the whole column: heading, tagline and actions. */
@media (min-width: 960px) {
.VPHero .image-container {
transform: none;
}
}
.VPHero .image-container img {
max-width: 208px;
max-height: 208px;
border-radius: 44px;
box-shadow: var(--nn-shadow-dialog);
}
.VPHome .VPFeature {
border-radius: var(--nn-radius-large);
border-color: var(--vp-c-divider);
transition: border-color 120ms ease-out, background-color 120ms ease-out;
}
.VPHome .VPFeature:hover {
border-color: var(--nn-accent);
background-color: var(--vp-c-bg-alt);
}
/* ========================================================================== */
/* Custom blocks */
/* ========================================================================== */
.vp-doc .custom-block {
border-radius: var(--nn-radius-large);
border: 1px solid var(--vp-c-divider);
border-left-width: 3px;
padding: var(--nn-space-3) var(--nn-space-4);
font-size: 15px;
}
.vp-doc .custom-block .custom-block-title {
font-weight: 600;
letter-spacing: -0.01em;
}
.vp-doc .custom-block p {
line-height: 1.65;
}
.vp-doc .custom-block.info {
border-color: var(--vp-c-divider);
border-left-color: var(--vp-c-text-3);
}
.vp-doc .custom-block.tip {
border-color: var(--vp-c-tip-soft);
border-left-color: var(--vp-c-tip-1);
}
.vp-doc .custom-block.warning {
border-color: var(--vp-c-warning-soft);
border-left-color: var(--vp-c-warning-1);
}
.vp-doc .custom-block.danger {
border-color: var(--vp-c-danger-soft);
border-left-color: var(--vp-c-danger-1);
}
.vp-doc .custom-block.details {
border-left-color: var(--vp-c-text-3);
}
.vp-doc .custom-block img {
margin-top: var(--nn-space-2);
}
/* ========================================================================== */
/* Platform tabs (vitepress-plugin-tabs) */
/* ========================================================================== */
.vp-doc .plugin-tabs {
border-radius: var(--nn-radius-large);
border: 1px solid var(--vp-c-divider);
background-color: transparent;
box-shadow: none;
overflow: hidden;
}
.vp-doc .plugin-tabs--tab-list {
background-color: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
padding: var(--nn-space-1) var(--nn-space-1) 0;
}
.vp-doc .plugin-tabs--tab {
font-size: 13px;
font-weight: 500;
color: var(--vp-c-text-2);
padding: var(--nn-space-1) var(--nn-space-2);
border-radius: var(--nn-radius-default) var(--nn-radius-default) 0 0;
transition: color 100ms ease-out, background-color 100ms ease-out;
}
.vp-doc .plugin-tabs--tab:hover {
background-color: var(--nn-hover);
color: var(--vp-c-text-1);
}
.vp-doc .plugin-tabs--tab[aria-selected="true"] {
color: var(--vp-c-brand-1);
font-weight: 600;
background-color: var(--vp-c-bg);
}
.vp-doc .plugin-tabs--tab::after {
background-color: transparent;
height: 2px;
}
.vp-doc .plugin-tabs--tab[aria-selected="true"]::after {
background-color: var(--nn-accent);
}
.vp-doc .plugin-tabs--content {
padding: var(--nn-space-4);
}
.vp-doc .plugin-tabs--content > :first-child {
margin-top: 0;
}
.vp-doc .plugin-tabs--content > :last-child {
margin-bottom: 0;
}
/* Nested tabs inside a custom block should not double up on chrome. */
.vp-doc .custom-block .plugin-tabs {
background-color: var(--vp-c-bg);
}
/* ========================================================================== */
/* Media, code, tables */
/* ========================================================================== */
.vp-doc img {
border-radius: var(--nn-radius-large);
border: 1px solid var(--vp-c-divider);
max-width: 100%;
}
/* The CSS reset makes every image `display: block`, which breaks the line when a
UI glyph is referenced mid-sentence ("press the ⋯ button"). An image sharing
its *line* with text is treated as a glyph — inline, sized to the text,
unframed. An image on its own line stays a block, even when it sits inside a
numbered step whose text is on the line above. */
.vp-doc img.inline-glyph {
display: inline-block;
vertical-align: text-bottom;
max-height: 1.5em;
width: auto;
margin: 0 2px;
border: none;
border-radius: var(--nn-radius-small);
}
/* ---------------------------- task lists -------------------------------- */
.vp-doc .contains-task-list {
padding-left: 0;
list-style: none;
}
.vp-doc .task-list-item {
display: flex;
align-items: flex-start;
gap: 8px;
}
.vp-doc .task-list-item-checkbox {
appearance: none;
flex-shrink: 0;
width: 16px;
height: 16px;
margin: 6px 0 0;
border: 1.5px solid var(--vp-c-divider);
border-radius: var(--nn-radius-small);
background-color: var(--vp-c-bg);
}
.vp-doc .task-list-item-checkbox:checked {
border-color: var(--nn-accent);
background-color: var(--nn-accent);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
background-size: 12px;
background-position: center;
background-repeat: no-repeat;
}
.vp-doc .task-list-item label {
cursor: default;
}
.vp-doc img[src$=".svg"] {
border: none;
}
.vp-doc [class*="language-"] {
border-radius: var(--nn-radius-large);
border: 1px solid var(--vp-c-divider);
}
.vp-doc code {
font-size: 0.875em;
border-radius: var(--nn-radius-small);
padding: 3px 5px;
}
.vp-doc :not(pre) > code {
color: var(--vp-c-text-1);
background-color: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
}
.vp-doc kbd {
display: inline-block;
padding: 2px 6px;
border-radius: var(--nn-radius-default);
border: 1px solid var(--vp-c-divider);
border-bottom-width: 2px;
background-color: var(--vp-c-bg-alt);
font-family: var(--vp-font-family-mono);
font-size: 12px;
line-height: 1.6;
}
.vp-doc table {
display: table;
width: 100%;
border-radius: var(--nn-radius-large);
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-collapse: separate;
border-spacing: 0;
}
.vp-doc th,
.vp-doc td {
border: none;
border-bottom: 1px solid var(--vp-c-divider);
}
.vp-doc tr:last-child td {
border-bottom: none;
}
.vp-doc tr:nth-child(2n) {
background-color: transparent;
}
.vp-doc tr:hover td {
background-color: var(--nn-hover);
}
.vp-doc th {
background-color: var(--vp-c-bg-alt);
font-weight: 600;
font-size: 13px;
letter-spacing: 0.01em;
}
/* ========================================================================== */
/* Search, footer nav */
/* ========================================================================== */
.VPLocalSearchBox .search-bar {
border-radius: var(--nn-radius-default);
border: 1.5px solid var(--vp-c-divider);
}
.VPLocalSearchBox .search-bar:focus-within {
border-color: var(--nn-accent);
border-width: 2px;
}
.VPLocalSearchBox .result {
border-radius: var(--nn-radius-default);
}
.pager-link {
border-radius: var(--nn-radius-large);
border-color: var(--vp-c-divider);
transition: background-color 100ms ease-out, border-color 100ms ease-out;
}
.pager-link:hover {
background-color: var(--vp-c-bg-alt);
border-color: var(--nn-accent);
}
.VPDocFooter .edit-link-button {
font-weight: 500;
}
@media (max-width: 640px) {
.vp-doc h1 {
font-size: 27px;
}
.vp-doc h2 {
font-size: 20px;
}
.vp-doc .plugin-tabs--content,
.vp-doc .custom-block {
padding: var(--nn-space-3);
}
}

View File

@@ -0,0 +1,36 @@
/**
* Documentation versions.
*
* The **latest** version is served from the site root (`/create-a-note`,
* `/organizing-notes/...`) so canonical URLs never move. Older versions are
* served from `/v<version>/`.
*
* Older versions are stored as *differences*, not copies. `contents/_versions/`
* holds only the pages whose content actually differs from the current docs;
* every other page is shared, and the full `/v<version>/` tree is composed at
* build time by `scripts/build-versions.mjs`.
*
* Cutting a new version: npm run version -- 3.5
* Changing a page afterwards: npm run fork -- 3.4 <page> (before editing)
*/
/** The version the docs at the site root describe. */
export const LATEST = "3.4";
/** Older versions, newest first. */
export const ARCHIVED = [];
export const isArchivedPath = (path) =>
ARCHIVED.some((v) => path.startsWith(`/v${v}/`));
export const versionOfPath = (path) =>
ARCHIVED.find((v) => path.startsWith(`/v${v}/`)) ?? LATEST;
/** The version picker shown in the nav bar. */
export const versionsNavItem = {
text: `v${LATEST}`,
items: [
{ text: `v${LATEST} (latest)`, link: "/" },
...ARCHIVED.map((v) => ({ text: `v${v}`, link: `/v${v}/` }))
]
};

85
docs/help/README.md Normal file
View File

@@ -0,0 +1,85 @@
# Notesnook Help
The source of [notesnook.com/help](https://notesnook.com/help), built with [VitePress](https://vitepress.dev).
```bash
npm install
npm run dev # http://localhost:5173
npm run build # production build; fails on dead internal links
npm run preview # serve the built site
```
## Where things are
| | |
| ------------------------ | --------------------------------------------------- |
| `contents/` | the articles — a file's path here is its public URL |
| `contents/public/` | images and fonts, served from `/` |
| `.vitepress/config.mts` | site config, nav, head |
| `.vitepress/sidebar.mjs` | the sidebar — **add every new article here** |
| `.vitepress/theme/` | Notesnook design tokens and self-hosted fonts |
## UI labels come from the app
Button and menu labels are written as string keys and resolved at build time from `@notesnook/intl`:
```md
Click on `{{archive}}` to archive the note.
```
Rename that string in the app and this page updates on the next build. An unknown key fails the build. Run `npm run strings` to see which hardcoded labels could become keys (`-- --fix` rewrites them). Don't add strings to `packages/intl` for the docs — if there's no key, write plain text.
## Writing an article
1. Create `contents/<section>/<slug>.md` with `title` (short, used in the sidebar) and `description` (one sentence, used as the search snippet) frontmatter.
2. Add it to the right group in `.vitepress/sidebar.mjs`.
3. Run `npm run build` before opening a PR.
Steps that differ per platform go in tabs, which stay in sync across the whole site via `key:platform`:
```md
:::tabs key:platform
== Desktop/Web
1. Right click on a note to open the `Note properties` menu.
== Mobile
1. Press the three dot button on a note.
:::
```
Callouts use VitePress containers — `::: info`, `::: tip`, `::: warning`, `::: danger`, `::: details`.
Renaming or moving a file changes a live URL that the apps and support replies link to. Don't, unless a 301 goes into `contents/public/_redirects` with it. Some pages are linked from inside the app via `packages/intl/src/strings.ts`, and the importer package links to the `importing-notes/*` slugs — grep both before touching a slug.
## Versioning
The docs are versioned by Notesnook version. The **latest** version lives at the site root, so canonical URLs never move; older versions are served from `/v<version>/` and reachable from the version picker in the nav bar.
Older versions are stored as **differences, not copies**. A page is shared by every version until it actually changes; only then does the old text get its own file. `.vitepress/versions.mjs` holds `LATEST` and the list of older versions.
**When Notesnook ships a new version:**
```bash
npm run version -- <next-version>
```
Nothing is copied — the outgoing version becomes an older version whose pages are all still shared with the root.
**When you change a page in a way that doesn't apply to the old version**, preserve the old text first, then edit the root copy as usual:
```bash
npm run fork -- <old-version> organizing-notes/archive-notes
```
That writes `contents/_versions/<old-version>/organizing-notes/archive-notes.md` — the only file that version needs. For a page that didn't exist in an older version, add its path to `contents/_versions/<version>/_excluded.txt` instead.
`npm run versions` (run automatically before dev and build) composes the full `/v<version>/` trees from the shared pages plus those overrides. The composed trees live in `contents/v<version>/` and are gitignored — never edit them.
Archived pages carry a banner linking to the current version of the same page, are excluded from search and the sitemap, and are `noindex` so they don't compete with the latest docs. Images are shared across versions.
## Deployment
`.github/workflows/help.publish.yml` builds and deploys `.vitepress/dist/` to Cloudflare Pages on every push to `master` that touches `docs/help/**`.

138
docs/help/STYLE.md Normal file
View File

@@ -0,0 +1,138 @@
# Help docs — writing conventions
Every page on notesnook.com/help follows these. They cover accuracy, plan tags, SEO and linking.
## 1. Accuracy is non-negotiable
Never describe UI from memory. Every menu label, settings path, limit, default and plan gate must be traced to source in this monorepo before it is written:
- user-facing strings — `packages/intl/src/strings.ts`
- web/desktop UI — `apps/web/src/`, `apps/desktop/src/`
- mobile UI — `apps/mobile/app/`
- limits and plan gates — `packages/common/src/utils/is-feature-available.ts`
- behaviour, sync, encryption, retention — `packages/core/src/`
### Quote labels by key, not by hand
Don't type a label and hope it stays true. Write the **string key** and the build resolves it from the app's own catalogue:
```md
Click on `{{archive}}` renders: Click on `Archive`
Open `{{privacyAndSecurity}}` renders: Open `Privacy & security`
```
Keys come from `packages/intl/src/strings.ts` — the same catalogue the apps render from — so when someone renames a string in the app, every page quoting it updates on the next build. An unknown key **fails the build**; it never ships as a placeholder.
- Plural entries take a count: `{{notebooks:2}}`.
- `npm run strings` lists labels that could be keys but aren't; `-- --fix` rewrites the unambiguous ones.
- **Never add a string to `packages/intl` for the docs' sake.** If a label has no key — third-party UI, native OS text, a screen that isn't localized — write it as plain text.
If web and mobile differ, both go in the platform tabs. If you cannot verify something, leave it out and flag it — never guess.
## 2. Frontmatter
```yaml
---
title: Archive # short sidebar label, 23 words
pageTitle: How to archive notes… # optional: SEO <title>, ~60 chars
description: One sentence… # required, <160 chars, becomes the search snippet
keywords: # optional, real search phrases
- archive notes notesnook
schema: howto # optional: howto | faq | article (default article)
faqs: # required when schema: faq
- q:
a:
---
```
`schema: howto` turns the page's numbered steps into HowTo structured data automatically. `schema: faq` emits FAQPage structured data from the `faqs` list — the same Q&As must also appear in the page body.
## 3. Plan tags
Any feature that needs a paid plan is tagged inline, on the heading that introduces it:
```md
## Set a note to expire <PlanTag plan="pro" />
```
`plan` is the **lowest** plan that unlocks it: `essential`, `pro` or `believer`. Plans are cumulative — a `pro` tag means Pro and Believer. For platform-limited features add a note:
```md
## Pin a note to your notifications <PlanTag plan="pro" note="Android only" />
```
Verify the tier in `is-feature-available.ts` before tagging. Also state the consequence in prose where it matters ("free plans keep 100 versions per note"), and link to [Plans & limits](/plans-and-limits).
## 4. Article shape
```md
# Full human title
One or two sentences: what this is and why someone would want it.
## Task in imperative form
:::tabs key:platform
== Desktop/Web
1.
== Mobile
1.
:::
```
- One `# H1`, first line of the body.
- `##` per task, phrased as an action: "Archive a note", not "Archiving".
- Numbered steps, one action each, ideally ≤ 6.
- Close with the confirmation state — what the user should now see.
- `key:platform` is mandatory on platform tabs so the choice syncs sitewide. Labels: `Desktop/Web`, `Desktop`, `Web`, `Mobile`, `Android`, `iOS`, `Windows`, `macOS`, `Linux`.
- If a feature is missing on a platform, say so in that tab rather than omitting it.
## 5. Callouts
`::: info` context · `::: tip` shortcut · `::: warning` data loss or something irreversible · `::: danger` unrecoverable · `::: details` folded tangent.
Anything touching the vault, encryption, recovery or deletion must state plainly that **Notesnook cannot recover data or passwords for you**.
## 6. SEO
The help site already ranks #1 for high-intent queries like `import enex`, so each page is a landing page:
- **Write the H1 as the question a person types.** "How do I import notes from Evernote?" beats "Evernote importing".
- **Use real phrasings in `##` headings** — "Can I use it offline?", "Why is my note not syncing?" — they win featured snippets.
- **Answer in the first 40 words** after the H1. That paragraph is what Google quotes.
- **Never leave alt text empty.** Describe what the reader should look for: `![The Archive item in the note context menu](/img.png)`.
- **Every page ends with a `## Related pages` list** of 36 links with descriptive anchors (the home page, `/docs` and `/404` are exempt — they are already link lists) — "[backing up your notes](/backup-and-restore-notes-in-notesnook)", never "click here". This is what builds the internal link graph.
- **Link the first mention** of any concept that has its own page, in body text, with the concept as the anchor.
- **Add `<GetNotesnook />`** to pages people arrive at from search with buying intent — importers, comparisons, "how do I move from X" — placed after the instructions, never before them.
- Prefer one page that fully answers a question over three thin pages.
## 7. Internal linking clusters
Pages are grouped into clusters, each with a hub that links to every member and members that link back to the hub and sideways to siblings:
| Cluster | Hub |
| ------------------ | ------------------------------------------------------------- |
| Importing | [Importing notes](/importing-notes/) |
| Editor | [Editor toolbar](/rich-text-editor/rich-text-editor-toolbar) |
| Organization | [Notebooks](/organizing-notes/organize-notes-using-notebooks) |
| Privacy & security | [How is my data encrypted?](/how-is-my-data-encrypted) |
| Sync | [How sync works](/sync/how-sync-works) |
| Plans | [Plans & limits](/plans-and-limits) |
Any page that mentions a paid feature links to the plans hub. Any page that mentions encryption links to the encryption hub.
## 8. Things that are not allowed
- Undocumented guesses about UI, limits or plan gates.
- Version numbers in body copy ("as of v3.2").
- "Simply", "just", "easily", "seamlessly", "powerful".
- Telling the reader to contact support before the documented steps.
- Image paths that don't exist — leave `<!-- TODO: screenshot — … -->` instead.
- Renaming or moving an existing page (its URL is live and linked from the apps). Some pages are linked from inside the app itself via `packages/intl/src/strings.ts` — grep it before touching a slug.
- More than one `# H1` on a page, and `##`/`###` headings inside a `:::tabs` block. Headings in tabs are emitted once per tab, so they show up twice in the page outline with duplicate anchors.
- Unquoted frontmatter values containing `: ` — the YAML parser fails the build. Quote them.
- Alt text that describes nothing: `drawing`, a filename, or an unfilled template. Inline UI glyphs ("press the ⋯ button") are the one case where a short label is correct.

View File

@@ -1,5 +1,6 @@
---
title: Oops!
description: The page you're looking for could not be found. Here are some helpful links to get you back on track.
---
# Oops!
@@ -8,6 +9,8 @@ Unfortunately, we cannot find what you are looking for.
Here are some other pages you might be interested in:
1. [Installing Notesnook](organizing-notes/organize-notes-using-notebooks)
2. [Creating your first note](create-a-note-in-notesnook)
3. [How is my data encrypted?](how-is-my-data-encrypted)
1. [Creating your first note](/create-a-note-in-notesnook)
2. [Organizing notes with notebooks](/organizing-notes/organize-notes-using-notebooks)
3. [How is my data encrypted?](/how-is-my-data-encrypted)
Still stuck? [Contact us](https://notesnook.com/contact-us).

View File

@@ -1,16 +0,0 @@
---
title: Notesnook Help
description: Your complete and free resource to using Notesnook as a daily note taking app to organize your work and life while safeguarding your privacy.
---
# Welcome to Notesnook Help
Notesnook is a free and open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
And with that convenience in mind, we believe that it is equally important to put together a simple & useful help website about everything Notesnook. Our help is a complete walkthrough of the Notesnook app and it's features.
Before we get started, let's [download & install Notesnook](https://notesnook.com/downloads) on all your devices.
Once that's done, let's learn how to [create your first note](/create-a-note-in-notesnook) in Notesnook.

View File

@@ -1,8 +0,0 @@
<link rel="stylesheet" type="text/css" href="/custom.css" />
<script
async
defer
data-website-id="ad34576b-2721-436c-b36a-47a614009d2b"
src="https://aas.streetwriters.co/script.js"
data-domains="help.notesnook.com"
></script>

View File

@@ -1,194 +0,0 @@
/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-display: swap;
font-weight: 400;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf")
format("truetype");
}
/* open-sans-600 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf")
format("truetype");
}
/* open-sans-700 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.ttf")
format("truetype");
}
/* open-sans-italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400;
font-display: swap;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.ttf")
format("truetype");
}
/* open-sans-600italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 600;
font-display: swap;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.ttf")
format("truetype");
}
/* open-sans-700italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 700;
font-display: swap;
src: local(""),
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff")
format("woff"),
/* Modern Browsers */
url("/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.ttf")
format("truetype");
}
/* fira-code-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: "Fira Code";
font-style: normal;
font-weight: 400;
src: url("/fonts/fira-code-v21-latin-regular.woff2") format("woff2"),
/* Super Modern Browsers */ url("/fonts/fira-code-v21-latin-regular.woff")
format("woff"),
/* Modern Browsers */ url("/fonts/fira-code-v21-latin-regular.ttf")
format("truetype"); /* Safari, Android, iOS */
}
html.light {
--font-family: "Open Sans", sans-serif;
--monospace-font-family: "Fira Code", monospace;
--primary: #008837;
--primary-fg: #fff;
--header-fg: #008837;
--link: #008837;
--heading: #000;
--page-bg: #ffffff;
--document-bg: #f7f7f7;
--selection-bg: #00883766;
--selection-fg: #fff;
--border: #e8e8e8;
--input-border: #e8e8e8;
--hover-bg: #e8e8e8;
--info-dim: #00883720;
--fg: #403f53;
--fg-dim: #403f53cc;
--fg-dimmer: #403f53bb;
--code-bg: #e8e8e8;
}
html.dark {
--font-family: "Open Sans", sans-serif;
--monospace-font-family: "Fira Code", monospace;
--primary: #008837;
--primary-fg: #fff;
--header-fg: #008837;
--link: #008837;
--page-bg: #0d0d0d;
--document-bg: #151515;
--border: #383838;
--info-dim: #00883720;
--button-bg: #3b3b3b;
--hover-bg: #202020;
--button-fg: #fff;
--input-bg: #2b2b2b;
--input-placeholder: #ababab;
--code-bg: #202020;
--selection-bg: #00883766;
--selection-fg: #fff;
--border-dim: #383838;
--input-border: #383838;
--input-bg: #151515;
}
nav li {
line-height: 24px;
}
li.nested {
min-height: 38px !important;
}
nav li li:not(li.nested):hover {
background-color: var(--hover-bg);
}
pre {
border: 1px solid var(--border);
}
.project-title a {
color: var(--fg);
font-weight: 600;
}
.project-subtitle {
color: var(--fg);
font-weight: 600;
}
.docgen-content {
box-shadow: none;
}
nav details[open] {
max-height: max-content;
}

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