Compare commits

...
Author SHA1 Message Date
StandardNotes CI 8c364f6d58 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-07-26 10:45:24 +00:00
Aman Harwara 9bc7533010 fix: preview modal keyboard handling (#1345) 2022-07-26 15:54:15 +05:30
StandardNotes CI 6f7dfaa9f6 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-07-25 15:10:47 +00:00
Aman Harwara 7b6b2bc5b1 fix: make modals mobile-friendly (#1337) 2022-07-25 20:18:24 +05:30
StandardNotes CI 36288ad36a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-07-20 21:12:51 +00:00
Aman Harwara 2573407851 feat: responsive popovers & menus (#1323) 2022-07-21 02:20:14 +05:30
Mo baf7fb0019 ci: ability to skip e2e workflow [skip e2e] 2022-07-18 11:36:21 -05:00
StandardNotes CI 9ea1b35e0f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2022-07-18 16:25:28 +00:00
Mo 98fc870d76 feat(releases): add mobile urls 2022-07-18 11:04:20 -05:00
Mo 4d30331861 chore: workflows 2022-07-15 07:41:04 -05:00
Mo 12e2b83344 chore: update readme [skip ci] 2022-07-15 07:38:38 -05:00
dependabot[bot] 9e835c3b07 chore(deps): bump actions/checkout from 2 to 3 (#1264)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-15 11:48:13 +02:00
StandardNotes CI 12168945f1 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
2022-07-14 15:27:11 +00:00
Mo a827274ac6 refactor(web): use asterisk icon for upgrade indicator (#1318) 2022-07-14 10:06:31 -05:00
StandardNotes CI 65c377ba62 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-07-14 14:52:44 +00:00
Mo c02ec9a5f9 ci(mobile): missing metadata 2022-07-14 09:32:26 -05:00
StandardNotes CI 40b344fd00 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-07-14 13:25:03 +00:00
StandardNotes CI 776fa579c4 chore(release): components 2022-07-14 13:24:49 +00:00
Mo b174493b4e fix(ste): revert border radius change 2022-07-14 08:01:18 -05:00
Mo a7a81b15a9 chore: mobile workflow 2022-07-14 07:42:07 -05:00
StandardNotes CI 135e5924bf chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2022-07-14 07:31:16 +00:00
Vardan Hakobyan aaf68a8f73 fix(web): prevent jumping the page contents on mobile when editing notes offline (#1294) 2022-07-14 11:06:06 +04:00
109 changed files with 1873 additions and 1735 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+4 -4
View File
@@ -19,7 +19,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
@@ -54,7 +54,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
@@ -128,7 +128,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
@@ -157,7 +157,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
@@ -42,7 +42,7 @@ jobs:
run:
working-directory: packages/desktop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
+1
View File
@@ -1,6 +1,7 @@
name: Sync Repo
on:
workflow_dispatch:
push:
branches:
- '*'
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Export version from package.json
run:
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV
@@ -77,7 +77,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Export version from package.json
run:
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Export version from package.json
run: |
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV
@@ -66,7 +66,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Export version from package.json
run: |
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Install dependencies
+4
View File
@@ -52,17 +52,20 @@ jobs:
run: yarn test
- name: Login to Docker Hub
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Publish SNJS Docker image for E2E testing
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
run: |
yarn docker build @standardnotes/snjs -t standardnotes/snjs:${{ github.sha }}
docker push standardnotes/snjs:${{ github.sha }}
- name: Run E2E test suite
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
uses: convictional/[email protected]
with:
owner: standardnotes
@@ -84,6 +87,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.CI_NPM_TOKEN }}
- name: Publish SNJS Docker image as stable
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
run: |
docker tag standardnotes/snjs:${{ github.sha }} standardnotes/snjs:latest
docker push standardnotes/snjs:latest
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install dependencies
run: yarn install --immutable
- name: Login to Docker Hub
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Install dependencies
@@ -34,7 +34,7 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Copy robots.txt
run: cp packages/web-server/public/robots.txt.production packages/web-server/public/robots.txt
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Install dependencies
@@ -31,7 +31,7 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Copy robots.txt
run: cp packages/web-server/public/robots.txt.development packages/web-server/public/robots.txt
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
TestDockerBuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Copy default configuration
-15
View File
@@ -71,18 +71,3 @@ You can configure the `DEFAULT_SYNC_SERVER` environment variable to set the defa
```
DEFAULT_SYNC_SERVER=https://sync.myserver
```
## Running E2E tests
In order to run a stable server environment for E2E tests that will is up to date with what we have on production please checkout locally the [e2e repository](https://github.com/standardnotes/e2e).
To start the server type in the e2e repository:
```
yarn install --immutable
yarn test:stable-server
```
Then once the whole server infra is up and ready for your tests you can run the e2e suite in the local browser by typing (make sure you have built all the packages beforehand):
```
yarn start:server:e2e
```
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.7.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
### Bug Fixes
* **ste:** revert border radius change ([b174493](https://github.com/standardnotes/app/commit/b174493b4ea3ca164cf5754ab791022b17b612ac))
## [2.7.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/components-meta
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -75,9 +75,9 @@
"binary": "c4479f8768f5f6cda41850212a8e03927990adfa33ebd36e29ee6de2335e8e46"
},
"org.standardnotes.simple-task-editor": {
"version": "1.4.2",
"base64": "1eff91ed0151905431a49b40089b957578c60492718bd0a6aad00d2e02fc5c02",
"binary": "dae94bf60e464b58733ad1396e8e4adb78d01cd67c1e56fdc62e44334c0d6a0e"
"version": "1.4.3",
"base64": "5e8f767ac00d5a5af9b3650e56230a2446db4656515fb88ee5293da989b0f172",
"binary": "7c6f499b8107f80ec5fd84e591c2c33291d1fc6db85f098df4f8308a0cda901d"
},
"org.standardnotes.token-vault": {
"version": "2.2.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/components-meta",
"version": "2.7.6",
"version": "2.7.7",
"private": true,
"author": "Standard Notes.",
"main": "dist",
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
### Bug Fixes
* **ste:** revert border radius change ([b174493](https://github.com/standardnotes/app/commit/b174493b4ea3ca164cf5754ab791022b17b612ac))
## [1.4.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/simple-task-editor
@@ -85,7 +85,7 @@ body {
left: 0;
height: 10px;
width: 10px;
border-radius: 10px;
border-radius: 1px;
background-color: var(--sn-stylekit-contrast-background-color);;
border: 1px solid var(--sn-stylekit-contrast-border-color);
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/simple-task-editor",
"private": true,
"version": "1.4.2",
"version": "1.4.3",
"main": "dist/dist.js",
"scripts": {
"components:compile": "webpack --config webpack.prod.js",
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.23.81](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.80](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-25)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.77](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.74](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.23.74",
"version": "3.23.81",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/icons
## [1.4.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/icons",
"version": "1.4.6",
"version": "1.4.7",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -0,0 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.00002 2.5L1.66669 7.5L10 18.3333L18.3334 7.5L15 2.5H5.00002Z"/>
</svg>

After

Width:  |  Height:  |  Size: 156 B

@@ -0,0 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.525 15L10 12.875L6.47502 15L7.40835 10.9916L4.30002 8.29996L8.40002 7.94996L10 4.16663L11.6 7.94163L15.7 8.29163L12.5917 10.9833L13.525 15ZM10 1.66663C5.39169 1.66663 1.66669 5.41663 1.66669 9.99996C1.66669 12.2101 2.54466 14.3297 4.10746 15.8925C4.88129 16.6663 5.79994 17.2802 6.81099 17.699C7.82204 18.1177 8.90567 18.3333 10 18.3333C12.2102 18.3333 14.3298 17.4553 15.8926 15.8925C17.4554 14.3297 18.3334 12.2101 18.3334 9.99996C18.3334 8.90561 18.1178 7.82198 17.699 6.81093C17.2802 5.79988 16.6664 4.88122 15.8926 4.1074C15.1188 3.33358 14.2001 2.71975 13.189 2.30096C12.178 1.88217 11.0944 1.66663 10 1.66663V1.66663Z"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

+4
View File
@@ -47,6 +47,7 @@ import CopyIcon from './ic-copy.svg'
import CreateAccountIllustration from './create-account-illustration.svg'
import DashboardIcon from './ic-dashboard.svg'
import DiamondIcon from './diamond-with-horizontal-lines.svg'
import DiamondFilledIcon from './ic-diamond-filled.svg'
import DownloadIcon from './ic-download.svg'
import DragIcon from './ic-drag.svg'
import DrawIcon from './ic-draw.svg'
@@ -167,6 +168,7 @@ import SNLogoIcon from './ic-standard-notes.svg'
import SortDescendingIcon from './ic-sort-descending.svg'
import SpreadsheetsIcon from './ic-spreadsheets.svg'
import StarFilledIcon from './ic-star-filled.svg'
import StarCircleFilled from './ic-star-circle-filled.svg'
import StarIcon from './ic-star.svg'
import StarVariantFilledIcon from './ic-star-variant-filled.svg'
import StrikethroughIcon from './ic-strikethrough.svg'
@@ -245,6 +247,7 @@ export {
CopyIcon,
CreateAccountIllustration,
DashboardIcon,
DiamondFilledIcon,
DiamondIcon,
DownloadIcon,
DragIcon,
@@ -365,6 +368,7 @@ export {
SNLogoIcon,
SortDescendingIcon,
SpreadsheetsIcon,
StarCircleFilled,
StarFilledIcon,
StarIcon,
StarVariantFilledIcon,
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.30.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-25)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/mobile
## [3.30.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/mobile
+2
View File
@@ -64,6 +64,8 @@ platform :ios do
skip_metadata: true,
skip_screenshots: true,
skip_binary_upload: true,
run_precheck_before_submit: false,
submission_information: { add_id_info_uses_idfa: false },
release_notes: {
'default': 'Fixes and improvements.'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.30.29",
"version": "3.30.37",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+34
View File
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-25)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-20)
**Note:** Version bump only for package @standardnotes/releases
# [1.3.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
### Features
* **releases:** add mobile urls ([98fc870](https://github.com/standardnotes/app/commit/98fc870d761b50dadc00ebb4e41abba987bbcfb0))
## [1.2.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.2.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.2.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.2.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/releases
## [1.2.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.2.95",
"version": "1.3.3",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+3
View File
@@ -32,6 +32,9 @@ const Downloads = {
WinIa32Exe: `${ReleaseUrl}-win-ia32.exe`,
WinX64Exe: `${ReleaseUrl}-win-x64.exe`,
WinExe: `${ReleaseUrl}-win.exe`,
AppStoreiOS: 'https://itunes.apple.com/us/app/standard-notes/id1285392450?mt=8',
GooglePlay: 'https://play.google.com/store/apps/details?id=com.standardnotes',
WebApp: 'https://app.standardnotes.com',
}
ensureDirExists('dist')
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.124.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
**Note:** Version bump only for package @standardnotes/snjs
## [2.124.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/snjs
## [2.124.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/snjs
+62
View File
@@ -0,0 +1,62 @@
# SNJS
SNJS is a client-side JavaScript library for [Standard Notes](https://standardnotes.com) that contains shared logic for all Standard Notes clients.
## Introduction
SNJS is a shared library for use in all Standard Notes clients (desktop, web, and mobile). Its role is to extract any business or data logic from client code, so that clients are mostly responsible for UI-level code, and dont have to think about encryption and key management, or even authentication or storage specifics. Extracting the code into a shared library also prevents us from having to write the same critical code on multiple platforms.
The entry point of SNJS is the [`SNApplication`](packages/snjs/lib/application.ts) class. The application class is a complete unit of application functionality. Theoretically, many instances of an application can be created, each with its own storage namespace and memory state. This can allow clients to support multiple user accounts.
An application must be supplied a custom subclass of [DeviceInterface](packages/snjs/lib/device_interface.ts). This allows the library to generalize all behavior a client will need to perform throughout normal client operation, such as saving data to a local database store, saving key/values, and accessing the keychain.
On Web platforms SNJS interacts with [`sncrypto`](https://github.com/standardnotes/snjs/tree/packages/sncrypto-common) to perform operations as mentioned in the [specification](https://github.com/standardnotes/snjs/blob/main/packages/snjs/specification.md) document. This includes operations like key generation and data encryption.
SNJS also interacts with a Standard Notes [syncing server](https://github.com/standardnotes/syncing-server-js), which is a zero-knowledge data and sync store that deals with encrypted data, and never learns of client secrets or sensitive information.
## Installation
`yarn add snjs`
## Integrating in module environment
```javascript
import { SNApplication } from 'snjs';
```
## Integrating in non-module web environment
```javascript
<script src="snjs.js"></script>
Object.assign(window, SNLibrary);
```
## Building
1. `yarn install --pure-lockfile`
2. `yarn start` to start Webpack in development mode (watches changes), or `yarn build` to create dist files.
## Tests
### E2E Tests
To run a stable server environment for E2E tests that is up to date with production, clone the [e2e repository](https://github.com/standardnotes/e2e), then run:
```
yarn install --immutable
yarn test:stable-server
```
Once the server infrastructure is ready, and you've built all packages, you can run the test suite in the browser via:
```
yarn start:server:e2e
```
### Unit Tests
From the root of the repository, run:
```
yarn run test:unit
```
+3
View File
@@ -40,6 +40,8 @@ export type IconType =
| 'color-fill'
| 'copy'
| 'dashboard'
| 'diamond-filled'
| 'diamond'
| 'download'
| 'drag'
| 'draw'
@@ -151,6 +153,7 @@ export type IconType =
| 'spreadsheets'
| 'standard-notes-2'
| 'standard-notes'
| 'star-circle-filled'
| 'star-filled'
| 'star-variant-filled'
| 'star'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.124.3",
"version": "2.124.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/toast
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/toast",
"version": "1.3.3",
"version": "1.3.4",
"private": true,
"main": "./src/index.ts",
"scripts": {
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-26)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-25)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-20)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.101](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.100](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/web-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web-server",
"version": "1.2.97",
"version": "1.2.104",
"license": "AGPL-3.0-or-later",
"private": true,
"author": "Standard Notes.",
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.41.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-26)
### Bug Fixes
* preview modal keyboard handling ([#1345](https://github.com/standardnotes/app/issues/1345)) ([9bc7533](https://github.com/standardnotes/app/commit/9bc7533010662732d671fd1a93e2fb498e9d14dc))
## [3.41.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-25)
### Bug Fixes
* make modals mobile-friendly ([#1337](https://github.com/standardnotes/app/issues/1337)) ([7b6b2bc](https://github.com/standardnotes/app/commit/7b6b2bc5b168e2262c32a65b03e7c214d9671be1))
# [3.41.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-20)
### Features
* responsive popovers & menus ([#1323](https://github.com/standardnotes/app/issues/1323)) ([2573407](https://github.com/standardnotes/app/commit/2573407851c1ee3e9392ce504f020db68b7856b2))
## [3.40.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-18)
**Note:** Version bump only for package @standardnotes/web
## [3.40.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/web
## [3.40.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
### Bug Fixes
* **web:** prevent jumping the page contents on mobile when editing notes offline ([#1294](https://github.com/standardnotes/app/issues/1294)) ([aaf68a8](https://github.com/standardnotes/app/commit/aaf68a8f73cf52b1256e3ba879cc00bbcdcf251d))
## [3.40.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/web
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.40.4",
"version": "3.41.2",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -1,26 +1,24 @@
import { observer } from 'mobx-react-lite'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { WebApplication } from '@/Application/Application'
import { useCallback, useRef, FunctionComponent, KeyboardEventHandler } from 'react'
import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react'
import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { AccountMenuPane } from './AccountMenuPane'
import MenuPaneSelector from './MenuPaneSelector'
type Props = {
export type AccountMenuProps = {
viewControllerManager: ViewControllerManager
application: WebApplication
onClickOutside: () => void
mainApplicationGroup: ApplicationGroup
}
const AccountMenu: FunctionComponent<Props> = ({
const AccountMenu: FunctionComponent<AccountMenuProps> = ({
application,
viewControllerManager,
onClickOutside,
mainApplicationGroup,
}) => {
const { currentPane, shouldAnimateCloseMenu } = viewControllerManager.accountMenuController
const { currentPane } = viewControllerManager.accountMenuController
const closeAccountMenu = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu()
@@ -33,11 +31,6 @@ const AccountMenu: FunctionComponent<Props> = ({
[viewControllerManager],
)
const ref = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(ref, () => {
onClickOutside()
})
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
(event) => {
switch (event.key) {
@@ -56,22 +49,15 @@ const AccountMenu: FunctionComponent<Props> = ({
)
return (
<div ref={ref} id="account-menu" className="sn-component">
<div
className={`max-h-120 absolute bottom-full left-0 z-footer-bar-item-panel flex min-w-80 max-w-xs cursor-auto flex-col overflow-y-auto rounded bg-default py-2 shadow-main ${
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150'
}`}
onKeyDown={handleKeyDown}
>
<MenuPaneSelector
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
menuPane={currentPane}
setMenuPane={setCurrentPane}
closeMenu={closeAccountMenu}
/>
</div>
<div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}>
<MenuPaneSelector
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
menuPane={currentPane}
setMenuPane={setCurrentPane}
closeMenu={closeAccountMenu}
/>
</div>
)
}
@@ -23,7 +23,9 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
isOpen,
hideWorkspaceOptions = false,
}: Props) => {
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
mainApplicationGroup.getDescriptors(),
)
useEffect(() => {
const applicationDescriptors = mainApplicationGroup.getDescriptors()
@@ -1,13 +1,13 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu'
import MenuItem from '@/Components/Menu/MenuItem'
import { MenuItemType } from '@/Components/Menu/MenuItemType'
import Popover from '@/Components/Popover/Popover'
type Props = {
mainApplicationGroup: ApplicationGroup
@@ -16,32 +16,11 @@ type Props = {
const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const toggleMenu = useCallback(() => {
if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsOpen(!isOpen)
}, [isOpen, setIsOpen])
useEffect(() => {
if (isOpen) {
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
})
}
}, [isOpen])
setIsOpen((isOpen) => !isOpen)
}, [])
return (
<>
@@ -58,19 +37,20 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
</div>
<Icon type="chevron-right" className="text-neutral" />
</MenuItem>
{isOpen && (
<div
ref={menuRef}
className="max-h-120 fixed min-w-68 overflow-y-auto rounded bg-default py-2 shadow-main"
style={menuStyle}
>
<WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
isOpen={isOpen}
/>
</div>
)}
<Popover
align="end"
anchorElement={buttonRef.current}
className="py-2"
open={isOpen}
side="right"
togglePopover={toggleMenu}
>
<WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
isOpen={isOpen}
/>
</Popover>
</>
)
}
@@ -1,11 +1,7 @@
import { WebApplication } from '@/Application/Application'
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import AttachedFilesPopover from './AttachedFilesPopover'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { PopoverTabs } from './PopoverTabs'
@@ -18,6 +14,8 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
import { FileItem, SNNote } from '@standardnotes/snjs'
import { addToast, ToastType } from '@standardnotes/toast'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
@@ -34,7 +32,6 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
application,
featuresController,
filesController,
filePreviewModalController,
navigationController,
notesController,
selectionController,
@@ -46,24 +43,9 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
const premiumModal = usePremiumModal()
const note: SNNote | undefined = notesController.firstSelectedNote
const [open, setOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
useEffect(() => {
if (filePreviewModalController.isOpen) {
keepMenuOpen(true)
} else {
keepMenuOpen(false)
}
}, [filePreviewModalController.isOpen, keepMenuOpen])
const [currentTab, setCurrentTab] = useState(
navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles,
@@ -78,29 +60,14 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
}, [currentTab, isAttachedTabDisabled])
const toggleAttachedFilesMenu = useCallback(async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
const newOpenState = !isOpen
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !open
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setOpen(newOpenState)
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
}, [onClickPreprocessing, open])
setIsOpen(newOpenState)
}, [onClickPreprocessing, isOpen])
const prospectivelyShowFilesPremiumModal = useCallback(() => {
if (!featuresController.hasFiles) {
@@ -132,10 +99,10 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop()
useEffect(() => {
if (isDraggingFiles && !open) {
if (isDraggingFiles && !isOpen) {
void toggleAttachedFilesMenu()
}
}, [isDraggingFiles, open, toggleAttachedFilesMenu])
}, [isDraggingFiles, isOpen, toggleAttachedFilesMenu])
const filesDragInCallback = useCallback((tab: PopoverTabs) => {
setCurrentTab(tab)
@@ -162,53 +129,41 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
return (
<div ref={containerRef}>
<Disclosure open={open} onChange={toggleAttachedFilesMenuWithEntitlementCheck}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
}
}}
ref={buttonRef}
className={`bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast ${
attachedFilesCount > 0 ? 'py-1 px-3' : ''
}`}
onBlur={closeOnBlur}
>
<VisuallyHidden>Attached files</VisuallyHidden>
<Icon type="attachment-file" className="block" />
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className="slide-down-animation max-h-120 fixed flex min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default shadow-main transition-transform duration-150"
onBlur={closeOnBlur}
>
{open && (
<AttachedFilesPopover
application={application}
filesController={filesController}
attachedFiles={attachedFiles}
allFiles={allFiles}
closeOnBlur={closeOnBlur}
currentTab={currentTab}
isDraggingFiles={isDraggingFiles}
setCurrentTab={setCurrentTab}
attachedTabDisabled={isAttachedTabDisabled}
/>
)}
</DisclosurePanel>
</Disclosure>
<button
className={classNames(
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
attachedFilesCount > 0 ? 'py-1 px-3' : '',
)}
title="Attached files"
aria-label="Attached files"
onClick={toggleAttachedFilesMenuWithEntitlementCheck}
ref={buttonRef}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
}}
>
<Icon type="attachment-file" />
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
</button>
<Popover
togglePopover={toggleAttachedFilesMenuWithEntitlementCheck}
anchorElement={buttonRef.current}
open={isOpen}
className="pt-2 md:pt-0"
>
<AttachedFilesPopover
application={application}
filesController={filesController}
attachedFiles={attachedFiles}
allFiles={allFiles}
currentTab={currentTab}
isDraggingFiles={isDraggingFiles}
setCurrentTab={setCurrentTab}
attachedTabDisabled={isAttachedTabDisabled}
/>
</Popover>
</div>
)
}
@@ -16,7 +16,6 @@ type Props = {
filesController: FilesController
allFiles: FileItem[]
attachedFiles: FileItem[]
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
currentTab: PopoverTabs
isDraggingFiles: boolean
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
@@ -28,7 +27,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
filesController,
allFiles,
attachedFiles,
closeOnBlur,
currentTab,
isDraggingFiles,
setCurrentTab,
@@ -87,7 +85,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onClick={() => {
setCurrentTab(PopoverTabs.AttachedFiles)
}}
onBlur={closeOnBlur}
disabled={attachedTabDisabled}
>
Attached
@@ -100,7 +97,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onClick={() => {
setCurrentTab(PopoverTabs.AllFiles)
}}
onBlur={closeOnBlur}
>
All files
</button>
@@ -117,7 +113,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onInput={(e) => {
setSearchQuery((e.target as HTMLInputElement).value)
}}
onBlur={closeOnBlur}
ref={searchInputRef}
/>
{searchQuery.length > 0 && (
@@ -127,7 +122,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
setSearchQuery('')
searchInputRef.current?.focus()
}}
onBlur={closeOnBlur}
>
<Icon type="clear-circle-filled" className="text-neutral" />
</button>
@@ -144,7 +138,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
isAttachedToNote={attachedFiles.includes(file)}
handleFileAction={filesController.handleFileAction}
getIconType={application.iconsController.getIconForFileType}
closeOnBlur={closeOnBlur}
previewHandler={previewHandler}
/>
)
@@ -161,7 +154,7 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
? 'No files attached to this note'
: 'No files found in this account'}
</div>
<Button onClick={handleAttachFilesClick} onBlur={closeOnBlur}>
<Button onClick={handleAttachFilesClick}>
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
</Button>
<div className="mt-3 text-xs text-passive-0">Or drop your files here</div>
@@ -172,7 +165,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
<button
className="flex w-full cursor-pointer items-center border-0 border-t border-solid border-border bg-transparent px-3 py-3 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={handleAttachFilesClick}
onBlur={closeOnBlur}
>
<Icon type="add" className="mr-2 text-neutral" />
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
@@ -22,7 +22,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
isAttachedToNote,
handleFileAction,
getIconType,
closeOnBlur,
previewHandler,
}) => {
const [fileName, setFileName] = useState(file.name)
@@ -116,7 +115,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
isAttachedToNote={isAttachedToNote}
handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile}
closeOnBlur={closeOnBlur}
previewHandler={previewHandler}
/>
</div>
@@ -8,7 +8,6 @@ type CommonProps = {
handleFileAction: (action: PopoverFileItemAction) => Promise<{
didHandleAction: boolean
}>
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
previewHandler: (file: FileItem) => void
}
@@ -1,14 +1,12 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { PopoverFileSubmenuProps } from './PopoverFileItemProps'
import { PopoverFileItemActionType } from './PopoverFileItemAction'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import Popover from '../Popover/Popover'
const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
file,
@@ -19,187 +17,135 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
}) => {
const menuContainerRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [isFileProtected, setIsFileProtected] = useState(file.protected)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const closeMenu = useCallback(() => {
setIsMenuOpen(false)
setIsOpen(false)
}, [])
const toggleMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
setIsOpen((isOpen) => !isOpen)
}, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return (
<div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleMenu}>
<DisclosureButton
ref={menuButtonRef}
onBlur={closeOnBlur}
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
>
<Icon type="more" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
style={{
...menuStyle,
position: 'fixed',
<button
ref={menuButtonRef}
onClick={toggleMenu}
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
>
<Icon type="more" className="text-neutral" />
</button>
<Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
previewHandler(file)
closeMenu()
}}
className={`${
isMenuOpen ? 'flex' : 'hidden'
} max-h-120 fixed min-w-60 flex-col overflow-y-auto rounded bg-default py-1 shadow-main`}
>
{isMenuOpen && (
<>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
previewHandler(file)
closeMenu()
}}
>
<Icon type="file" className="mr-2 text-neutral" />
Preview file
</button>
{isAttachedToNote ? (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DetachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link-off" className="mr-2 text-neutral" />
Detach from note
</button>
) : (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link" className="mr-2 text-neutral" />
Attach to note
</button>
)}
<HorizontalSeparator classes="my-1" />
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.ToggleFileProtection,
payload: { file },
callback: (isProtected: boolean) => {
setIsFileProtected(isProtected)
},
}).catch(console.error)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" />
Password protection
</span>
<Switch
className="pointer-events-none px-0"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
checked={isFileProtected}
/>
</button>
<HorizontalSeparator classes="my-1" />
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DownloadFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="download" className="mr-2 text-neutral" />
Download
</button>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
setIsRenamingFile(true)
}}
>
<Icon type="pencil" className="mr-2 text-neutral" />
Rename
</button>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DeleteFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</button>
<div className="px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">File ID:</span> {file.uuid}
</div>
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
</div>
</div>
</>
)}
</DisclosurePanel>
</Disclosure>
<Icon type="file" className="mr-2 text-neutral" />
Preview file
</button>
{isAttachedToNote ? (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DetachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link-off" className="mr-2 text-neutral" />
Detach from note
</button>
) : (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="link" className="mr-2 text-neutral" />
Attach to note
</button>
)}
<HorizontalSeparator classes="my-1" />
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.ToggleFileProtection,
payload: { file },
callback: (isProtected: boolean) => {
setIsFileProtected(isProtected)
},
}).catch(console.error)
}}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" />
Password protection
</span>
<Switch
className="pointer-events-none px-0"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
checked={isFileProtected}
/>
</button>
<HorizontalSeparator classes="my-1" />
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DownloadFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="download" className="mr-2 text-neutral" />
Download
</button>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
setIsRenamingFile(true)
}}
>
<Icon type="pencil" className="mr-2 text-neutral" />
Rename
</button>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DeleteFile,
payload: { file },
}).catch(console.error)
closeMenu()
}}
>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</button>
<div className="px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">File ID:</span> {file.uuid}
</div>
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
</div>
</div>
</Popover>
</div>
)
}
@@ -1,11 +1,10 @@
import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import Popover from '../Popover/Popover'
type Props = {
mainApplicationGroup: ApplicationGroup
@@ -14,36 +13,12 @@ type Props = {
const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
useCloseOnClickOutside(containerRef, () => setIsOpen(false))
const toggleMenu = useCallback(() => {
if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsOpen(!isOpen)
}, [isOpen])
useEffect(() => {
if (isOpen) {
const timeToWaitBeforeCheckingMenuCollision = 0
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
}, timeToWaitBeforeCheckingMenuCollision)
}
}, [isOpen])
setIsOpen((isOpen) => !isOpen)
}, [])
return (
<div ref={containerRef}>
@@ -51,20 +26,22 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
<Icon type="user-switch" className="mr-2 text-neutral" />
Switch workspace
</Button>
{isOpen && (
<div
ref={menuRef}
className="max-h-120 fixed min-w-68 overflow-y-auto rounded-md bg-default py-2 shadow-main"
style={menuStyle}
>
<WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
isOpen={isOpen}
hideWorkspaceOptions={true}
/>
</div>
)}
<Popover
align="center"
anchorElement={buttonRef.current}
className="py-2"
open={isOpen}
overrideZIndex="z-modal"
side="right"
togglePopover={toggleMenu}
>
<WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
isOpen={isOpen}
hideWorkspaceOptions={true}
/>
</Popover>
</div>
)
}
@@ -1,13 +1,10 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from './ChangeEditorMenu'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
@@ -22,89 +19,38 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
}: Props) => {
const note = viewControllerManager.notesController.firstSelectedNote
const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
const toggleChangeEditorMenu = async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !isOpen
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(newOpenState)
setTimeout(() => {
setIsVisible(newOpenState)
})
const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen
if (willMenuOpen && onClickPreprocessing) {
await onClickPreprocessing()
}
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
return (
<div ref={containerRef}>
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="Change note type"
aria-label="Change note type"
onClick={toggleMenu}
ref={buttonRef}
>
<Icon type="dashboard" />
</button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
<ChangeEditorMenu
application={application}
isVisible={isOpen}
note={note}
closeMenu={() => {
setIsOpen(false)
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
>
<VisuallyHidden>Change note type</VisuallyHidden>
<Icon type="dashboard" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className="slide-down-animation max-h-120 fixed flex min-w-68 max-w-xs flex-col overflow-y-auto rounded bg-default shadow-main transition-transform duration-150"
onBlur={closeOnBlur}
>
{isOpen && (
<ChangeEditorMenu
closeOnBlur={closeOnBlur}
application={application}
isVisible={isVisible}
note={note}
closeMenu={() => {
setIsOpen(false)
}}
/>
)}
</DisclosurePanel>
</Disclosure>
/>
</Popover>
</div>
)
}
@@ -14,7 +14,7 @@ import {
SNNote,
TransactionalMutation,
} from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react'
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { createEditorMenuGroups } from './createEditorMenuGroups'
@@ -24,10 +24,10 @@ import {
transactionForDisassociateComponentWithCurrentNote,
} from '../NoteView/TransactionFunctions'
import { reloadFont } from '../NoteView/FontFunctions'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
type ChangeEditorMenuProps = {
application: WebApplication
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
closeMenu: () => void
isVisible: boolean
note: SNNote | undefined
@@ -35,25 +35,17 @@ type ChangeEditorMenuProps = {
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
application,
closeOnBlur,
closeMenu,
isVisible,
note,
}) => {
const [editors] = useState<SNComponent[]>(() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({ application, closeMenu, isVisible, note }) => {
const editors = useMemo(
() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
[application.componentManager],
)
const [groups, setGroups] = useState<EditorMenuGroup[]>([])
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
const [currentEditor, setCurrentEditor] = useState<SNComponent>()
useEffect(() => {
setGroups(createEditorMenuGroups(application, editors))
}, [application, editors])
useEffect(() => {
if (note) {
setCurrentEditor(application.componentManager.editorForNote(note))
@@ -194,7 +186,6 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
type={MenuItemType.RadioButton}
onClick={onClickEditorItem}
className={'flex-row-reverse py-2'}
onBlur={closeOnBlur}
checked={item.isEntitled ? isSelectedEditor(item) : undefined}
>
<div className="flex flex-grow items-center justify-between">
@@ -202,7 +193,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
</div>
{!item.isEntitled && <Icon type="premium-feature" />}
{!item.isEntitled && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
</div>
</MenuItem>
)
@@ -1,10 +1,9 @@
import { WebApplication } from '@/Application/Application'
import { Disclosure, DisclosurePanel } from '@reach/disclosure'
import { memo, useCallback, useRef, useState } from 'react'
import Icon from '../../Icon/Icon'
import { DisplayOptionsMenuPositionProps } from './DisplayOptionsMenuProps'
import DisplayOptionsMenuPortal from './DisplayOptionsMenuPortal'
import StyledDisplayOptionsButton from './StyledDisplayOptionsButton'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '@/Components/Popover/Popover'
import DisplayOptionsMenu from './DisplayOptionsMenu'
type Props = {
application: {
@@ -26,21 +25,12 @@ const ContentListHeader = ({
isFilesSmartView,
optionsSubtitle,
}: Props) => {
const [displayOptionsMenuPosition, setDisplayOptionsMenuPosition] = useState<DisplayOptionsMenuPositionProps>()
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
const toggleDisplayOptionsMenu = useCallback(() => {
if (displayOptionsButtonRef.current) {
const buttonBoundingRect = displayOptionsButtonRef.current.getBoundingClientRect()
setDisplayOptionsMenuPosition({
top: buttonBoundingRect.bottom,
left: buttonBoundingRect.right - buttonBoundingRect.width,
})
}
setShowDisplayOptionsMenu((show) => !show)
}, [])
@@ -52,24 +42,30 @@ const ContentListHeader = ({
</div>
<div className="flex">
<div className="relative" ref={displayOptionsContainerRef}>
<Disclosure open={showDisplayOptionsMenu} onChange={toggleDisplayOptionsMenu}>
<StyledDisplayOptionsButton $pressed={showDisplayOptionsMenu} ref={displayOptionsButtonRef}>
<Icon type="sort-descending" />
</StyledDisplayOptionsButton>
<DisclosurePanel>
{showDisplayOptionsMenu && displayOptionsMenuPosition && (
<DisplayOptionsMenuPortal
application={application}
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
containerRef={displayOptionsContainerRef}
isOpen={showDisplayOptionsMenu}
isFilesSmartView={isFilesSmartView}
top={displayOptionsMenuPosition.top}
left={displayOptionsMenuPosition.left}
/>
)}
</DisclosurePanel>
</Disclosure>
<button
className={classNames(
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
showDisplayOptionsMenu && 'bg-contrast',
)}
onClick={toggleDisplayOptionsMenu}
ref={displayOptionsButtonRef}
>
<Icon type="sort-descending" />
</button>
<Popover
open={showDisplayOptionsMenu}
anchorElement={displayOptionsButtonRef.current}
togglePopover={toggleDisplayOptionsMenu}
align="start"
className="py-2"
>
<DisplayOptionsMenu
application={application}
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
isFilesSmartView={isFilesSmartView}
isOpen={showDisplayOptionsMenu}
/>
</Popover>
</div>
<button
className="ml-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125"
@@ -97,14 +97,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
}, [application, hideEditorIcon])
return (
<Menu
className={
'slide-down-animation z-index-dropdown-menu flex min-w-70 flex-col overflow-y-auto rounded border border-solid border-border bg-default py-1 text-sm shadow-main transition-transform duration-150'
}
a11yLabel="Notes list options menu"
closeMenu={closeDisplayOptionsMenu}
isOpen={isOpen}
>
<Menu className="text-sm" a11yLabel="Notes list options menu" closeMenu={closeDisplayOptionsMenu} isOpen={isOpen}>
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
<MenuItem
className="py-2"
@@ -1,63 +0,0 @@
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { DisplayOptionsMenuPositionProps, DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
import DisplayOptionsMenu from './DisplayOptionsMenu'
import { useRef, useEffect, RefObject } from 'react'
type Props = DisplayOptionsMenuProps &
DisplayOptionsMenuPositionProps & {
containerRef: RefObject<HTMLDivElement>
}
const PositionedOptionsMenu = styled.div<DisplayOptionsMenuPositionProps>`
position: absolute;
top: ${(props) => props.top}px;
left: ${(props) => props.left}px;
z-index: var(--z-index-dropdown-menu);
`
const DisplayOptionsMenuPortal = ({
application,
closeDisplayOptionsMenu,
containerRef,
isFilesSmartView,
isOpen,
top,
left,
}: Props) => {
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const closeIfClickedOutside = (event: MouseEvent) => {
const isDescendantOfMenu = menuRef.current?.contains(event.target as Node)
const isDescendantOfContainer = containerRef.current?.contains(event.target as Node)
if (!isDescendantOfMenu && !isDescendantOfContainer) {
closeDisplayOptionsMenu()
}
}
document.addEventListener('click', closeIfClickedOutside, { capture: true })
return () => {
document.removeEventListener('click', closeIfClickedOutside, {
capture: true,
})
}
}, [closeDisplayOptionsMenu, containerRef])
return createPortal(
<PositionedOptionsMenu top={top} left={left} ref={menuRef}>
<div className="sn-component">
<DisplayOptionsMenu
application={application}
closeDisplayOptionsMenu={closeDisplayOptionsMenu}
isFilesSmartView={isFilesSmartView}
isOpen={isOpen}
/>
</div>
</PositionedOptionsMenu>,
document.body,
)
}
export default DisplayOptionsMenuPortal
@@ -1,10 +1,8 @@
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useRef } from 'react'
import Popover from '../Popover/Popover'
import FileMenuOptions from './FileMenuOptions'
type Props = {
@@ -15,92 +13,27 @@ type Props = {
const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => {
const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController
const [contextMenuStyle, setContextMenuStyle] = useState<React.CSSProperties>({
top: 0,
left: 0,
visibility: 'hidden',
})
const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState<number | 'auto'>('auto')
const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open))
useCloseOnClickOutside(contextMenuRef, () => filesController.setShowFileContextMenu(false))
const reloadContextMenuLayout = useCallback(() => {
const { clientHeight } = document.documentElement
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
const maxContextMenuHeight = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
let openUpBottom = true
if (footerHeightInPx) {
const bottomSpace = clientHeight - footerHeightInPx - fileContextMenuLocation.y
const upSpace = fileContextMenuLocation.y
if (maxContextMenuHeight > bottomSpace) {
if (upSpace > maxContextMenuHeight) {
openUpBottom = false
setContextMenuMaxHeight('auto')
} else {
if (upSpace > bottomSpace) {
setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER)
openUpBottom = false
} else {
setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER)
}
}
} else {
setContextMenuMaxHeight('auto')
}
}
if (openUpBottom) {
setContextMenuStyle({
top: fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
} else {
setContextMenuStyle({
bottom: clientHeight - fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
}
}, [fileContextMenuLocation.x, fileContextMenuLocation.y])
useEffect(() => {
if (showFileContextMenu) {
reloadContextMenuLayout()
}
}, [reloadContextMenuLayout, showFileContextMenu])
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
return (
<div
ref={contextMenuRef}
className="max-h-120 fixed z-dropdown-menu flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
style={{
...contextMenuStyle,
maxHeight: contextMenuMaxHeight,
}}
<Popover
open={showFileContextMenu}
anchorPoint={fileContextMenuLocation}
togglePopover={() => setShowFileContextMenu(!showFileContextMenu)}
side="right"
align="start"
className="py-2"
>
<FileMenuOptions
filesController={filesController}
selectionController={selectionController}
closeOnBlur={closeOnBlur}
closeMenu={() => setShowFileContextMenu(false)}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
/>
</div>
<div ref={contextMenuRef}>
<FileMenuOptions
filesController={filesController}
selectionController={selectionController}
closeMenu={() => setShowFileContextMenu(false)}
shouldShowRenameOption={false}
shouldShowAttachOption={false}
/>
</div>
</Popover>
)
})
@@ -11,7 +11,6 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
type Props = {
closeMenu: () => void
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
filesController: FilesController
selectionController: SelectedItemsController
isFileAttachedToNote?: boolean
@@ -22,7 +21,6 @@ type Props = {
const FileMenuOptions: FunctionComponent<Props> = ({
closeMenu,
closeOnBlur,
filesController,
selectionController,
isFileAttachedToNote,
@@ -73,7 +71,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
return (
<>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onPreview}
>
@@ -84,7 +81,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
<>
{isFileAttachedToNote ? (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onDetach}
>
@@ -93,7 +89,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button>
) : shouldShowAttachOption ? (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onAttach}
>
@@ -109,7 +104,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
onClick={() => {
void filesController.setProtectionForFiles(!hasProtectedFiles, selectionController.selectedFiles)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" />
@@ -123,7 +117,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button>
<HorizontalSeparator classes="my-1" />
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
void filesController.downloadFiles(selectionController.selectedFiles)
@@ -134,7 +127,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button>
{shouldShowRenameOption && (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
renameToggleCallback?.(true)
@@ -145,7 +137,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button>
)}
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
void filesController.deleteFilesPermanently(selectionController.selectedFiles)
@@ -1,13 +1,10 @@
import Icon from '@/Components/Icon/Icon'
import VisuallyHidden from '@reach/visually-hidden'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCallback, useRef, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import FileMenuOptions from './FileMenuOptions'
import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import Popover from '../Popover/Popover'
type Props = {
filesController: FilesController
@@ -15,80 +12,34 @@ type Props = {
}
const FilesOptionsPanel = ({ filesController, selectionController }: Props) => {
const [open, setOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
const onDisclosureChange = useCallback(async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
setOpen((open) => !open)
}
}, [])
const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), [])
return (
<Disclosure open={open} onChange={onDisclosureChange}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
<>
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="File options menu"
aria-label="File options menu"
onClick={toggleMenu}
ref={buttonRef}
>
<VisuallyHidden>Actions</VisuallyHidden>
<Icon type="more" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className={`${
open ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
onBlur={closeOnBlur}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{open && (
<FileMenuOptions
filesController={filesController}
selectionController={selectionController}
closeOnBlur={closeOnBlur}
closeMenu={() => {
setOpen(false)
}}
shouldShowAttachOption={false}
shouldShowRenameOption={false}
/>
)}
</DisclosurePanel>
</Disclosure>
<Icon type="more" />
</button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<FileMenuOptions
filesController={filesController}
selectionController={selectionController}
closeMenu={() => {
setIsOpen(false)
}}
shouldShowAttachOption={false}
shouldShowRenameOption={false}
/>
</Popover>
</>
)
}
@@ -27,9 +27,9 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
const keyDownHandler: KeyboardEventHandler = useCallback(
(event) => {
const hasNotPressedLeftOrRightKeys = event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right
const KeysToHandle: string[] = [KeyboardKey.Left, KeyboardKey.Right, KeyboardKey.Escape]
if (hasNotPressedLeftOrRightKeys) {
if (!KeysToHandle.includes(event.key)) {
return
}
@@ -54,9 +54,12 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
}
break
}
case KeyboardKey.Escape:
dismiss()
break
}
},
[currentFile.uuid, otherFiles, setCurrentFile],
[currentFile.uuid, dismiss, otherFiles, setCurrentFile],
)
const IconComponent = useMemo(
@@ -81,37 +84,39 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
className="flex min-h-[90%] min-w-[90%] flex-col rounded bg-[color:var(--modal-background-color)] p-0 shadow-main "
>
<div
className="min-h-6 flex flex-shrink-0 items-center justify-between border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none"
className="flex h-full w-full flex-col focus:shadow-none focus:outline-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
onKeyDown={keyDownHandler}
>
<div className="flex items-center">
<div className="h-6 w-6">{IconComponent}</div>
<span className="ml-3 font-medium">{currentFile.name}</span>
<div className="min-h-6 flex flex-shrink-0 items-center justify-between border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none">
<div className="flex items-center">
<div className="h-6 w-6">{IconComponent}</div>
<span className="ml-3 font-medium">{currentFile.name}</span>
</div>
<div className="flex items-center">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setShowFileInfoPanel((show) => !show)}
>
<Icon type="info" className="text-neutral" />
</button>
<button
ref={closeButtonRef}
onClick={dismiss}
aria-label="Close modal"
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
>
<Icon type="close" className="text-neutral" />
</button>
</div>
</div>
<div className="flex items-center">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setShowFileInfoPanel((show) => !show)}
>
<Icon type="info" className="text-neutral" />
</button>
<button
ref={closeButtonRef}
onClick={dismiss}
aria-label="Close modal"
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
>
<Icon type="close" className="text-neutral" />
</button>
<div className="flex min-h-0 flex-grow">
<div className="relative flex max-w-full flex-grow items-center justify-center">
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
</div>
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
</div>
</div>
<div className="flex min-h-0 flex-grow">
<div className="relative flex max-w-full flex-grow items-center justify-center">
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
</div>
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
</div>
</DialogContent>
</DialogOverlay>
)
@@ -0,0 +1,52 @@
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useRef } from 'react'
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
type Props = AccountMenuProps & {
isOpen: boolean
hasError: boolean
toggleMenu: () => void
user: unknown
}
const AccountMenuButton = ({
application,
hasError,
isOpen,
mainApplicationGroup,
onClickOutside,
toggleMenu,
user,
viewControllerManager,
}: Props) => {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button
ref={buttonRef}
onClick={toggleMenu}
className={classNames(
isOpen ? 'bg-border' : '',
'flex h-full w-8 cursor-pointer items-center justify-center rounded-full',
)}
>
<div className={hasError ? 'text-danger' : (user ? 'text-info' : 'text-neutral') + ' h-5 w-5'}>
<Icon type="account-circle" className="max-h-5 hover:text-info" />
</div>
</button>
<Popover anchorElement={buttonRef.current} open={isOpen} togglePopover={toggleMenu} side="top" className="py-2">
<AccountMenu
onClickOutside={onClickOutside}
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
/>
</Popover>
</>
)
}
export default AccountMenuButton
@@ -12,13 +12,13 @@ import {
STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
} from '@/Constants/Strings'
import { alertDialog, confirmDialog } from '@/Services/AlertService'
import AccountMenu from '@/Components/AccountMenu/AccountMenu'
import Icon from '@/Components/Icon/Icon'
import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
import { Fragment } from 'react'
import { AccountMenuPane } from '../AccountMenu/AccountMenuPane'
import { EditorEventSource } from '@/Types/EditorEventSource'
import QuickSettingsButton from './QuickSettingsButton'
import AccountMenuButton from './AccountMenuButton'
type Props = {
application: WebApplication
@@ -287,12 +287,10 @@ class Footer extends PureComponent<Props, State> {
}
accountMenuClickHandler = () => {
this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu()
this.viewControllerManager.accountMenuController.toggleShow()
}
quickSettingsClickHandler = () => {
this.viewControllerManager.accountMenuController.closeAccountMenu()
this.viewControllerManager.quickSettingsMenuController.toggle()
}
@@ -342,55 +340,31 @@ class Footer extends PureComponent<Props, State> {
override render() {
return (
<div className="sn-component">
<div
<footer
id="footer-bar"
className="z-footer-bar flex h-6 w-full select-none items-center justify-between border-t border-border bg-contrast px-3 text-text"
>
<div className="left flex h-full">
<div className="sk-app-bar-item relative z-footer-bar-item ml-0 select-none">
<div
onClick={this.accountMenuClickHandler}
className={
(this.state.showAccountMenu ? 'bg-border' : '') +
' flex h-full w-8 cursor-pointer items-center justify-center rounded-full'
}
>
<div
className={
this.state.hasError ? 'text-danger' : (this.user ? 'text-info' : 'text-neutral') + ' h-5 w-5'
}
>
<Icon type="account-circle" className="max-h-5 hover:text-info" />
</div>
</div>
{this.state.showAccountMenu && (
<AccountMenu
onClickOutside={this.clickOutsideAccountMenu}
viewControllerManager={this.viewControllerManager}
application={this.application}
mainApplicationGroup={this.props.applicationGroup}
/>
)}
<AccountMenuButton
application={this.application}
hasError={this.state.hasError}
isOpen={this.state.showAccountMenu}
mainApplicationGroup={this.props.applicationGroup}
onClickOutside={this.clickOutsideAccountMenu}
toggleMenu={this.accountMenuClickHandler}
user={this.user}
viewControllerManager={this.viewControllerManager}
/>
</div>
<div className="sk-app-bar-item ml-0-important relative z-footer-bar-item select-none">
<div
onClick={this.quickSettingsClickHandler}
className="flex h-full w-8 cursor-pointer items-center justify-center"
>
<div className="h-5">
<Icon
type="tune"
className={(this.state.showQuickSettingsMenu ? 'text-info' : '') + ' rounded hover:text-info'}
/>
</div>
</div>
{this.state.showQuickSettingsMenu && (
<QuickSettingsMenu
onClickOutside={this.clickOutsideQuickSettingsMenu}
viewControllerManager={this.viewControllerManager}
application={this.application}
/>
)}
<div className="relative z-footer-bar-item select-none">
<QuickSettingsButton
isOpen={this.state.showQuickSettingsMenu}
toggleMenu={this.quickSettingsClickHandler}
application={this.application}
preferencesController={this.viewControllerManager.preferencesController}
quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController}
/>
</div>
{this.state.showBetaWarning && (
<Fragment>
@@ -454,7 +428,7 @@ class Footer extends PureComponent<Props, State> {
</div>
)}
</div>
</div>
</footer>
</div>
)
}
@@ -0,0 +1,55 @@
import { WebApplication } from '@/Application/Application'
import { PreferencesController } from '@/Controllers/PreferencesController'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import { useRef } from 'react'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
type Props = {
isOpen: boolean
toggleMenu: () => void
application: WebApplication
preferencesController: PreferencesController
quickSettingsMenuController: QuickSettingsController
}
const QuickSettingsButton = ({
application,
isOpen,
toggleMenu,
preferencesController,
quickSettingsMenuController,
}: Props) => {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button
onClick={toggleMenu}
className="flex h-full w-8 cursor-pointer items-center justify-center"
ref={buttonRef}
>
<div className="h-5">
<Icon type="tune" className={(isOpen ? 'text-info' : '') + ' rounded hover:text-info'} />
</div>
</button>
<Popover
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
side="top"
align="start"
className="py-2"
>
<QuickSettingsMenu
preferencesController={preferencesController}
quickSettingsMenuController={quickSettingsMenuController}
application={application}
/>
</Popover>
</>
)
}
export default QuickSettingsButton
@@ -1,190 +1,104 @@
import { FunctionComponent, useMemo } from 'react'
import { IconType } from '@standardnotes/snjs'
import {
AccessibilityIcon,
AccountCircleIcon,
AddIcon,
ArchiveIcon,
ArrowLeftIcon,
ArrowsSortDownIcon,
ArrowsSortUpIcon,
AttachmentFileIcon,
AuthenticatorIcon,
CheckBoldIcon,
CheckCircleIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ClearCircleFilledIcon,
CloseIcon,
CloudOffIcon,
CodeIcon,
CopyIcon,
DashboardIcon,
DownloadIcon,
EditorIcon,
EmailIcon,
EyeIcon,
EyeOffIcon,
FileDocIcon,
FileIcon,
FileImageIcon,
FileMovIcon,
FileMusicIcon,
FileOtherIcon,
FilePdfIcon,
FilePptIcon,
FileXlsIcon,
FileZipIcon,
FolderIcon,
HashtagIcon,
HashtagOffIcon,
HelpIcon,
HistoryIcon,
InfoIcon,
KeyboardIcon,
LinkIcon,
LinkOffIcon,
ListBulleted,
ListedIcon,
LockFilledIcon,
LockIcon,
MarkdownIcon,
MenuArrowDownAlt,
MenuArrowDownIcon,
MenuArrowRightIcon,
MenuCloseIcon,
MoreIcon,
NotesFilledIcon,
NotesIcon,
PasswordIcon,
PencilFilledIcon,
PencilIcon,
PencilOffIcon,
PinFilledIcon,
PinIcon,
PlainTextIcon,
PremiumFeatureIcon,
RestoreIcon,
RichTextIcon,
SearchIcon,
SecurityIcon,
ServerIcon,
SettingsIcon,
SignInIcon,
SignOutIcon,
SortDescendingIcon,
SpreadsheetsIcon,
StarIcon,
SubtractIcon,
SyncIcon,
TasksIcon,
ThemesIcon,
TrashFilledIcon,
TrashIcon,
TrashSweepIcon,
TuneIcon,
UnarchiveIcon,
UnpinIcon,
UserAddIcon,
UserIcon,
UserSwitch,
WarningIcon,
WindowIcon,
} from '@standardnotes/icons'
import * as icons from '@standardnotes/icons'
export const ICONS = {
'account-circle': AccountCircleIcon,
'arrow-left': ArrowLeftIcon,
'arrows-sort-down': ArrowsSortDownIcon,
'arrows-sort-up': ArrowsSortUpIcon,
'attachment-file': AttachmentFileIcon,
'check-bold': CheckBoldIcon,
'check-circle': CheckCircleIcon,
'chevron-down': ChevronDownIcon,
'chevron-right': ChevronRightIcon,
'clear-circle-filled': ClearCircleFilledIcon,
'cloud-off': CloudOffIcon,
'eye-off': EyeOffIcon,
'file-doc': FileDocIcon,
'file-image': FileImageIcon,
'file-mov': FileMovIcon,
'file-music': FileMusicIcon,
'file-other': FileOtherIcon,
'file-pdf': FilePdfIcon,
'file-ppt': FilePptIcon,
'file-xls': FileXlsIcon,
'file-zip': FileZipIcon,
'hashtag-off': HashtagOffIcon,
'link-off': LinkOffIcon,
'list-bulleted': ListBulleted,
'lock-filled': LockFilledIcon,
'menu-arrow-down-alt': MenuArrowDownAlt,
'menu-arrow-down': MenuArrowDownIcon,
'menu-arrow-right': MenuArrowRightIcon,
'menu-close': MenuCloseIcon,
'notes-filled': NotesFilledIcon,
'pencil-filled': PencilFilledIcon,
'pencil-off': PencilOffIcon,
'pin-filled': PinFilledIcon,
'plain-text': PlainTextIcon,
'premium-feature': PremiumFeatureIcon,
'rich-text': RichTextIcon,
'sort-descending': SortDescendingIcon,
'trash-filled': TrashFilledIcon,
'trash-sweep': TrashSweepIcon,
'user-add': UserAddIcon,
'user-switch': UserSwitch,
accessibility: AccessibilityIcon,
add: AddIcon,
archive: ArchiveIcon,
authenticator: AuthenticatorIcon,
check: CheckIcon,
close: CloseIcon,
code: CodeIcon,
copy: CopyIcon,
dashboard: DashboardIcon,
download: DownloadIcon,
editor: EditorIcon,
email: EmailIcon,
eye: EyeIcon,
file: FileIcon,
folder: FolderIcon,
hashtag: HashtagIcon,
help: HelpIcon,
history: HistoryIcon,
info: InfoIcon,
keyboard: KeyboardIcon,
link: LinkIcon,
listed: ListedIcon,
lock: LockIcon,
markdown: MarkdownIcon,
more: MoreIcon,
notes: NotesIcon,
password: PasswordIcon,
pencil: PencilIcon,
pin: PinIcon,
restore: RestoreIcon,
search: SearchIcon,
security: SecurityIcon,
server: ServerIcon,
settings: SettingsIcon,
signIn: SignInIcon,
signOut: SignOutIcon,
spreadsheets: SpreadsheetsIcon,
star: StarIcon,
subtract: SubtractIcon,
sync: SyncIcon,
tasks: TasksIcon,
themes: ThemesIcon,
trash: TrashIcon,
tune: TuneIcon,
unarchive: UnarchiveIcon,
unpin: UnpinIcon,
user: UserIcon,
warning: WarningIcon,
window: WindowIcon,
'account-circle': icons.AccountCircleIcon,
'arrow-left': icons.ArrowLeftIcon,
'arrows-sort-down': icons.ArrowsSortDownIcon,
'arrows-sort-up': icons.ArrowsSortUpIcon,
'attachment-file': icons.AttachmentFileIcon,
'check-bold': icons.CheckBoldIcon,
'check-circle': icons.CheckCircleIcon,
'chevron-down': icons.ChevronDownIcon,
'chevron-right': icons.ChevronRightIcon,
'clear-circle-filled': icons.ClearCircleFilledIcon,
'cloud-off': icons.CloudOffIcon,
'diamond-filled': icons.DiamondFilledIcon,
'eye-off': icons.EyeOffIcon,
'file-doc': icons.FileDocIcon,
'file-image': icons.FileImageIcon,
'file-mov': icons.FileMovIcon,
'file-music': icons.FileMusicIcon,
'file-other': icons.FileOtherIcon,
'file-pdf': icons.FilePdfIcon,
'file-ppt': icons.FilePptIcon,
'file-xls': icons.FileXlsIcon,
'file-zip': icons.FileZipIcon,
'hashtag-off': icons.HashtagOffIcon,
'link-off': icons.LinkOffIcon,
'list-bulleted': icons.ListBulleted,
'lock-filled': icons.LockFilledIcon,
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
'menu-arrow-down': icons.MenuArrowDownIcon,
'menu-arrow-right': icons.MenuArrowRightIcon,
'menu-close': icons.MenuCloseIcon,
'notes-filled': icons.NotesFilledIcon,
'pencil-filled': icons.PencilFilledIcon,
'pencil-off': icons.PencilOffIcon,
'pin-filled': icons.PinFilledIcon,
'plain-text': icons.PlainTextIcon,
'premium-feature': icons.PremiumFeatureIcon,
'rich-text': icons.RichTextIcon,
'sort-descending': icons.SortDescendingIcon,
'star-circle-filled': icons.StarCircleFilled,
'star-filled': icons.StarFilledIcon,
'star-variant-filled': icons.StarVariantFilledIcon,
'trash-filled': icons.TrashFilledIcon,
'trash-sweep': icons.TrashSweepIcon,
'user-add': icons.UserAddIcon,
'user-switch': icons.UserSwitch,
accessibility: icons.AccessibilityIcon,
add: icons.AddIcon,
archive: icons.ArchiveIcon,
asterisk: icons.AsteriskIcon,
authenticator: icons.AuthenticatorIcon,
check: icons.CheckIcon,
close: icons.CloseIcon,
code: icons.CodeIcon,
copy: icons.CopyIcon,
dashboard: icons.DashboardIcon,
diamond: icons.DiamondIcon,
download: icons.DownloadIcon,
editor: icons.EditorIcon,
email: icons.EmailIcon,
eye: icons.EyeIcon,
file: icons.FileIcon,
folder: icons.FolderIcon,
hashtag: icons.HashtagIcon,
help: icons.HelpIcon,
history: icons.HistoryIcon,
info: icons.InfoIcon,
keyboard: icons.KeyboardIcon,
link: icons.LinkIcon,
listed: icons.ListedIcon,
lock: icons.LockIcon,
markdown: icons.MarkdownIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
password: icons.PasswordIcon,
pencil: icons.PencilIcon,
pin: icons.PinIcon,
restore: icons.RestoreIcon,
search: icons.SearchIcon,
security: icons.SecurityIcon,
server: icons.ServerIcon,
settings: icons.SettingsIcon,
signIn: icons.SignInIcon,
signOut: icons.SignOutIcon,
spreadsheets: icons.SpreadsheetsIcon,
star: icons.StarIcon,
subtract: icons.SubtractIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
unarchive: icons.UnarchiveIcon,
unpin: icons.UnpinIcon,
user: icons.UserIcon,
warning: icons.WarningIcon,
window: icons.WindowIcon,
}
type Props = {
@@ -0,0 +1,4 @@
import { IconType } from '@standardnotes/snjs'
export const PremiumFeatureIconName: IconType = 'asterisk'
export const PremiumFeatureIconClass = 'text-info'
@@ -1,13 +1,12 @@
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { observer } from 'mobx-react-lite'
import NotesOptions from '@/Components/NotesOptions/NotesOptions'
import { useCallback, useEffect, useRef } from 'react'
import { useRef } from 'react'
import { WebApplication } from '@/Application/Application'
import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
@@ -24,43 +23,33 @@ const NotesContextMenu = ({
noteTagsController,
historyModalController,
}: Props) => {
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = notesController
const { contextMenuOpen, contextMenuClickLocation } = notesController
const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => notesController.setContextMenuOpen(open))
useCloseOnClickOutside(contextMenuRef, () => notesController.setContextMenuOpen(false))
const reloadContextMenuLayout = useCallback(() => {
notesController.reloadContextMenuLayout()
}, [notesController])
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
return contextMenuOpen ? (
<div
ref={contextMenuRef}
className="max-h-120 fixed z-dropdown-menu flex min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
style={{
...contextMenuPosition,
maxHeight: contextMenuMaxHeight,
return (
<Popover
align="start"
anchorPoint={{
x: contextMenuClickLocation.x,
y: contextMenuClickLocation.y,
}}
className="py-2"
open={contextMenuOpen}
side="right"
togglePopover={() => notesController.setContextMenuOpen(!contextMenuOpen)}
>
<NotesOptions
application={application}
closeOnBlur={closeOnBlur}
navigationController={navigationController}
notesController={notesController}
noteTagsController={noteTagsController}
historyModalController={historyModalController}
/>
</div>
) : null
<div ref={contextMenuRef}>
<NotesOptions
application={application}
navigationController={navigationController}
notesController={notesController}
noteTagsController={noteTagsController}
historyModalController={historyModalController}
/>
</div>
</Popover>
)
}
export default observer(NotesContextMenu)
@@ -1,12 +1,11 @@
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { KeyboardKey } from '@/Services/IOService'
import Popover from '../Popover/Popover'
type Props = {
navigationController: NavigationController
@@ -16,101 +15,59 @@ type Props = {
const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesController, noteTagsController }) => {
const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [isOpen, setIsOpen] = useState(false)
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const toggleTagsMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
}, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return (
<div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsMenuOpen(false)
}
}}
onBlur={closeOnBlur}
ref={menuButtonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="hashtag" className="mr-2 text-neutral" />
Add tag
</div>
<Icon type="chevron-right" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsMenuOpen(false)
menuButtonRef.current?.focus()
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className={`${
isMenuOpen ? 'flex' : 'hidden'
} max-h-120 fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main`}
>
{navigationController.tags.map((tag) => (
<button
key={tag.uuid}
className="max-w-80 flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onBlur={closeOnBlur}
onClick={() => {
notesController.isTagInSelectedNotes(tag)
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
: notesController.addTagToSelectedNotes(tag).catch(console.error)
}}
>
<span
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
<button
onClick={toggleMenu}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
}
}}
ref={buttonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="hashtag" className="mr-2 text-neutral" />
Add tag
</div>
<Icon type="chevron-right" className="text-neutral" />
</button>
<Popover
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
side="right"
align="start"
className="py-2"
>
{navigationController.tags.map((tag) => (
<button
key={tag.uuid}
className="max-w-80 flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
notesController.isTagInSelectedNotes(tag)
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
: notesController.addTagToSelectedNotes(tag).catch(console.error)
}}
>
<span
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`}
>
{noteTagsController.getLongTitle(tag)}
</span>
</button>
))}
</DisclosurePanel>
</Disclosure>
>
{noteTagsController.getLongTitle(tag)}
</span>
</button>
))}
</Popover>
</div>
)
}
@@ -1,12 +1,10 @@
import { KeyboardKey } from '@/Services/IOService'
import { WebApplication } from '@/Application/Application'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import Popover from '../Popover/Popover'
type ChangeEditorOptionProps = {
application: WebApplication
@@ -15,91 +13,48 @@ type ChangeEditorOptionProps = {
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => {
setIsOpen(open)
setIsVisible(open)
})
const toggleChangeEditorMenu = useCallback(() => {
if (!isOpen) {
const menuStyle = calculateSubmenuStyle(buttonRef.current)
if (menuStyle) {
setMenuStyle(menuStyle)
}
}
setIsOpen(!isOpen)
}, [isOpen])
useEffect(() => {
if (isOpen) {
setTimeout(() => {
const newMenuStyle = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuStyle) {
setMenuStyle(newMenuStyle)
setIsVisible(true)
}
}, 5)
}
}, [isOpen])
const toggleMenu = useCallback(async () => {
setIsOpen((isOpen) => !isOpen)
}, [])
return (
<div ref={menuContainerRef}>
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
}
<button
onClick={toggleMenu}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
}
}}
ref={buttonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="dashboard" className="mr-2 text-neutral" />
Change note type
</div>
<Icon type="chevron-right" className="text-neutral" />
</button>
<Popover
align="start"
anchorElement={buttonRef.current}
className="pt-2 md:pt-0"
open={isOpen}
side="right"
togglePopover={toggleMenu}
>
<ChangeEditorMenu
application={application}
note={note}
isVisible={isOpen}
closeMenu={() => {
setIsOpen(false)
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="dashboard" className="mr-2 text-neutral" />
Change note type
</div>
<Icon type="chevron-right" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
buttonRef.current?.focus()
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className="max-h-120 fixed flex min-w-68 flex-col overflow-y-auto rounded bg-default shadow-main"
>
{isOpen && (
<ChangeEditorMenu
application={application}
closeOnBlur={closeOnBlur}
note={note}
isVisible={isVisible}
closeMenu={() => {
setIsOpen(false)
}}
/>
)}
</DisclosurePanel>
</Disclosure>
/>
</Popover>
</div>
)
}
@@ -9,10 +9,9 @@ import Spinner from '@/Components/Spinner/Spinner'
type ListedActionsMenuProps = {
application: WebApplication
note: SNNote
recalculateMenuStyle: () => void
}
const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedActionsMenuProps) => {
const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
@@ -88,14 +87,11 @@ const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedAc
console.error(err)
} finally {
setIsFetchingAccounts(false)
setTimeout(() => {
recalculateMenuStyle()
})
}
}
void fetchListedAccounts()
}, [application, note.uuid, recalculateMenuStyle])
}, [application, note.uuid])
return (
<>
@@ -1,11 +1,10 @@
import { WebApplication } from '@/Application/Application'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import ListedActionsMenu from './ListedActionsMenu'
import { KeyboardKey } from '@/Services/IOService'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
@@ -14,74 +13,42 @@ type Props = {
const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => {
const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [isOpen, setIsOpen] = useState(false)
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const toggleListedMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
}, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return (
<div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
<DisclosureButton
ref={menuButtonRef}
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="listed" className="mr-2 text-neutral" />
Listed actions
</div>
<Icon type="chevron-right" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
style={{
...menuStyle,
position: 'fixed',
}}
className={`${
isMenuOpen ? 'flex' : 'hidden'
} max-h-120 fixed min-w-68 flex-col overflow-y-auto rounded bg-default pb-1 shadow-main`}
>
{isMenuOpen && (
<ListedActionsMenu application={application} note={note} recalculateMenuStyle={recalculateMenuStyle} />
)}
</DisclosurePanel>
</Disclosure>
<button
onClick={toggleMenu}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
}
}}
ref={buttonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="listed" className="mr-2 text-neutral" />
Listed actions
</div>
<Icon type="chevron-right" className="text-neutral" />
</button>
<Popover
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
side="right"
align="end"
className="pt-2 md:pt-0"
>
<ListedActionsMenu application={application} note={note} />
</Popover>
</div>
)
}
@@ -15,13 +15,11 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
type DeletePermanentlyButtonProps = {
closeOnBlur: NotesOptionsProps['closeOnBlur']
onClick: () => void
}
const DeletePermanentlyButton = ({ closeOnBlur, onClick }: DeletePermanentlyButtonProps) => (
const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onClick}
>
@@ -177,7 +175,6 @@ const NotesOptions = ({
notesController,
noteTagsController,
historyModalController,
closeOnBlur,
}: NotesOptionsProps) => {
const [altKeyDown, setAltKeyDown] = useState(false)
@@ -270,7 +267,6 @@ const NotesOptions = ({
{notes.length === 1 && (
<>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openRevisionHistoryModal}
>
@@ -285,7 +281,6 @@ const NotesOptions = ({
onClick={() => {
notesController.setLockSelectedNotes(!locked)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="pencil-off" className={iconClass} />
@@ -298,7 +293,6 @@ const NotesOptions = ({
onClick={() => {
notesController.setHideSelectedNotePreviews(!hidePreviews)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="rich-text" className={iconClass} />
@@ -311,7 +305,6 @@ const NotesOptions = ({
onClick={() => {
notesController.setProtectSelectedNotes(!protect).catch(console.error)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className={iconClass} />
@@ -335,7 +328,6 @@ const NotesOptions = ({
)}
{unpinned && (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
notesController.setPinSelectedNotes(true)
@@ -347,7 +339,6 @@ const NotesOptions = ({
)}
{pinned && (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
notesController.setPinSelectedNotes(false)
@@ -358,7 +349,6 @@ const NotesOptions = ({
</button>
)}
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={downloadSelectedItems}
>
@@ -366,7 +356,6 @@ const NotesOptions = ({
Export
</button>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={duplicateSelectedItems}
>
@@ -375,7 +364,6 @@ const NotesOptions = ({
</button>
{unarchived && (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
notesController.setArchiveSelectedNotes(true).catch(console.error)
@@ -387,7 +375,6 @@ const NotesOptions = ({
)}
{archived && (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
notesController.setArchiveSelectedNotes(false).catch(console.error)
@@ -400,14 +387,12 @@ const NotesOptions = ({
{notTrashed &&
(altKeyDown ? (
<DeletePermanentlyButton
closeOnBlur={closeOnBlur}
onClick={async () => {
await notesController.deleteNotesPermanently()
}}
/>
) : (
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => {
await notesController.setTrashSelectedNotes(true)
@@ -420,7 +405,6 @@ const NotesOptions = ({
{trashed && (
<>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => {
await notesController.setTrashSelectedNotes(false)
@@ -430,13 +414,11 @@ const NotesOptions = ({
<span className="text-success">Restore</span>
</button>
<DeletePermanentlyButton
closeOnBlur={closeOnBlur}
onClick={async () => {
await notesController.deleteNotesPermanently()
}}
/>
<button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => {
await notesController.emptyTrash()
@@ -1,16 +1,13 @@
import Icon from '@/Components/Icon/Icon'
import VisuallyHidden from '@reach/visually-hidden'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useRef, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { observer } from 'mobx-react-lite'
import NotesOptions from './NotesOptions'
import { WebApplication } from '@/Application/Application'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import Popover from '../Popover/Popover'
type Props = {
application: WebApplication
@@ -29,83 +26,38 @@ const NotesOptionsPanel = ({
historyModalController,
onClickPreprocessing,
}: Props) => {
const [open, setOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen
if (willMenuOpen && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
return (
<Disclosure
open={open}
onChange={async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !open
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setOpen(newOpenState)
}
}}
>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
<>
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="Note options menu"
aria-label="Note options menu"
onClick={toggleMenu}
ref={buttonRef}
>
<VisuallyHidden>Actions</VisuallyHidden>
<Icon type="more" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className={`${
open ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
onBlur={closeOnBlur}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{open && (
<NotesOptions
application={application}
navigationController={navigationController}
notesController={notesController}
noteTagsController={noteTagsController}
historyModalController={historyModalController}
closeOnBlur={closeOnBlur}
/>
)}
</DisclosurePanel>
</Disclosure>
<Icon type="more" />
</button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<NotesOptions
application={application}
navigationController={navigationController}
notesController={notesController}
noteTagsController={noteTagsController}
historyModalController={historyModalController}
/>
</Popover>
</>
)
}
@@ -10,5 +10,4 @@ export type NotesOptionsProps = {
notesController: NotesController
noteTagsController: NoteTagsController
historyModalController: HistoryModalController
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
}
@@ -25,7 +25,7 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
}, [callback, dismiss])
return (
<ModalDialog className="!w-[350px]">
<ModalDialog className="w-full md:!w-[350px]">
<ModalDialogLabel closeDialog={deny}>Activate Component</ModalDialogLabel>
<ModalDialogDescription>
<div className="text-base">
@@ -33,7 +33,7 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
{' would like to interact with your '}
{permissionsString}
</div>
<div className="sk-panel-row">
<div className="sk-panel-row [word-break:break-word]">
<p className="sk-p">
Components use an offline messaging system to communicate. Learn more at{' '}
<a href="https://standardnotes.com/permissions" rel="noopener" target="_blank" className="sk-a info">
@@ -0,0 +1,54 @@
import { CSSProperties } from 'react'
import { PopoverAlignment, PopoverSide } from './Types'
import { OppositeSide, checkCollisions, getNonCollidingSide, getNonCollidingAlignment } from './Utils/Collisions'
import { getPositionedPopoverRect } from './Utils/Rect'
const getStylesFromRect = (rect: DOMRect): CSSProperties => {
return {
willChange: 'transform',
transform: `translate(${rect.x}px, ${rect.y}px)`,
}
}
type Options = {
align: PopoverAlignment
anchorRect?: DOMRect
documentRect: DOMRect
popoverRect?: DOMRect
side: PopoverSide
}
export const getPositionedPopoverStyles = ({
align,
anchorRect,
documentRect,
popoverRect,
side,
}: Options): [CSSProperties | null, PopoverSide, PopoverAlignment] => {
if (!popoverRect || !anchorRect) {
return [null, side, align]
}
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return [null, side, align]
}
const rectForPreferredSide = getPositionedPopoverRect(popoverRect, anchorRect, side, align)
const preferredSideRectCollisions = checkCollisions(rectForPreferredSide, documentRect)
const oppositeSide = OppositeSide[side]
const rectForOppositeSide = getPositionedPopoverRect(popoverRect, anchorRect, oppositeSide, align)
const oppositeSideRectCollisions = checkCollisions(rectForOppositeSide, documentRect)
const finalSide = getNonCollidingSide(side, preferredSideRectCollisions, oppositeSideRectCollisions)
const finalAlignment = getNonCollidingAlignment(finalSide, align, preferredSideRectCollisions, {
popoverRect,
buttonRect: anchorRect,
documentRect,
})
const finalPositionedRect = getPositionedPopoverRect(popoverRect, anchorRect, finalSide, finalAlignment)
return [getStylesFromRect(finalPositionedRect), finalSide, finalAlignment]
}
@@ -0,0 +1,36 @@
import PositionedPopoverContent from './PositionedPopoverContent'
import { PopoverProps } from './Types'
type Props = PopoverProps & {
open: boolean
}
const Popover = ({
align,
anchorElement,
anchorPoint,
children,
className,
open,
overrideZIndex,
side,
togglePopover,
}: Props) => {
return open ? (
<>
<PositionedPopoverContent
align={align}
anchorElement={anchorElement}
anchorPoint={anchorPoint}
className={className}
overrideZIndex={overrideZIndex}
side={side}
togglePopover={togglePopover}
>
{children}
</PositionedPopoverContent>
</>
) : null
}
export default Popover
@@ -0,0 +1,80 @@
import { useDocumentRect } from '@/Hooks/useDocumentRect'
import { useAutoElementRect } from '@/Hooks/useElementRect'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useState } from 'react'
import Icon from '../Icon/Icon'
import Portal from '../Portal/Portal'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
import { PopoverContentProps } from './Types'
import { getPopoverMaxHeight, getAppRect } from './Utils/Rect'
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
const PositionedPopoverContent = ({
align = 'end',
anchorElement,
anchorPoint,
children,
className,
overrideZIndex,
side = 'bottom',
togglePopover,
}: PopoverContentProps) => {
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const popoverRect = useAutoElementRect(popoverElement)
const anchorElementRect = useAutoElementRect(anchorElement, {
updateOnWindowResize: true,
})
const anchorPointRect = DOMRect.fromRect({
x: anchorPoint?.x,
y: anchorPoint?.y,
})
const anchorRect = anchorPoint ? anchorPointRect : anchorElementRect
const documentRect = useDocumentRect()
const [styles, positionedSide, positionedAlignment] = getPositionedPopoverStyles({
align,
anchorRect,
documentRect,
popoverRect: popoverRect ?? popoverElement?.getBoundingClientRect(),
side,
})
usePopoverCloseOnClickOutside({
popoverElement,
anchorElement,
togglePopover,
})
return (
<Portal>
<div
className={classNames(
'absolute top-0 left-0 flex h-full w-full min-w-80 cursor-auto flex-col overflow-y-auto rounded bg-default shadow-main md:h-auto md:max-w-xs',
overrideZIndex ? overrideZIndex : 'z-dropdown-menu',
className,
)}
style={{
...styles,
maxHeight: getPopoverMaxHeight(getAppRect(documentRect), anchorRect, positionedSide, positionedAlignment),
}}
ref={(node) => {
setPopoverElement(node)
}}
data-popover
>
<div className="md:hidden">
<div className="flex items-center justify-end px-3">
<button className="rounded-full border border-border p-1" onClick={togglePopover}>
<Icon type="close" className="h-4 w-4" />
</button>
</div>
<HorizontalSeparator classes="my-2" />
</div>
{children}
</div>
</Portal>
)
}
export default PositionedPopoverContent
@@ -0,0 +1,49 @@
import { ReactNode } from 'react'
export type PopoverState = 'closed' | 'positioning' | 'open'
export type PopoverElement = HTMLDivElement | HTMLMenuElement
export type PopoverSide = 'top' | 'left' | 'bottom' | 'right'
export type PopoverAlignment = 'start' | 'center' | 'end'
export type PopoverOptions = {
side: PopoverSide
align: PopoverAlignment
}
export type RectCollisions = Record<PopoverSide, boolean>
type Point = {
x: number
y: number
}
type PopoverAnchorElementProps = {
anchorElement: HTMLElement | null
anchorPoint?: never
}
type PopoverAnchorPointProps = {
anchorPoint: Point
anchorElement?: never
}
type CommonPopoverProps = {
align?: PopoverAlignment
children: ReactNode
side?: PopoverSide
overrideZIndex?: string
togglePopover: () => void
className?: string
}
export type PopoverContentProps = CommonPopoverProps & {
anchorElement?: HTMLElement | null
anchorPoint?: Point
}
export type PopoverProps =
| (CommonPopoverProps & PopoverAnchorElementProps)
| (CommonPopoverProps & PopoverAnchorPointProps)
@@ -0,0 +1,86 @@
import { PopoverSide, PopoverAlignment, RectCollisions } from '../Types'
import { getAppRect, getPositionedPopoverRect } from './Rect'
export const OppositeSide: Record<PopoverSide, PopoverSide> = {
top: 'bottom',
bottom: 'top',
left: 'right',
right: 'left',
}
export const checkCollisions = (popoverRect: DOMRect, containerRect: DOMRect): RectCollisions => {
const appRect = getAppRect(containerRect)
return {
top: popoverRect.top < appRect.top,
left: popoverRect.left < appRect.left,
bottom: popoverRect.bottom > appRect.bottom,
right: popoverRect.right > appRect.right,
}
}
export const getNonCollidingSide = (
preferredSide: PopoverSide,
preferredSideCollisions: RectCollisions,
oppositeSideCollisions: RectCollisions,
): PopoverSide => {
const oppositeSide = OppositeSide[preferredSide]
return preferredSideCollisions[preferredSide] && !oppositeSideCollisions[oppositeSide] ? oppositeSide : preferredSide
}
const OppositeAlignment: Record<Exclude<PopoverAlignment, 'center'>, PopoverAlignment> = {
start: 'end',
end: 'start',
}
export const getNonCollidingAlignment = (
finalSide: PopoverSide,
preferredAlignment: PopoverAlignment,
collisions: RectCollisions,
{
popoverRect,
buttonRect,
documentRect,
}: {
popoverRect: DOMRect
buttonRect: DOMRect
documentRect: DOMRect
},
): PopoverAlignment => {
const isHorizontalSide = finalSide === 'top' || finalSide === 'bottom'
const boundToCheckForStart = isHorizontalSide ? 'right' : 'bottom'
const boundToCheckForEnd = isHorizontalSide ? 'left' : 'top'
const prefersAligningAtStart = preferredAlignment === 'start'
const prefersAligningAtCenter = preferredAlignment === 'center'
const prefersAligningAtEnd = preferredAlignment === 'end'
if (prefersAligningAtCenter) {
if (collisions[boundToCheckForStart]) {
return 'end'
}
if (collisions[boundToCheckForEnd]) {
return 'start'
}
} else {
const oppositeAlignmentCollisions = checkCollisions(
getPositionedPopoverRect(popoverRect, buttonRect, finalSide, OppositeAlignment[preferredAlignment]),
documentRect,
)
if (
prefersAligningAtStart &&
collisions[boundToCheckForStart] &&
!oppositeAlignmentCollisions[boundToCheckForEnd]
) {
return 'end'
}
if (prefersAligningAtEnd && collisions[boundToCheckForEnd] && !oppositeAlignmentCollisions[boundToCheckForStart]) {
return 'start'
}
}
return preferredAlignment
}
@@ -0,0 +1,120 @@
import { PopoverSide, PopoverAlignment } from '../Types'
export const getPopoverMaxHeight = (
appRect: DOMRect,
buttonRect: DOMRect | undefined,
side: PopoverSide,
alignment: PopoverAlignment,
): number | 'none' => {
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return 'none'
}
const MarginFromAppBorderInPX = 10
let constraint = 0
if (buttonRect) {
switch (side) {
case 'top':
constraint = appRect.height - buttonRect.top
break
case 'bottom':
constraint = buttonRect.bottom
break
case 'left':
case 'right':
switch (alignment) {
case 'start':
constraint = buttonRect.top
break
case 'end':
constraint = appRect.height - buttonRect.bottom
break
}
break
}
}
return appRect.height - constraint - MarginFromAppBorderInPX
}
export const getMaxHeightAdjustedRect = (rect: DOMRect, maxHeight: number) => {
return DOMRect.fromRect({
width: rect.width,
height: rect.height < maxHeight ? rect.height : maxHeight,
x: rect.x,
y: rect.y,
})
}
export const getAppRect = (updatedDocumentRect?: DOMRect) => {
const footerRect = document.querySelector('footer')?.getBoundingClientRect()
const documentRect = updatedDocumentRect ? updatedDocumentRect : document.documentElement.getBoundingClientRect()
const appRect = footerRect
? DOMRect.fromRect({
width: documentRect.width,
height: documentRect.height - footerRect.height,
})
: documentRect
return appRect
}
export const getPositionedPopoverRect = (
popoverRect: DOMRect,
buttonRect: DOMRect,
side: PopoverSide,
align: PopoverAlignment,
): DOMRect => {
const { width, height } = popoverRect
const positionPopoverRect = DOMRect.fromRect(popoverRect)
switch (side) {
case 'top': {
positionPopoverRect.y = buttonRect.top - height
break
}
case 'bottom':
positionPopoverRect.y = buttonRect.bottom
break
case 'left':
positionPopoverRect.x = buttonRect.left - width
break
case 'right':
positionPopoverRect.x = buttonRect.right
break
}
if (side === 'top' || side === 'bottom') {
switch (align) {
case 'start':
positionPopoverRect.x = buttonRect.left
break
case 'center':
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2
break
case 'end':
positionPopoverRect.x = buttonRect.right - width
break
}
} else {
switch (align) {
case 'start':
positionPopoverRect.y = buttonRect.top
break
case 'center':
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2
break
case 'end':
positionPopoverRect.y = buttonRect.bottom - height
break
}
}
return positionPopoverRect
}
@@ -0,0 +1,36 @@
import { useEffect } from 'react'
type Options = {
popoverElement: HTMLElement | null
anchorElement: HTMLElement | null | undefined
togglePopover: () => void
}
export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, togglePopover }: Options) => {
useEffect(() => {
const closeIfClickedOutside = (event: MouseEvent) => {
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return
}
const target = event.target as Element
const isDescendantOfMenu = popoverElement?.contains(target)
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
const isDescendantOfPopover = target.closest('[data-popover]')
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfPopover) {
togglePopover()
}
}
document.addEventListener('click', closeIfClickedOutside, { capture: true })
return () => {
document.removeEventListener('click', closeIfClickedOutside, {
capture: true,
})
}
}, [anchorElement, popoverElement, togglePopover])
}
@@ -0,0 +1,24 @@
import { ReactNode, useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
type Props = {
children: ReactNode
}
const randomPortalId = () => Math.random()
const Portal = ({ children }: Props) => {
const [container, setContainer] = useState<HTMLElement>()
useEffect(() => {
const container = document.createElement('div')
container.id = `react-portal-${randomPortalId()}`
document.body.append(container)
setContainer(container)
return () => container.remove()
}, [])
return container ? createPortal(children, container) : null
}
export default Portal
@@ -12,6 +12,7 @@ import { sortThemes } from '@/Utils/SortThemes'
import PreferencesPane from '../PreferencesComponents/PreferencesPane'
import PreferencesGroup from '../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
type Props = {
application: WebApplication
@@ -57,7 +58,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
themesAsItems.push({
label: theme.name as string,
value: theme.identifier,
icon: 'premium-feature',
icon: PremiumFeatureIconName,
})
}
})
@@ -86,7 +87,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
}
const changeAutoLightTheme = (value: string, item: DropdownItem) => {
if (item.icon === 'premium-feature') {
if (item.icon === PremiumFeatureIconName) {
premiumModal.activate(`${item.label} theme`)
} else {
application.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier).catch(console.error)
@@ -95,7 +96,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
}
const changeAutoDarkTheme = (value: string, item: DropdownItem) => {
if (item.icon === 'premium-feature') {
if (item.icon === PremiumFeatureIconName) {
premiumModal.activate(`${item.label} theme`)
} else {
application.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier).catch(console.error)
@@ -5,7 +5,7 @@ type Props = {
}
const Bullet: FunctionComponent<Props> = ({ className = '' }) => (
<div className={`min-h-1 min-w-1 rounded-full bg-text ${className} mr-2`} />
<div className={`inline-block min-h-1 min-w-1 rounded-full bg-text ${className} mr-2`} />
)
export default Bullet
@@ -39,9 +39,8 @@ const SaveSecretKey: FunctionComponent<Props> = ({ activation: act }) => {
</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-row items-center">
<div className="flex flex-grow flex-col">
<div className="flex flex-row items-center">
<div className="flex flex-row flex-wrap items-center gap-1">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
<b>Save your secret key</b>{' '}
<a
@@ -52,11 +51,11 @@ const SaveSecretKey: FunctionComponent<Props> = ({ activation: act }) => {
</a>
:
</div>
<div className="min-w-2" />
<DecoratedInput
disabled={true}
right={[<CopyButton copyValue={act.secretKey} />, download]}
value={act.secretKey}
className={{ container: 'ml-2' }}
/>
</div>
<div className="h-2" />
@@ -20,11 +20,10 @@ const ScanQRCode: FunctionComponent<Props> = ({ activation: act }) => {
return (
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 1 of 3 - Scan QR code</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-row items-center">
<ModalDialogDescription className="h-33 flex flex-col items-center gap-5 md:flex-row">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode className="border-neutral-contrast-bg border-2 border-solid" value={act.qrCode} size={100} />
</div>
<div className="min-w-5" />
<div className="flex flex-grow flex-col gap-2">
<div className="flex flex-row items-center">
<Bullet />
@@ -20,24 +20,30 @@ const Verification: FunctionComponent<Props> = ({ activation: act }) => {
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 3 of 3 - Verification</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-row items-center">
<div className="flex flex-grow flex-col">
<div className="mb-4 flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="flex flex-grow flex-col gap-4">
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
Enter your <b>secret key</b>:
<Bullet className="align-middle" />
<span className="align-middle">
Enter your <b>secret key</b>:
</span>
</div>
<div className="min-w-2" />
<DecoratedInput className={{ container: `w-92 ${secretKeyClass}` }} onChange={act.setInputSecretKey} />
<DecoratedInput
className={{ container: `ml-2 w-full md:w-96 ${secretKeyClass}` }}
onChange={act.setInputSecretKey}
/>
</div>
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
Verify the <b>authentication code</b> generated by your authenticator app:
<Bullet className="align-middle" />
<span className="align-middle">
Verify the <b>authentication code</b> generated by your authenticator app:
</span>
</div>
<div className="min-w-2" />
<DecoratedInput className={{ container: `w-30 ${authTokenClass}` }} onChange={act.setInputOtpToken} />
<DecoratedInput
className={{ container: `ml-2 w-full md:w-30 ${authTokenClass}` }}
onChange={act.setInputOtpToken}
/>
</div>
</div>
</ModalDialogDescription>
@@ -1,14 +1,16 @@
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
import { FunctionComponent, useCallback, useRef } from 'react'
import Icon from '@/Components/Icon/Icon'
import { PremiumIllustration } from '@standardnotes/icons'
import { WebApplication } from '@/Application/Application'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { loadPurchaseFlowUrl } from '../PurchaseFlow/PurchaseFlowFunctions'
type Props = {
application: WebApplication
featureName: string
hasSubscription: boolean
hasAccount: boolean
onClose: () => void
showModal: boolean
}
@@ -17,6 +19,7 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
application,
featureName,
hasSubscription,
hasAccount,
onClose,
showModal,
}) => {
@@ -25,10 +28,12 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
const handleClick = useCallback(() => {
if (hasSubscription) {
openSubscriptionDashboard(application)
} else if (hasAccount) {
void loadPurchaseFlowUrl(application)
} else if (window.plansUrl) {
window.location.assign(window.plansUrl)
}
}, [application, hasSubscription])
}, [application, hasSubscription, hasAccount])
return showModal ? (
<AlertDialog leastDestructiveRef={plansButtonRef} className="p-0">
@@ -44,14 +49,17 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
<Icon className="text-neutral" type="close" />
</button>
</div>
<div className="flex items-center justify-center p-1" aria-hidden={true}>
<PremiumIllustration className="mb-2" />
<div
className="mx-auto mb-5 flex h-24 w-24 items-center justify-center rounded-[50%] bg-contrast"
aria-hidden={true}
>
<Icon className={`h-12 w-12 ${PremiumFeatureIconClass}`} type={PremiumFeatureIconName} />
</div>
<div className="mb-1 text-center text-lg font-bold">Enable Advanced Features</div>
</AlertDialogLabel>
<AlertDialogDescription className="mb-2 px-4.5 text-center text-sm text-passive-1">
In order to use <span className="font-semibold">{featureName}</span> and other advanced features, please
purchase a subscription or upgrade your current plan.
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features,
upgrade your current plan.
</AlertDialogDescription>
<div className="p-4">
<button
@@ -59,7 +67,7 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
{hasSubscription ? 'Upgrade Plan' : 'See Plans'}
Upgrade
</button>
</div>
</div>
@@ -4,6 +4,7 @@ import { FunctionComponent, MouseEventHandler, useCallback } from 'react'
import Icon from '@/Components/Icon/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import Switch from '@/Components/Switch/Switch'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
type Props = {
application: WebApplication
@@ -44,7 +45,7 @@ const FocusModeSwitch: FunctionComponent<Props> = ({ application, onToggle, onCl
<Switch className="px-0" checked={isEnabled} />
) : (
<div title="Premium feature">
<Icon type="premium-feature" />
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />
</div>
)}
</button>
@@ -1,27 +1,26 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers'
import { quickSettingsKeyDownHandler } from './EventHandlers'
import FocusModeSwitch from './FocusModeSwitch'
import ThemesMenuButton from './ThemesMenuButton'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { ThemeItem } from './ThemeItem'
import { sortThemes } from '@/Utils/SortThemes'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import Popover from '../Popover/Popover'
import { PreferencesController } from '@/Controllers/PreferencesController'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
const focusModeAnimationDuration = 1255
type MenuProps = {
viewControllerManager: ViewControllerManager
preferencesController: PreferencesController
quickSettingsMenuController: QuickSettingsController
application: WebApplication
onClickOutside: () => void
}
const toggleFocusMode = (enabled: boolean) => {
@@ -38,25 +37,23 @@ const toggleFocusMode = (enabled: boolean) => {
}
}
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewControllerManager, onClickOutside }) => {
const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled } =
viewControllerManager.quickSettingsMenuController
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({
application,
preferencesController,
quickSettingsMenuController,
}) => {
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
const [themesMenuOpen, setThemesMenuOpen] = useState(false)
const [themesMenuPosition, setThemesMenuPosition] = useState({})
const [defaultThemeOn, setDefaultThemeOn] = useState(false)
const themesMenuRef = useRef<HTMLDivElement>(null)
const themesButtonRef = useRef<HTMLButtonElement>(null)
const prefsButtonRef = useRef<HTMLButtonElement>(null)
const quickSettingsMenuRef = useRef<HTMLDivElement>(null)
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
const mainRef = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(mainRef, () => {
onClickOutside()
})
useEffect(() => {
toggleFocusMode(focusModeEnabled)
@@ -139,25 +136,14 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
prefsButtonRef.current?.focus()
}, [])
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleThemesMenu = useCallback(() => {
if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
setThemesMenuPosition({
left: themesButtonRect.right,
bottom: document.documentElement.clientHeight - themesButtonRect.bottom,
})
setThemesMenuOpen(true)
} else {
setThemesMenuOpen(false)
}
}, [themesMenuOpen])
setThemesMenuOpen((isOpen) => !isOpen)
}, [])
const openPreferences = useCallback(() => {
closeQuickSettingsMenu()
viewControllerManager.preferencesController.openPreferences()
}, [viewControllerManager, closeQuickSettingsMenu])
preferencesController.openPreferences()
}, [closeQuickSettingsMenu, preferencesController])
const toggleComponent = useCallback(
(component: SNComponent) => {
@@ -193,10 +179,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
[closeQuickSettingsMenu, themesMenuOpen],
)
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback((event) => {
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
}, [])
const toggleDefaultTheme = useCallback(() => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) {
@@ -205,90 +187,71 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
}, [application, themes])
return (
<div ref={mainRef} className="sn-component">
<div
className={`max-h-120 absolute bottom-full left-0 z-footer-bar-item-panel flex min-w-80 max-w-xs cursor-auto flex-col overflow-y-auto rounded bg-default py-2 shadow-main ${
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150'
}`}
ref={quickSettingsMenuRef}
onKeyDown={handleQuickSettingsKeyDown}
<div ref={mainRef} onKeyDown={handleQuickSettingsKeyDown}>
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
<button
onClick={toggleThemesMenu}
onKeyDown={handleBtnKeyDown}
ref={themesButtonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
<DisclosureButton
onKeyDown={handleBtnKeyDown}
onBlur={closeOnBlur}
ref={themesButtonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="themes" className="mr-2 text-neutral" />
Themes
</div>
<Icon type="chevron-right" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
onBlur={closeOnBlur}
ref={themesMenuRef}
onKeyDown={handlePanelKeyDown}
style={{
...themesMenuPosition,
}}
className={`${
themesMenuOpen ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
>
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={toggleDefaultTheme}
onBlur={closeOnBlur}
ref={defaultThemeButtonRef}
>
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
Default
</button>
{themes.map((theme) => (
<ThemesMenuButton
item={theme}
application={application}
key={theme.component?.uuid ?? theme.identifier}
onBlur={closeOnBlur}
/>
))}
</DisclosurePanel>
</Disclosure>
{toggleableComponents.map((component) => (
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
key={component.uuid}
>
<div className="flex items-center">
<Icon type="window" className="mr-2 text-neutral" />
{component.displayName}
</div>
<Switch checked={component.active} className="px-0" />
</button>
))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<HorizontalSeparator classes="my-2" />
<div className="flex items-center">
<Icon type="themes" className="mr-2 text-neutral" />
Themes
</div>
<Icon type="chevron-right" className="text-neutral" />
</button>
<Popover
togglePopover={toggleThemesMenu}
anchorElement={themesButtonRef.current}
open={themesMenuOpen}
side="right"
align="end"
className="py-2"
>
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
onClick={toggleDefaultTheme}
ref={defaultThemeButtonRef}
>
<Icon type="more" className="mr-2 text-neutral" />
Open Preferences
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
Default
</button>
</div>
{themes.map((theme) => (
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
))}
</Popover>
{toggleableComponents.map((component) => (
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
key={component.uuid}
>
<div className="flex items-center">
<Icon type="window" className="mr-2 text-neutral" />
{component.displayName}
</div>
<Switch checked={component.active} className="px-0" />
</button>
))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<HorizontalSeparator classes="my-2" />
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
>
<Icon type="more" className="mr-2 text-neutral" />
Open Preferences
</button>
</div>
)
}
@@ -6,14 +6,14 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
import Switch from '@/Components/Switch/Switch'
import { ThemeItem } from './ThemeItem'
import RadioIndicator from '../RadioIndicator/RadioIndicator'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
type Props = {
item: ThemeItem
application: WebApplication
onBlur: (event: { relatedTarget: EventTarget | null }) => void
}
const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur }) => {
const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
const premiumModal = usePremiumModal()
const isThirdPartyTheme = useMemo(
@@ -49,7 +49,6 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:bg-info-backdrop focus:shadow-none focus:shadow-none'
}
onClick={toggleTheme}
onBlur={onBlur}
>
{item.component?.isLayerable() ? (
<>
@@ -57,7 +56,7 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur
<Switch className="mr-2 px-0" checked={item.component?.active} />
{item.name}
</div>
{!canActivateTheme && <Icon type="premium-feature" />}
{!canActivateTheme && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
</>
) : (
<>
@@ -73,7 +72,7 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur
}}
></div>
) : (
<Icon type="premium-feature" />
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />
)}
</>
)}
@@ -46,7 +46,7 @@ const HistoryListContainer: FunctionComponent<Props> = ({ features, noteHistoryC
}
return (
<div className={'flex h-full min-w-60 flex-col overflow-auto border-r border-solid border-border'}>
<div className={'flex h-full flex-grow flex-col overflow-auto border-r border-solid border-border'}>
<div className="flex border-b border-solid border-border">
<TabButton type={RevisionType.Remote} />
<TabButton type={RevisionType.Session} />
@@ -1,20 +1,12 @@
import { getPlatformString } from '@/Utils'
import { DialogOverlay, DialogContent } from '@reach/dialog'
import { ReactNode } from 'react'
import styled from 'styled-components'
type Props = {
children: ReactNode
onDismiss: () => void
}
const StyledDialogContent = styled(DialogContent)`
width: 90%;
max-width: 90%;
min-height: 90%;
background: var(--modal-background-color);
`
const HistoryModalDialog = ({ children, onDismiss }: Props) => {
return (
<DialogOverlay
@@ -22,9 +14,12 @@ const HistoryModalDialog = ({ children, onDismiss }: Props) => {
onDismiss={onDismiss}
aria-label="Note revision history"
>
<StyledDialogContent aria-label="Note revision history" className="rounded-md p-0 shadow-lg">
<DialogContent
aria-label="Note revision history"
className="my-0 flex h-full w-full flex-col rounded-md bg-[color:var(--modal-background-color)] p-0 shadow-lg md:max-h-[90%] md:w-[90%] md:max-w-[90%]"
>
<div className="flex h-full flex-col overflow-hidden bg-default">{children}</div>
</StyledDialogContent>
</DialogContent>
</DialogOverlay>
)
}
@@ -5,6 +5,8 @@ import { RevisionHistoryModalContentProps } from './RevisionHistoryModalProps'
import HistoryModalFooter from './HistoryModalFooter'
import HistoryModalContentPane from './HistoryModalContentPane'
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
import Icon from '../Icon/Icon'
import { classNames } from '@/Utils/ConcatenateClassNames'
const HistoryModalDialogContent = ({
application,
@@ -16,11 +18,52 @@ const HistoryModalDialogContent = ({
}: RevisionHistoryModalContentProps) => {
const [noteHistoryController] = useState(() => new NoteHistoryController(application, note, selectionController))
const [selectedMobileTab, setSelectedMobileTab] = useState<'List' | 'Content'>('Content')
return (
<>
<div className="flex items-center border-b border-border md:hidden">
<button
className={classNames(
'relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:shadow-inner',
selectedMobileTab === 'List' ? 'font-medium text-info shadow-bottom' : 'text-text',
)}
onClick={() => {
setSelectedMobileTab('List')
}}
>
List
</button>
<button
className={classNames(
'relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:shadow-inner',
selectedMobileTab === 'Content' ? 'font-medium text-info shadow-bottom' : 'text-text',
)}
onClick={() => {
setSelectedMobileTab('Content')
}}
>
Content
</button>
<button className="ml-auto mr-2 rounded-full border border-border p-1.5" onClick={dismissModal}>
<Icon type="close" className="h-4 w-4" />
</button>
</div>
<div className="flex min-h-0 flex-grow">
<HistoryListContainer features={application.features} noteHistoryController={noteHistoryController} />
<div className="relative flex flex-grow flex-col">
<div
className={classNames(
'w-full md:flex md:w-auto md:min-w-60',
selectedMobileTab === 'List' ? 'flex' : 'hidden',
)}
>
<HistoryListContainer features={application.features} noteHistoryController={noteHistoryController} />
</div>
<div
className={classNames(
'relative flex-grow flex-col md:flex',
selectedMobileTab === 'Content' ? 'flex' : 'hidden',
)}
>
<HistoryModalContentPane
application={application}
noteHistoryController={noteHistoryController}
@@ -41,20 +41,22 @@ const HistoryModalFooter = ({ dismissModal, noteHistoryController }: Props) => {
}, [deleteRemoteRevision, selectedEntry])
return (
<div className="min-h-6 flex flex-shrink-0 items-center justify-between border-t border-solid border-border px-2.5 py-2">
<div>
<Button className="py-1.35" label="Close" onClick={dismissModal} />
</div>
<div className="min-h-6 flex flex-shrink-0 flex-wrap items-center gap-2.5 border-t border-solid border-border px-2.5 py-2 md:justify-between">
<Button className="py-1.35" label="Close" onClick={dismissModal} />
{selectedRevision && (
<div className="flex items-center">
<>
{(selectedEntry as RevisionListEntry).uuid && (
<Button className="mr-2.5" onClick={deleteSelectedRevision}>
<Button className="md:ml-auto" onClick={deleteSelectedRevision}>
{isDeletingRevision ? <Spinner className="my-1 h-3 w-3" /> : 'Delete this revision'}
</Button>
)}
<Button className="mr-2.5" label="Restore as a copy" onClick={restoreAsCopy} />
<Button
className={!(selectedEntry as RevisionListEntry).uuid ? 'md:ml-auto' : ''}
label="Restore as a copy"
onClick={restoreAsCopy}
/>
<Button className="" label="Restore version" onClick={restoreSelectedRevision} primary />
</div>
</>
)}
</div>
)
@@ -7,6 +7,7 @@ import { previewHistoryEntryTitle } from './utils'
import { FeaturesClientInterface, RevisionListEntry } from '@standardnotes/snjs/dist/@types'
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
import Spinner from '@/Components/Spinner/Spinner'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
type RemoteHistoryListProps = {
features: FeaturesClientInterface
@@ -47,7 +48,9 @@ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({ features
>
<div className="flex flex-grow items-center justify-between">
<div>{previewHistoryEntryTitle(entry)}</div>
{!features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />}
{!features.hasMinimumRole(entry.required_role) && (
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />
)}
</div>
</HistoryListItem>
))}
@@ -12,11 +12,11 @@ const ModalDialog = ({ children, onDismiss, className }: Props) => {
const ldRef = useRef<HTMLButtonElement>(null)
return (
<AlertDialogOverlay leastDestructiveRef={ldRef} onDismiss={onDismiss}>
<AlertDialogOverlay className="px-4 md:px-0" leastDestructiveRef={ldRef} onDismiss={onDismiss}>
<AlertDialogContent
tabIndex={0}
className={classNames(
'flex w-160 flex-col rounded border border-solid border-border bg-default p-0 shadow-main',
'flex w-full flex-col rounded border border-solid border-border bg-default p-0 shadow-main md:w-160',
className,
)}
>

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