mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
theme: add theme builder app
This commit is contained in:
committed by
Abdullah Atta
parent
ed6d125391
commit
380bfba776
@@ -33,7 +33,8 @@ const SCOPES = [
|
|||||||
"misc",
|
"misc",
|
||||||
"common",
|
"common",
|
||||||
"global",
|
"global",
|
||||||
"docs"
|
"docs",
|
||||||
|
"theme-builder"
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
33
apps/theme-builder/.gitignore
vendored
Normal file
33
apps/theme-builder/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.now
|
||||||
|
|
||||||
|
__diff_output__
|
||||||
|
|
||||||
|
dist
|
||||||
|
public/workbox
|
||||||
|
scripts/secrets
|
||||||
|
test-results
|
||||||
|
.swc
|
||||||
56
apps/theme-builder/README.md
Normal file
56
apps/theme-builder/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img style="align:center;" src="/resources/screenshots/web.png" alt="Notesnook theme builder screenshot" width="600" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">Notesnook Theme Builder</h1>
|
||||||
|
<h3 align="center">The theme builder app is built using React, Typescript & Javascript.</h3>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://app.notesnook.com/">Try it out!</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a> | <a href="../desktop/">Desktop app</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
## Build instructions
|
||||||
|
|
||||||
|
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
|
||||||
|
|
||||||
|
### Setting up the development environment
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
1. [Node.js](https://nodejs.org/en/download/)
|
||||||
|
2. [git](https://git-scm.com/downloads)
|
||||||
|
3. NPM (not yarn or pnpm)
|
||||||
|
|
||||||
|
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) on your system.
|
||||||
|
|
||||||
|
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/streetwriters/notesnook.git
|
||||||
|
|
||||||
|
# change directory
|
||||||
|
cd notesnook
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# this might take a while to complete
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can finally start the web app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:theme-builder
|
||||||
|
```
|
||||||
|
|
||||||
|
If you'd like to build in production mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:theme-builder
|
||||||
|
|
||||||
|
# serve the app locally
|
||||||
|
npx serve apps/theme-builder/build
|
||||||
|
```
|
||||||
49293
apps/theme-builder/package-lock.json
generated
Normal file
49293
apps/theme-builder/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
150
apps/theme-builder/package.json
Normal file
150
apps/theme-builder/package.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"name": "@notesnook/theme-builder",
|
||||||
|
"description": "Your private note taking space",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "./src/app.js",
|
||||||
|
"homepage": "https://notesnook.com/",
|
||||||
|
"repository": "https://github.com/streetwriters/notesnook",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/util-base64-browser": "^3.208.0",
|
||||||
|
"@brixtol/currency-symbols": "^1.1.1",
|
||||||
|
"@dnd-kit/core": "^6.0.5",
|
||||||
|
"@dnd-kit/modifiers": "^6.0.0",
|
||||||
|
"@dnd-kit/sortable": "^7.0.1",
|
||||||
|
"@emotion/react": "11.11.1",
|
||||||
|
"@mdi/js": "^7.2.96",
|
||||||
|
"@mdi/react": "^1.6.1",
|
||||||
|
"@notesnook-importer/core": "^1.7.1",
|
||||||
|
"@notesnook/common": "file:../../packages/common",
|
||||||
|
"@notesnook/core": "file:../../packages/core",
|
||||||
|
"@notesnook/crypto": "file:../../packages/crypto",
|
||||||
|
"@notesnook/crypto-worker": "file:../../packages/crypto-worker",
|
||||||
|
"@notesnook/desktop": "file:../desktop",
|
||||||
|
"@notesnook/editor": "file:../../packages/editor",
|
||||||
|
"@notesnook/logger": "file:../../packages/logger",
|
||||||
|
"@notesnook/streamable-fs": "file:../../packages/streamable-fs",
|
||||||
|
"@notesnook/theme": "file:../../packages/theme",
|
||||||
|
"@notesnook/themes-server": "file:../../servers/themes",
|
||||||
|
"@notesnook/ui": "file:../../packages/ui",
|
||||||
|
"@notesnook/web": "file:../web",
|
||||||
|
"@notesnook/web-clipper": "file:../../extensions/web-clipper",
|
||||||
|
"@react-pdf-viewer/core": "^3.12.0",
|
||||||
|
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||||
|
"@tanstack/react-query": "^4.29.19",
|
||||||
|
"@theme-ui/color": "^0.14.7",
|
||||||
|
"@theme-ui/components": "^0.14.7",
|
||||||
|
"@theme-ui/core": "^0.14.7",
|
||||||
|
"@trpc/client": "10.31.0",
|
||||||
|
"@trpc/react-query": "10.31.0",
|
||||||
|
"allotment": "^1.19.0",
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"clipboard-polyfill": "4.0.0",
|
||||||
|
"comlink": "^4.3.1",
|
||||||
|
"cronosjs": "^1.7.1",
|
||||||
|
"dayjs": "1.11.9",
|
||||||
|
"electron-trpc": "0.5.2",
|
||||||
|
"event-source-polyfill": "^1.0.25",
|
||||||
|
"fflate": "^0.8.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"framer-motion": "^6.5.1",
|
||||||
|
"hash-wasm": "^4.9.0",
|
||||||
|
"hotkeys-js": "^3.8.3",
|
||||||
|
"immer": "^9.0.6",
|
||||||
|
"katex": "0.16.2",
|
||||||
|
"mac-scrollbar": "^0.10.3",
|
||||||
|
"marked": "^4.1.0",
|
||||||
|
"pdfjs-dist": "3.6.172",
|
||||||
|
"phone": "^3.1.14",
|
||||||
|
"platform": "^1.3.6",
|
||||||
|
"qclone": "^1.2.0",
|
||||||
|
"react-dropzone": "^11.4.2",
|
||||||
|
"react-hot-toast": "^2.2.0",
|
||||||
|
"react-loading-skeleton": "^3.1.0",
|
||||||
|
"react-modal": "3.13.1",
|
||||||
|
"react-qrcode-logo": "^2.2.1",
|
||||||
|
"react-scroll-sync": "^0.9.0",
|
||||||
|
"react-virtuoso": "^4.4.2",
|
||||||
|
"timeago.js": "4.0.2",
|
||||||
|
"tinycolor2": "^1.6.0",
|
||||||
|
"w3c-keyname": "^2.2.6",
|
||||||
|
"web-streams-polyfill": "^3.1.1",
|
||||||
|
"wouter": "2.7.3",
|
||||||
|
"zustand": "4.3.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.22.5",
|
||||||
|
"@playwright/test": "^1.36.2",
|
||||||
|
"@trpc/server": "10.31.0",
|
||||||
|
"@types/babel__core": "^7.20.1",
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"@types/marked": "^4.0.7",
|
||||||
|
"@types/node-fetch": "^2.5.10",
|
||||||
|
"@types/platform": "^1.3.4",
|
||||||
|
"@types/react": "17.0.2",
|
||||||
|
"@types/react-dom": "17.0.2",
|
||||||
|
"@types/react-modal": "3.13.1",
|
||||||
|
"@types/tinycolor2": "^1.4.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"find-process": "^1.4.4",
|
||||||
|
"happy-dom": "^8.9.0",
|
||||||
|
"ip": "^1.1.8",
|
||||||
|
"lorem-ipsum": "^2.0.4",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
|
"react": "17.0.2",
|
||||||
|
"react-dom": "17.0.2",
|
||||||
|
"rollup": "^3.27.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
|
"swc-plugin-react-remove-properties": "^0.1.4",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vite-plugin-env-compatible": "^1.1.1",
|
||||||
|
"vite-plugin-pwa": "^0.16.3",
|
||||||
|
"vite-plugin-svgr": "^3.2.0",
|
||||||
|
"vitest": "^0.32.0",
|
||||||
|
"workbox-core": "^7.0.0",
|
||||||
|
"workbox-expiration": "^7.0.0",
|
||||||
|
"workbox-precaching": "^7.0.0",
|
||||||
|
"workbox-routing": "^7.0.0",
|
||||||
|
"workbox-strategies": "^7.0.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"react@>17": "17.0.2",
|
||||||
|
"react-dom@>17": "17.0.2",
|
||||||
|
"@types/react@>17": "17.0.2",
|
||||||
|
"@types/react-dom@>17": "17.0.2",
|
||||||
|
"@emotion/react@>11": "11.10.5",
|
||||||
|
"@theme-ui/components@>0": "0.14.7",
|
||||||
|
"@theme-ui/core@>0": "0.14.7"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "cross-env PLATFORM=web vite",
|
||||||
|
"build": "cross-env PLATFORM=web vite build"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all",
|
||||||
|
"ie >= 9"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version",
|
||||||
|
"last 3 ie version",
|
||||||
|
"last 4 edge version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Streetwriters (Private) Limited",
|
||||||
|
"email": "support@streetwriters.co",
|
||||||
|
"url": "https://streetwriters.co"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/theme-builder/resources/screenshots/web.png
Normal file
BIN
apps/theme-builder/resources/screenshots/web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
217
apps/theme-builder/src/app.css
Normal file
217
apps/theme-builder/src/app.css
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
:root {
|
||||||
|
--sash-size: 10px;
|
||||||
|
--sash-hover-size: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sash-module_sash__K-9lB.sash-module_hover__80W6I:before,
|
||||||
|
.sash-module_sash__K-9lB.sash-module_active__bJspD:before {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allotment-module_splitView__L-yRc.allotment-module_separatorBorder__x-rDS
|
||||||
|
> .allotment-module_splitViewContainer__rQnVa
|
||||||
|
> .allotment-module_splitViewView__MGZ6O:not(:first-child)::before {
|
||||||
|
background-color: var(--border);
|
||||||
|
}
|
||||||
|
/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
/* open-sans-600 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
/* open-sans-700 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
/* open-sans-italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
/* open-sans-600italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
/* open-sans-700italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(""),
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
/* Super Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Modern Browsers */
|
||||||
|
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpv-core__text-layer,
|
||||||
|
.rpv-core__text-layer *,
|
||||||
|
.selectable,
|
||||||
|
.selectable *,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
[contenteditable="true"],
|
||||||
|
[contenteditable="true"] * {
|
||||||
|
-webkit-touch-callout: initial;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
-khtml-user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
-ms-user-select: initial;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpv-core__text-layer-text::selection {
|
||||||
|
background-color: var(--textSelection) !important;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpv-core__text-layer-text::-moz-selection {
|
||||||
|
/* Code for Firefox */
|
||||||
|
background-color: var(--textSelection) !important;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
/* Code for Firefox */
|
||||||
|
background-color: var(--textSelection);
|
||||||
|
color: var(--paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--textSelection);
|
||||||
|
color: var(--paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-toast {
|
||||||
|
padding: 0px 10px !important;
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-pane {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.editor-pane {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.nav-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.pane::before {
|
||||||
|
width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route#settings,
|
||||||
|
#mainRouteContainer {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-track:hover,
|
||||||
|
.ms-track:active {
|
||||||
|
background: var(--background-secondary) !important;
|
||||||
|
border-left: 1px solid var(--border-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-track {
|
||||||
|
border-width: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-active .ms-thumb {
|
||||||
|
filter: brightness(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-thumb {
|
||||||
|
background: var(--paragraph-secondary) !important;
|
||||||
|
}
|
||||||
308
apps/theme-builder/src/app.tsx
Normal file
308
apps/theme-builder/src/app.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, Suspense, useRef } from "react";
|
||||||
|
import { Box, Flex } from "@theme-ui/components";
|
||||||
|
import {
|
||||||
|
BaseThemeProvider,
|
||||||
|
ScopedThemeProvider
|
||||||
|
} from "@notesnook/web/src/components/theme-provider";
|
||||||
|
import useMobile from "@notesnook/web/src/hooks/use-mobile";
|
||||||
|
import useTablet from "@notesnook/web/src/hooks/use-tablet";
|
||||||
|
import useDatabase from "@notesnook/web/src/hooks/use-database";
|
||||||
|
import { Allotment, AllotmentHandle, LayoutPriority } from "allotment";
|
||||||
|
import { useStore } from "@notesnook/web/src/stores/app-store";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import { ViewLoader } from "@notesnook/web/src/components/loaders/view-loader";
|
||||||
|
import NavigationMenu from "@notesnook/web/src/components/navigation-menu";
|
||||||
|
import StatusBar from "@notesnook/web/src/components/status-bar";
|
||||||
|
import { EditorLoader } from "@notesnook/web/src/components/loaders/editor-loader";
|
||||||
|
import { FlexScrollContainer } from "@notesnook/web/src/components/scroll-container";
|
||||||
|
import CachedRouter from "@notesnook/web/src/components/cached-router";
|
||||||
|
import { WebExtensionRelay } from "@notesnook/web/src/utils/web-extension-relay";
|
||||||
|
import { usePersistentState } from "@notesnook/web/src/hooks/use-persistent-state";
|
||||||
|
import ThemeBuilder from "./components/theme-builder";
|
||||||
|
|
||||||
|
new WebExtensionRelay();
|
||||||
|
|
||||||
|
const GlobalMenuWrapper = React.lazy(
|
||||||
|
() => import("@notesnook/web/src/components/global-menu-wrapper")
|
||||||
|
);
|
||||||
|
const AppEffects = React.lazy(() => import("@notesnook/web/src/app-effects"));
|
||||||
|
const MobileAppEffects = React.lazy(
|
||||||
|
() => import("@notesnook/web/src/app-effects.mobile")
|
||||||
|
);
|
||||||
|
const HashRouter = React.lazy(
|
||||||
|
() => import("@notesnook/web/src/components/hash-router")
|
||||||
|
);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [show, setShow] = useState(true);
|
||||||
|
const [isAppLoaded] = useDatabase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseThemeProvider sx={{ height: "100%" }} addGlobalStyles>
|
||||||
|
{isAppLoaded && (
|
||||||
|
<Suspense fallback={<div style={{ display: "none" }} />}>
|
||||||
|
<div id="menu-wrapper">
|
||||||
|
<GlobalMenuWrapper />
|
||||||
|
</div>
|
||||||
|
<AppEffects setShow={setShow} />
|
||||||
|
{isMobile && (
|
||||||
|
<MobileAppEffects
|
||||||
|
sliderId="slider"
|
||||||
|
overlayId="overlay"
|
||||||
|
setShow={setShow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
<Flex
|
||||||
|
id="app"
|
||||||
|
bg="background"
|
||||||
|
sx={{ overflow: "hidden", flexDirection: "column", height: "100%" }}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<MobileAppContents isAppLoaded={isAppLoaded} />
|
||||||
|
) : (
|
||||||
|
<DesktopAppContents
|
||||||
|
isAppLoaded={isAppLoaded}
|
||||||
|
setShow={setShow}
|
||||||
|
show={show}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Toaster containerClassName="toasts-container" />
|
||||||
|
</Flex>
|
||||||
|
</BaseThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
type SuspenseLoaderProps<TComponent extends React.JSXElementConstructor<any>> =
|
||||||
|
{
|
||||||
|
condition: boolean;
|
||||||
|
props?: React.ComponentProps<TComponent>;
|
||||||
|
component: TComponent;
|
||||||
|
fallback: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SuspenseLoader<TComponent extends React.JSXElementConstructor<any>>({
|
||||||
|
condition,
|
||||||
|
props,
|
||||||
|
component,
|
||||||
|
fallback
|
||||||
|
}: SuspenseLoaderProps<TComponent>) {
|
||||||
|
if (!condition) return fallback;
|
||||||
|
|
||||||
|
const Component = component as (
|
||||||
|
props: any
|
||||||
|
) => React.ReactComponentElement<any, any>;
|
||||||
|
return (
|
||||||
|
<Suspense fallback={IS_DESKTOP_APP ? null : fallback}>
|
||||||
|
<Component {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DesktopAppContentsProps = {
|
||||||
|
isAppLoaded: boolean;
|
||||||
|
show: boolean;
|
||||||
|
setShow: (show: boolean) => void;
|
||||||
|
};
|
||||||
|
function DesktopAppContents({
|
||||||
|
isAppLoaded,
|
||||||
|
show,
|
||||||
|
setShow
|
||||||
|
}: DesktopAppContentsProps) {
|
||||||
|
const isFocusMode = useStore((store: any) => store.isFocusMode);
|
||||||
|
const isTablet = useTablet();
|
||||||
|
const [paneSizes, setPaneSizes] = usePersistentState("paneSizes", [
|
||||||
|
isTablet ? 60 : 180,
|
||||||
|
isTablet ? 240 : 380
|
||||||
|
]);
|
||||||
|
const panesRef = useRef<AllotmentHandle>(null);
|
||||||
|
const [isNarrow, setIsNarrow] = useState(paneSizes[0] <= 55);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
variant="rowFill"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment
|
||||||
|
ref={panesRef}
|
||||||
|
proportionalLayout={false}
|
||||||
|
onDragEnd={(sizes) => {
|
||||||
|
setPaneSizes(sizes);
|
||||||
|
setIsNarrow(sizes[0] <= 55);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane
|
||||||
|
className="pane nav-pane"
|
||||||
|
minSize={50}
|
||||||
|
preferredSize={isTablet ? 50 : paneSizes[0]}
|
||||||
|
visible={!isFocusMode}
|
||||||
|
priority={LayoutPriority.Low}
|
||||||
|
>
|
||||||
|
<NavigationMenu
|
||||||
|
toggleNavigationContainer={(state) => {
|
||||||
|
setShow(state || !show);
|
||||||
|
}}
|
||||||
|
isTablet={isNarrow}
|
||||||
|
/>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane
|
||||||
|
className="pane middle-pane"
|
||||||
|
minSize={2}
|
||||||
|
preferredSize={paneSizes[1]}
|
||||||
|
visible={show}
|
||||||
|
priority={LayoutPriority.Normal}
|
||||||
|
>
|
||||||
|
<ScopedThemeProvider
|
||||||
|
className="listMenu"
|
||||||
|
scope="list"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
bg: "background"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAppLoaded && <CachedRouter />}
|
||||||
|
</ScopedThemeProvider>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane
|
||||||
|
className="pane editor-pane"
|
||||||
|
priority={LayoutPriority.High}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
bg: "background"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAppLoaded && (
|
||||||
|
<SuspenseLoader
|
||||||
|
fallback={<EditorLoader />}
|
||||||
|
component={HashRouter}
|
||||||
|
condition={isAppLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane
|
||||||
|
className="pane theme-builder-pane"
|
||||||
|
minSize={50}
|
||||||
|
maxSize={250}
|
||||||
|
>
|
||||||
|
<ThemeBuilder />
|
||||||
|
</Allotment.Pane>
|
||||||
|
</Allotment>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<StatusBar />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
|
||||||
|
return (
|
||||||
|
<FlexScrollContainer
|
||||||
|
id="slider"
|
||||||
|
suppressScrollX
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
overflowY: "hidden",
|
||||||
|
scrollSnapType: "x mandatory",
|
||||||
|
scrollBehavior: "smooth",
|
||||||
|
WebkitOverflowScrolling: "touch",
|
||||||
|
scrollSnapStop: "always",
|
||||||
|
overscrollBehavior: "contain",
|
||||||
|
overflowX: "auto",
|
||||||
|
flex: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
scrollSnapAlign: "start",
|
||||||
|
scrollSnapStop: "always",
|
||||||
|
width: [300, 60],
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavigationMenu toggleNavigationContainer={() => {}} isTablet={false} />
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
className="listMenu"
|
||||||
|
variant="columnFill"
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
scrollSnapAlign: "start",
|
||||||
|
scrollSnapStop: "always",
|
||||||
|
flexShrink: 0,
|
||||||
|
width: "100vw"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SuspenseLoader
|
||||||
|
condition={isAppLoaded}
|
||||||
|
component={CachedRouter}
|
||||||
|
fallback={<ViewLoader />}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
id="overlay"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 999,
|
||||||
|
opacity: 0,
|
||||||
|
visibility: "visible",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}}
|
||||||
|
bg="black"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
scrollSnapAlign: "start",
|
||||||
|
scrollSnapStop: "always",
|
||||||
|
flexDirection: "column",
|
||||||
|
flexShrink: 0,
|
||||||
|
width: "100vw"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SuspenseLoader
|
||||||
|
fallback={<EditorLoader />}
|
||||||
|
component={HashRouter}
|
||||||
|
condition={isAppLoaded}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FlexScrollContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
599
apps/theme-builder/src/components/theme-builder/index.tsx
Normal file
599
apps/theme-builder/src/components/theme-builder/index.tsx
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { debounce } from "@notesnook/common";
|
||||||
|
import {
|
||||||
|
COLORS,
|
||||||
|
ThemeAuthor,
|
||||||
|
ThemeDefinition,
|
||||||
|
THEME_SCOPES,
|
||||||
|
Variants,
|
||||||
|
validateTheme
|
||||||
|
} from "@notesnook/theme";
|
||||||
|
import { Button, Flex, Input, Text } from "@theme-ui/components";
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useStore } from "@notesnook/web/src/stores/theme-store";
|
||||||
|
import { showToast } from "@notesnook/web/src/utils/toast";
|
||||||
|
import Accordion from "@notesnook/web/src/components/accordion";
|
||||||
|
import { showFilePicker } from "@notesnook/web/src/utils/file-picker";
|
||||||
|
import Field from "@notesnook/web/src/components/field";
|
||||||
|
import { Close } from "@notesnook/web/src/components/icons";
|
||||||
|
|
||||||
|
const JSON_SCHEMA_URL =
|
||||||
|
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json";
|
||||||
|
const ThemeInfoTemplate: Omit<
|
||||||
|
ThemeDefinition,
|
||||||
|
"authors" | "compatibilityVersion" | "colorScheme" | "codeBlockCSS" | "scopes"
|
||||||
|
> = {
|
||||||
|
name: "",
|
||||||
|
id: "",
|
||||||
|
version: 0,
|
||||||
|
license: "",
|
||||||
|
homepage: "",
|
||||||
|
description: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
function toTitleCase(value: string) {
|
||||||
|
return (
|
||||||
|
value.slice(0, 1).toUpperCase() +
|
||||||
|
value.slice(1).replace(/([A-Z]+)*([A-Z][a-z])/g, "$1 $2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatten = (object: { [name: string]: any }) => {
|
||||||
|
const flattenedObject: { [name: string]: any } = {};
|
||||||
|
|
||||||
|
for (const innerObj in object) {
|
||||||
|
if (typeof object[innerObj] === "object") {
|
||||||
|
if (typeof object[innerObj] === "function") continue;
|
||||||
|
|
||||||
|
const newObject = flatten(object[innerObj]);
|
||||||
|
for (const key in newObject) {
|
||||||
|
flattenedObject[innerObj + "." + key] = newObject[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof object[innerObj] === "function") continue;
|
||||||
|
flattenedObject[innerObj] = object[innerObj];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flattenedObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
function unflatten(data: any) {
|
||||||
|
const result = {};
|
||||||
|
for (const i in data) {
|
||||||
|
const keys = i.split(".");
|
||||||
|
keys.reduce(function (r: any, e, j) {
|
||||||
|
return (
|
||||||
|
r[e] ||
|
||||||
|
(r[e] = isNaN(Number(keys[j + 1]))
|
||||||
|
? keys.length - 1 == j
|
||||||
|
? data[i]
|
||||||
|
: {}
|
||||||
|
: [])
|
||||||
|
);
|
||||||
|
}, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeBuilder() {
|
||||||
|
const currentTheme = useStore((state: any) =>
|
||||||
|
state.colorScheme === "dark" ? state.darkTheme : state.lightTheme
|
||||||
|
);
|
||||||
|
const setTheme = useStore((state: any) => state.setTheme);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const currentThemeFlattened = flatten(currentTheme);
|
||||||
|
|
||||||
|
const [authors, setAuthors] = useState(
|
||||||
|
currentTheme.authors || [
|
||||||
|
{
|
||||||
|
name: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formRef = useRef(null);
|
||||||
|
|
||||||
|
const onChange: React.FormEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
if (!formRef.current) return;
|
||||||
|
const body = new FormData(formRef.current);
|
||||||
|
const flattenedThemeRaw = {
|
||||||
|
...Object.fromEntries(body.entries()),
|
||||||
|
...flatten({ authors: [...authors] })
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedTheme: { [name: string]: any } = {};
|
||||||
|
|
||||||
|
for (const key in flattenedThemeRaw) {
|
||||||
|
if (flattenedThemeRaw[key] === "" || !flattenedThemeRaw[key]) continue;
|
||||||
|
if (key === "compatibilityVersion" || key === "version") {
|
||||||
|
flattenedTheme[key] = parseFloat(flattenedThemeRaw[key]);
|
||||||
|
} else {
|
||||||
|
flattenedTheme[key] = flattenedThemeRaw[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = unflatten(flattenedTheme);
|
||||||
|
const result = validateTheme(theme as ThemeDefinition);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
showToast("error", result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme({ ...theme } as ThemeDefinition);
|
||||||
|
}, 3000),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadThemeFile = async () => {
|
||||||
|
const file = await showFilePicker({
|
||||||
|
acceptedFileTypes: ".nnbackup,application/json,.json"
|
||||||
|
});
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
const theme = (await new Promise((resolve) => {
|
||||||
|
reader.addEventListener("load", (event) => {
|
||||||
|
const text = event.target?.result;
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(text as string));
|
||||||
|
} catch (e) {
|
||||||
|
alert(
|
||||||
|
"Error: Could not read the backup file provided. Either it's corrupted or invalid."
|
||||||
|
);
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reader.readAsText(file);
|
||||||
|
})) as ThemeDefinition | undefined;
|
||||||
|
if (
|
||||||
|
!theme ||
|
||||||
|
!theme.scopes ||
|
||||||
|
!theme.compatibilityVersion ||
|
||||||
|
!theme.id ||
|
||||||
|
!theme.version
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
setLoading(true);
|
||||||
|
setTheme(theme);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportTheme = () => {
|
||||||
|
const json = JSON.stringify({
|
||||||
|
...currentTheme,
|
||||||
|
$schema: JSON_SCHEMA_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
FileSaver.saveAs(
|
||||||
|
new Blob([json], {
|
||||||
|
type: "text/plain"
|
||||||
|
}),
|
||||||
|
`${currentTheme.id}.json`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeColor = (
|
||||||
|
target: HTMLInputElement,
|
||||||
|
sibling: HTMLInputElement
|
||||||
|
) => {
|
||||||
|
const value = target.value;
|
||||||
|
if ((sibling as HTMLInputElement).value !== value) {
|
||||||
|
(sibling as HTMLInputElement).value = target.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return loading ? null : (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
overflowY: "scroll",
|
||||||
|
padding: "10px 10px",
|
||||||
|
rowGap: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
|
variant="heading"
|
||||||
|
>
|
||||||
|
Theme Builder 1.0
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Button sx={{ py: "7px" }} variant="secondary" onClick={exportTheme}>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export theme
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ py: "7px" }} variant="secondary" onClick={loadThemeFile}>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load theme file
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
as="form"
|
||||||
|
id="theme-form"
|
||||||
|
ref={formRef}
|
||||||
|
onChange={onChange}
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
flexDirection: "column",
|
||||||
|
rowGap: "0.5rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.keys(ThemeInfoTemplate).map((key) => {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
key={key}
|
||||||
|
label={toTitleCase(key)}
|
||||||
|
name={key}
|
||||||
|
defaultValue={currentThemeFlattened[key]}
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "normal",
|
||||||
|
color: "paragraph-secondary"
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginLeft: "0px",
|
||||||
|
marginRight: "0px",
|
||||||
|
height: "30px",
|
||||||
|
fontSize: "12px"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<SelectItem
|
||||||
|
label="Color Scheme"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
title: "Light",
|
||||||
|
value: "light"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dark",
|
||||||
|
value: "dark"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
defaultValue={currentThemeFlattened["colorScheme"]}
|
||||||
|
name="colorScheme"
|
||||||
|
key="colorScheme"
|
||||||
|
/>
|
||||||
|
<SelectItem
|
||||||
|
label="Compatibility version"
|
||||||
|
name="compatibilityVersion"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
title: "1.0",
|
||||||
|
value: 1.0
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
defaultValue={currentThemeFlattened["compatibilityVersion"]}
|
||||||
|
key="compatibilityVersion"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{authors.map((author: any, index: number) => (
|
||||||
|
<Flex
|
||||||
|
key={author.name}
|
||||||
|
sx={{
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "paragraph-secondary",
|
||||||
|
flexShrink: 0,
|
||||||
|
flex: 0.5,
|
||||||
|
marginRight: "20px",
|
||||||
|
mb: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Author {index + 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{authors.length > 1 ? (
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
height: 25
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (authors.length === 1) {
|
||||||
|
console.log("Theme must have at least one author");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAuthors((current: any) => {
|
||||||
|
const authors = [...current];
|
||||||
|
authors.splice(index, 1);
|
||||||
|
return authors;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Close
|
||||||
|
sx={{
|
||||||
|
width: 15,
|
||||||
|
height: 15
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{["name", "email", "url"].map((key) => (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
key={key}
|
||||||
|
label={toTitleCase(key)}
|
||||||
|
name={`authors.${index}.${key}`}
|
||||||
|
required={key === "name"}
|
||||||
|
defaultValue={author[key as keyof ThemeAuthor]}
|
||||||
|
styles={{
|
||||||
|
container: {
|
||||||
|
ml: 1
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "normal",
|
||||||
|
color: "paragraph-secondary"
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginLeft: "0px",
|
||||||
|
marginRight: "0px",
|
||||||
|
height: "30px",
|
||||||
|
fontSize: "12px"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAuthors((current: any) => {
|
||||||
|
const authors = [...current];
|
||||||
|
authors.push({
|
||||||
|
name: ""
|
||||||
|
});
|
||||||
|
return authors;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Text color="accent">Add author</Text>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{THEME_SCOPES.map((scopeName) => (
|
||||||
|
<>
|
||||||
|
<Accordion
|
||||||
|
isClosed={false}
|
||||||
|
buttonSx={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
borderRadius: 0,
|
||||||
|
p: 0,
|
||||||
|
py: 1
|
||||||
|
}}
|
||||||
|
titleSx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "paragraph-secondary"
|
||||||
|
}}
|
||||||
|
title={toTitleCase(scopeName)}
|
||||||
|
>
|
||||||
|
{Variants.map((variantName) => (
|
||||||
|
<>
|
||||||
|
<Accordion
|
||||||
|
isClosed={true}
|
||||||
|
buttonSx={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
p: 0,
|
||||||
|
py: 1
|
||||||
|
}}
|
||||||
|
titleSx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "paragraph-secondary",
|
||||||
|
fontWeight: "normal",
|
||||||
|
ml: 1
|
||||||
|
}}
|
||||||
|
title={toTitleCase(variantName)}
|
||||||
|
>
|
||||||
|
{COLORS.map((colorName) => (
|
||||||
|
<Flex
|
||||||
|
key={colorName}
|
||||||
|
sx={{
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
ml: 3
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "paragraph-secondary",
|
||||||
|
flexShrink: 0,
|
||||||
|
flex: 0.5,
|
||||||
|
marginRight: "20px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{colorName}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
height: "25px",
|
||||||
|
borderRadius: 0,
|
||||||
|
flex: 0.4,
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
outline: "none",
|
||||||
|
":hover": {
|
||||||
|
borderWidth: "0px",
|
||||||
|
outline: "none"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
/hover|shade|backdrop|textSelection/g.test(
|
||||||
|
colorName
|
||||||
|
)
|
||||||
|
? `Hex RGB & ARGB values both are supported. (e.g. #dbdbdb99)`
|
||||||
|
: `Only Hex RGB values are supported. No Alpha. (e.g. #f33ff3)`
|
||||||
|
}
|
||||||
|
required={scopeName === "base"}
|
||||||
|
name={`scopes.${scopeName}.${variantName}.${colorName}`}
|
||||||
|
defaultValue={
|
||||||
|
currentThemeFlattened[
|
||||||
|
`scopes.${scopeName}.${variantName}.${colorName}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChangeColor(
|
||||||
|
event.target,
|
||||||
|
event.target
|
||||||
|
.nextElementSibling as HTMLInputElement
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
onChange={(event) => {
|
||||||
|
onChangeColor(
|
||||||
|
event.target,
|
||||||
|
event.target
|
||||||
|
.previousElementSibling as HTMLInputElement
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
/hover|shade|backdrop|textSelection/g.test(
|
||||||
|
colorName
|
||||||
|
)
|
||||||
|
? `Only Hex RGB values are supported. No Alpha. (e.g. #f33ff3)`
|
||||||
|
: `Hex RGB & ARGB values both are supported. (e.g. #dbdbdb99)`
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
currentThemeFlattened[
|
||||||
|
`scopes.${scopeName}.${variantName}.${colorName}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 0,
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
outline: "none",
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
padding: "0px"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem(props: {
|
||||||
|
options: { title: string; value: any }[];
|
||||||
|
defaultValue: any;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "normal",
|
||||||
|
color: "paragraph-secondary"
|
||||||
|
}}
|
||||||
|
mb={1}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--background)",
|
||||||
|
outline: "none",
|
||||||
|
border: "1px solid var(--border-secondary)",
|
||||||
|
borderRadius: "5px",
|
||||||
|
color: "var(--paragraph)",
|
||||||
|
height: "33px",
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
|
name={props.name}
|
||||||
|
defaultValue={props.defaultValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = (e.target as HTMLSelectElement).value;
|
||||||
|
props.onChange?.(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
apps/theme-builder/src/global.d.ts
vendored
Normal file
37
apps/theme-builder/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-var */
|
||||||
|
|
||||||
|
import "vite/client";
|
||||||
|
import "vite-plugin-svgr/client";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var PUBLIC_URL: string;
|
||||||
|
var APP_VERSION: string;
|
||||||
|
var GIT_HASH: string;
|
||||||
|
var IS_DESKTOP_APP: boolean;
|
||||||
|
var IS_TESTING: boolean;
|
||||||
|
var PLATFORM: "web" | "desktop";
|
||||||
|
var IS_BETA: boolean;
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
os?: () => NodeJS.Platform | "mas";
|
||||||
|
NativeNNCrypto?: new () => import("@notesnook/crypto").NNCrypto;
|
||||||
|
}
|
||||||
|
}
|
||||||
265
apps/theme-builder/src/index.html
Normal file
265
apps/theme-builder/src/index.html
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAHUUExURUxpcf////////////////////////////////////////////////////////////////////////////////////39/f39/f////////////////////////////////z8/O3s7P7+/vTz8wAAAPz7++/v7/Py8u/u7vDv7+7t7dfX1/79/ezr66Kiovv6+vf29vX09OTj4w0NDfv7++Ti4vr6+vT09Ovq6vn5+f39/fb19dnX1/b29tvb29/f3xMTE/j4+Pn4+O7s7MnJyerq6uno6M/Pz8jIyN3d3dnZ2d7e3kZGRvj39/38/Pf39+zs7PDw8OPi4ubl5cvLy+rp6czMzPLy8vPz887Ozujo6MrKyuDf3+fn59PT0+bm5uXl5c3NzfX19evr6wsLC+/t7ePj4/Ty8u3r6/Lx8eXk5OPh4dDQ0Nza2tjX19vZ2fHw8Nva2tLR0djW1tbW1tra2tLS0uHh4evp6eLi4tXV1djY2L+/vysrK7q6utHR0aGhoa6urrm5uQICAigoKCEhIZCQkFxcXBgYGFRUVBISEm1tbQ4ODklJSbu7uyAgID09PZqamkVFRXh4eLGxsTg4OOTk5FFRUQEBAQoKCnx8fBcXF7/+rP8AAAAfdFJOUwBOqO2h6KT9Mtyr5+w3Str+6TTZOjXb31M24O8BOd0PapSsAAABxElEQVQ4y2NgYOBgYWWXxwLYBdmEGICAjx9Z1NJJVQfB42QE6hdG0RaoaaCqhKSCl4EF1VxNVAXyTAzcaArUUBXwMLBjKDBBViDAII+uwBZFgTy6AjU1awIKNKz1aa0g1VBfB58CDS08CuIb40AKzLAqsJKXN2tXacChwKS+rTtB3tJHRU9ewxmbFZ6tKiqTrSAKdIO1LDEUaPeoqMzsUwAp0NIt8Uj0ijJXQFVgnDJVZcK0XrACXcXY0sjoHM8AfyUkBbOn9KvMmgRSYFEW4hUVqJagHeaShqTAwX7ijJQusAkW5VXRHu6eruFqaigKkqarqAAVKFbYxWiYq1XGedijuMHBWz6xo7m2Rl7RwtvdJaw6NjzCFl2BVVNdshJQgbqBsqtdpIu7nasjXIFXS2cyRL2CW6i6JTCgFCLitUPs4QrM7cDmKZi7FRYVm6o5QeLbDGICLNEqJAVk59rk2djY5Me4FThawQOKFSzrrxmaZWpqBAamQUGmwRaakNTNzMAm7+ekrKmlqOjsrIgAGcAA1ci0NVPiYhAV8zVURgbp6nBg7isuwsDAKKfkp4AEVOHASklKApS9JZmY5bECaS5ZGQYA3vqpy6NoYh0AAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#01c352" />
|
||||||
|
<meta name="msapplication-TileColor" content="#01c352" />
|
||||||
|
<meta name="theme-color" content="#01c352" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<!-- <link rel="manifest" href="/manifest.json" /> -->
|
||||||
|
<!--
|
||||||
|
Notice the use of in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<meta name="description" content="Your private note taking space" />
|
||||||
|
<meta property="og:title" content="Notesnook" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="A fully open source & end-to-end encrypted note taking alternative to Evernote."
|
||||||
|
/>
|
||||||
|
<meta property="og:image" content="https://app.notesnook.com/banner.jpg" />
|
||||||
|
<meta
|
||||||
|
property="og:image:alt"
|
||||||
|
content="A fully open source & end-to-end encrypted note taking alternative to Evernote."
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content="https://app.notesnook.com/" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="Notesnook by Streetwriters LLC" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<title>Notesnook</title>
|
||||||
|
|
||||||
|
<script nonce="7WIq8hRwApoXhctoGZZthMLYQLRNiprTwcPi6Azdf">
|
||||||
|
const colorScheme = JSON.parse(
|
||||||
|
window.localStorage.getItem("colorScheme") || '"light"'
|
||||||
|
);
|
||||||
|
const root = document.querySelector("html");
|
||||||
|
if (root) root.setAttribute("data-theme", colorScheme);
|
||||||
|
</script>
|
||||||
|
<script type="module" src="/index.tsx"></script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--bg: #0f0f0f;
|
||||||
|
--fg: #fff;
|
||||||
|
--three-bars-bg: #494949;
|
||||||
|
--gradient-stop-color: #111111;
|
||||||
|
--n-letter-color: #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--bg: #fff;
|
||||||
|
--three-bars-bg: #bebebe;
|
||||||
|
--gradient-stop-color: #fff9f9;
|
||||||
|
--n-letter-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .react-loading-skeleton {
|
||||||
|
--base-color: var(--background-secondary);
|
||||||
|
--highlight-color: #var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .react-loading-skeleton {
|
||||||
|
--base-color: var(--background-secondary);
|
||||||
|
--highlight-color: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash svg {
|
||||||
|
transform: scale(1);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-size: 18px;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeUp {
|
||||||
|
0% {
|
||||||
|
transform: translateY(500px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
} */
|
||||||
|
.ReactModal__Overlay {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactModal__Overlay--after-open {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactModal__Overlay--before-close {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel.carousel-slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/highlightjs@9.16.2/highlight.pack.min.js"></script>
|
||||||
|
-->
|
||||||
|
<div id="root"></div>
|
||||||
|
<div
|
||||||
|
id="splash"
|
||||||
|
style="
|
||||||
|
background-color: var(--bg);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 339 339" style="width: 150px">
|
||||||
|
<defs />
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#a"
|
||||||
|
id="b"
|
||||||
|
x1="188.61227"
|
||||||
|
x2="193.54405"
|
||||||
|
y1="165.2058"
|
||||||
|
y2="216.81519"
|
||||||
|
gradientTransform="rotate(5 4448 -4204) scale(2.93671)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<linearGradient id="a">
|
||||||
|
<stop offset="0" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="var(--gradient-stop-color)"
|
||||||
|
stop-opacity="0"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="167.8"
|
||||||
|
x2="270.6"
|
||||||
|
y1="76.9"
|
||||||
|
y2="64.2"
|
||||||
|
gradientTransform="rotate(5 465 -2050) scale(1.50082)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
xlink:href="#a"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(0 42)">
|
||||||
|
<path fill="url(#b)" d="M160 205l154 42-141 44-155-42z" />
|
||||||
|
<path fill="url(#c)" d="M160-35v240l154 42 1-253z" />
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.2"
|
||||||
|
d="M160 205V-35m0 240L18 249m142-44l154 41"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="var(--n-letter-color)"
|
||||||
|
d="M84 109l35 54V98l21-7v91l-27 9-35-54v65l-21 6v-91z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
width="86.1"
|
||||||
|
height="12.6"
|
||||||
|
x="185"
|
||||||
|
y="97"
|
||||||
|
fill="var(--three-bars-bg)"
|
||||||
|
ry="2.3"
|
||||||
|
transform="skewY(15) scale(.9669 1)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="var(--three-bars-bg)"
|
||||||
|
d="M181 169l99 26 2 3v8c0 1-1 2-2 1l-99-26-2-3v-7c0-2 1-2 2-2zm0-47l99 27 2 2v8l-2 2-99-27c-1 0-2-1-2-3v-7l2-2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="dialogContainer"></div>
|
||||||
|
<div id="floatingViewContainer"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
209
apps/theme-builder/src/index.tsx
Normal file
209
apps/theme-builder/src/index.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./polyfills";
|
||||||
|
import "@notesnook/core/types";
|
||||||
|
import {
|
||||||
|
AppEventManager,
|
||||||
|
AppEvents
|
||||||
|
} from "@notesnook/web/src/common/app-events";
|
||||||
|
import { render } from "react-dom";
|
||||||
|
import {
|
||||||
|
getCurrentHash,
|
||||||
|
getCurrentPath,
|
||||||
|
makeURL
|
||||||
|
} from "@notesnook/web/src/navigation";
|
||||||
|
import Config from "@notesnook/web/src/utils/config";
|
||||||
|
|
||||||
|
import { initalizeLogger, logger } from "@notesnook/web/src/utils/logger";
|
||||||
|
import { AuthProps } from "@notesnook/web/src/views/auth";
|
||||||
|
import { loadDatabase } from "@notesnook/web/src/hooks/use-database";
|
||||||
|
|
||||||
|
type Route<TProps = null> = {
|
||||||
|
component: () => Promise<{
|
||||||
|
default: TProps extends null
|
||||||
|
? () => JSX.Element
|
||||||
|
: (props: TProps) => JSX.Element;
|
||||||
|
}>;
|
||||||
|
props: TProps | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteWithPath<T = null> = {
|
||||||
|
route: Route<T>;
|
||||||
|
path: Routes;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Routes = keyof typeof routes;
|
||||||
|
// | "/account/recovery"
|
||||||
|
// | "/account/verified"
|
||||||
|
// | "/signup"
|
||||||
|
// | "/login"
|
||||||
|
// | "/sessionexpired"
|
||||||
|
// | "/recover"
|
||||||
|
// | "/mfa/code"
|
||||||
|
// | "/mfa/select"
|
||||||
|
// | "default";
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
"/account/recovery": {
|
||||||
|
component: () => import("@notesnook/web/src/views/recovery"),
|
||||||
|
props: { route: "methods" }
|
||||||
|
},
|
||||||
|
"/account/verified": {
|
||||||
|
component: () => import("@notesnook/web/src/views/email-confirmed"),
|
||||||
|
props: {}
|
||||||
|
},
|
||||||
|
"/signup": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "signup" }
|
||||||
|
},
|
||||||
|
"/sessionexpired": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "sessionExpiry" }
|
||||||
|
},
|
||||||
|
"/login": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "login:email" }
|
||||||
|
},
|
||||||
|
"/login/password": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "login:email" }
|
||||||
|
},
|
||||||
|
"/recover": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "recover" }
|
||||||
|
},
|
||||||
|
"/login/mfa/code": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "login:email" }
|
||||||
|
},
|
||||||
|
"/login/mfa/select": {
|
||||||
|
component: () => import("@notesnook/web/src/views/auth"),
|
||||||
|
props: { route: "login:email" }
|
||||||
|
},
|
||||||
|
default: { component: () => import("./app"), props: null }
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const sessionExpiryExceptions: Routes[] = [
|
||||||
|
"/recover",
|
||||||
|
"/account/recovery",
|
||||||
|
"/sessionexpired",
|
||||||
|
"/login/mfa/code",
|
||||||
|
"/login/mfa/select",
|
||||||
|
"/login/password"
|
||||||
|
];
|
||||||
|
|
||||||
|
const serviceWorkerWhitelist: Routes[] = ["default"];
|
||||||
|
|
||||||
|
function getRoute(): RouteWithPath<AuthProps> | RouteWithPath {
|
||||||
|
const path = getCurrentPath() as Routes;
|
||||||
|
logger.info(`Getting route for path: ${path}`);
|
||||||
|
|
||||||
|
const signup = redirectToRegistration(path);
|
||||||
|
const sessionExpired = isSessionExpired(path);
|
||||||
|
const fallback = fallbackRoute();
|
||||||
|
const route = (
|
||||||
|
routes[path] ? { route: routes[path], path } : null
|
||||||
|
) as RouteWithPath<AuthProps> | null;
|
||||||
|
|
||||||
|
return signup || sessionExpired || route || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackRoute(): RouteWithPath {
|
||||||
|
return { route: routes.default, path: "default" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToRegistration(path: Routes): RouteWithPath<AuthProps> | null {
|
||||||
|
if (!IS_TESTING && !shouldSkipInitiation() && !routes[path]) {
|
||||||
|
window.history.replaceState({}, "", makeURL("/signup", getCurrentHash()));
|
||||||
|
return { route: routes["/signup"], path: "/signup" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSessionExpired(path: Routes): RouteWithPath<AuthProps> | null {
|
||||||
|
const isSessionExpired = Config.get("sessionExpired", false);
|
||||||
|
if (isSessionExpired && !sessionExpiryExceptions.includes(path)) {
|
||||||
|
logger.info(`User session has expired. Routing to /sessionexpired`);
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
makeURL("/sessionexpired", getCurrentHash())
|
||||||
|
);
|
||||||
|
return { route: routes["/sessionexpired"], path: "/sessionexpired" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderApp();
|
||||||
|
|
||||||
|
async function renderApp() {
|
||||||
|
await initalizeLogger();
|
||||||
|
const {
|
||||||
|
path,
|
||||||
|
route: { component, props }
|
||||||
|
} = getRoute();
|
||||||
|
|
||||||
|
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
|
||||||
|
if (IS_DESKTOP_APP) await loadDatabase("db");
|
||||||
|
|
||||||
|
logger.measure("app render");
|
||||||
|
|
||||||
|
const { default: Component } = await component();
|
||||||
|
render(
|
||||||
|
<Component route={props?.route || "login:email"} />,
|
||||||
|
document.getElementById("root"),
|
||||||
|
() => {
|
||||||
|
logger.measure("app render");
|
||||||
|
|
||||||
|
document.getElementById("splash")?.remove();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeServiceWorker() {
|
||||||
|
if (!IS_DESKTOP_APP) {
|
||||||
|
logger.info("Initializing service worker...");
|
||||||
|
const serviceWorker = await import("./service-worker-registration");
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.register({
|
||||||
|
onUpdate: async (registration: ServiceWorkerRegistration) => {
|
||||||
|
if (!registration.waiting) return;
|
||||||
|
const { getServiceWorkerVersion } = await import(
|
||||||
|
"@notesnook/web/src/utils/version"
|
||||||
|
);
|
||||||
|
const { formatted } = await getServiceWorkerVersion(
|
||||||
|
registration.waiting
|
||||||
|
);
|
||||||
|
AppEventManager.publish(AppEvents.updateDownloadCompleted, {
|
||||||
|
version: formatted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipInitiation() {
|
||||||
|
return localStorage.getItem("skipInitiation") || false;
|
||||||
|
}
|
||||||
21
apps/theme-builder/src/polyfills.ts
Normal file
21
apps/theme-builder/src/polyfills.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
window.Buffer = Buffer;
|
||||||
20
apps/theme-builder/src/react-app-env.d.ts
vendored
Normal file
20
apps/theme-builder/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="react-scripts" />
|
||||||
175
apps/theme-builder/src/service-worker-registration.ts
Normal file
175
apps/theme-builder/src/service-worker-registration.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://cra.link/PWA
|
||||||
|
|
||||||
|
type ServiceWorkerRegistrationConfig = {
|
||||||
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === "localhost" ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === "[::1]" ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config: ServiceWorkerRegistrationConfig) {
|
||||||
|
if (import.meta.env.PROD && "serviceWorker" in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
const swUrl = `${PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
"This web app is being served cache-first by a service " +
|
||||||
|
"worker. To learn more, visit https://cra.link/PWA"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(
|
||||||
|
swUrl: string,
|
||||||
|
config: ServiceWorkerRegistrationConfig
|
||||||
|
) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then((registration) => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === "installed") {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
"New content is available and will be used when all " +
|
||||||
|
"tabs for this page are closed. See https://cra.link/PWA."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log("Content is cached for offline use.");
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error during service worker registration:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(
|
||||||
|
swUrl: string,
|
||||||
|
config: ServiceWorkerRegistrationConfig
|
||||||
|
) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { "Service-Worker": "script" }
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(
|
||||||
|
"No internet connection found. App is running in offline mode."
|
||||||
|
);
|
||||||
|
if (config && config.onError) {
|
||||||
|
config.onError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
106
apps/theme-builder/src/service-worker.ts
Normal file
106
apps/theme-builder/src/service-worker.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-var */
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import { clientsClaim } from "workbox-core";
|
||||||
|
import { ExpirationPlugin } from "workbox-expiration";
|
||||||
|
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
|
||||||
|
import { registerRoute } from "workbox-routing";
|
||||||
|
import { StaleWhileRevalidate } from "workbox-strategies";
|
||||||
|
|
||||||
|
declare var self: ServiceWorkerGlobalScope & typeof globalThis;
|
||||||
|
|
||||||
|
clientsClaim();
|
||||||
|
|
||||||
|
const precacheRoutes = self.__WB_MANIFEST;
|
||||||
|
const filters = [/KaTeX/i, /hack/i, /code-lang-/i];
|
||||||
|
precacheAndRoute(
|
||||||
|
precacheRoutes.filter((route) => {
|
||||||
|
return filters.every(
|
||||||
|
(filter) => !filter.test(typeof route === "string" ? route : route.url)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up App Shell-style routing, so that all navigation requests
|
||||||
|
// are fulfilled with your index.html shell. Learn more at
|
||||||
|
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||||
|
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
|
||||||
|
registerRoute(
|
||||||
|
// Return false to exempt requests from being fulfilled by index.html.
|
||||||
|
({ request, url }) => {
|
||||||
|
// If this isn't a navigation, skip.
|
||||||
|
if (request.mode !== "navigate") {
|
||||||
|
return false;
|
||||||
|
} // If this is a URL that starts with /_, skip.
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/_")) {
|
||||||
|
return false;
|
||||||
|
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||||
|
|
||||||
|
if (url.pathname.match(fileExtensionRegexp)) {
|
||||||
|
return false;
|
||||||
|
} // Return true to signal that we want to use the handler.
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
createHandlerBoundToURL(PUBLIC_URL + "/index.html")
|
||||||
|
);
|
||||||
|
|
||||||
|
// An example runtime caching route for requests that aren't handled by the
|
||||||
|
// precache, in this case same-origin .png requests like those from in public/
|
||||||
|
registerRoute(
|
||||||
|
// Add in any other file extensions or routing criteria as needed.
|
||||||
|
({ url }) =>
|
||||||
|
url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: "images",
|
||||||
|
plugins: [
|
||||||
|
// Ensure that once this runtime cache reaches a maximum size the
|
||||||
|
// least-recently used images are removed.
|
||||||
|
new ExpirationPlugin({ maxEntries: 50 })
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// This allows the web app to trigger skipWaiting via
|
||||||
|
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||||
|
self.addEventListener("message", (event) => {
|
||||||
|
const { data } = event;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case "SKIP_WAITING":
|
||||||
|
self.skipWaiting();
|
||||||
|
break;
|
||||||
|
case "GET_VERSION":
|
||||||
|
{
|
||||||
|
if (!event.source) return;
|
||||||
|
event.source.postMessage({
|
||||||
|
type: data.type,
|
||||||
|
version: APP_VERSION,
|
||||||
|
hash: GIT_HASH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
12
apps/theme-builder/tsconfig.json
Normal file
12
apps/theme-builder/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"maxNodeModuleJsDepth": 10,
|
||||||
|
"noEmit": true,
|
||||||
|
"downlevelIteration": true
|
||||||
|
},
|
||||||
|
"include": ["src", "global.d.ts"]
|
||||||
|
}
|
||||||
172
apps/theme-builder/vite.config.ts
Normal file
172
apps/theme-builder/vite.config.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PluginOption, defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
import svgrPlugin from "vite-plugin-svgr";
|
||||||
|
import envCompatible from "vite-plugin-env-compatible";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
import { WEB_MANIFEST } from "./web-manifest";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { version } from "./package.json";
|
||||||
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
|
import { OutputPlugin } from "rollup";
|
||||||
|
|
||||||
|
const gitHash = (() => {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
} catch (e) {
|
||||||
|
return process.env.GIT_HASH || "gitless";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const appVersion = version.replaceAll(".", "");
|
||||||
|
const isTesting =
|
||||||
|
process.env.TEST === "true" || process.env.NODE_ENV === "development";
|
||||||
|
const isDesktop = process.env.PLATFORM === "desktop";
|
||||||
|
const isAnalyzing = process.env.ANALYZING === "true";
|
||||||
|
process.env.NN_BUILD_TIMESTAMP = isTesting ? "0" : `${Date.now()}`;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
envPrefix: "NN_",
|
||||||
|
root: "src/",
|
||||||
|
publicDir: "../node_modules/@notesnook/web/public",
|
||||||
|
build: {
|
||||||
|
target: isDesktop ? "esnext" : "modules",
|
||||||
|
outDir: "../build",
|
||||||
|
minify: "esbuild",
|
||||||
|
cssMinify: true,
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
plugins: [emitEditorStyles()],
|
||||||
|
assetFileNames: "assets/[name]-[hash:12][extname]",
|
||||||
|
chunkFileNames: "assets/[name]-[hash:12].js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
GIT_HASH: `"${gitHash}"`,
|
||||||
|
APP_VERSION: `"${appVersion}"`,
|
||||||
|
PUBLIC_URL: `"${process.env.PUBLIC_URL || ""}"`,
|
||||||
|
IS_DESKTOP_APP: isDesktop,
|
||||||
|
PLATFORM: `"${process.env.PLATFORM}"`,
|
||||||
|
IS_TESTING: process.env.TEST === "true",
|
||||||
|
IS_BETA: process.env.BETA === "true"
|
||||||
|
},
|
||||||
|
logLevel: process.env.NODE_ENV === "production" ? "warn" : "info",
|
||||||
|
resolve: {
|
||||||
|
dedupe: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"@mdi/js",
|
||||||
|
"@mdi/react",
|
||||||
|
"@emotion/react",
|
||||||
|
"katex"
|
||||||
|
],
|
||||||
|
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
find: /desktop-bridge/gm,
|
||||||
|
replacement: isDesktop
|
||||||
|
? "desktop-bridge/index.desktop"
|
||||||
|
: "desktop-bridge/index"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
format: "es",
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
inlineDynamicImports: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [autoprefixer()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...(isAnalyzing
|
||||||
|
? [
|
||||||
|
visualizer({
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
open: true
|
||||||
|
}) as PluginOption
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(isDesktop && process.env.NODE_ENV === "production"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
VitePWA({
|
||||||
|
strategies: "injectManifest",
|
||||||
|
minify: true,
|
||||||
|
manifest: WEB_MANIFEST,
|
||||||
|
injectRegister: null,
|
||||||
|
srcDir: "",
|
||||||
|
filename: "service-worker.ts"
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
react({
|
||||||
|
plugins: isTesting
|
||||||
|
? undefined
|
||||||
|
: [["swc-plugin-react-remove-properties", {}]]
|
||||||
|
}),
|
||||||
|
envCompatible({
|
||||||
|
prefix: "NN_",
|
||||||
|
mountedPath: "process.env"
|
||||||
|
}),
|
||||||
|
svgrPlugin({
|
||||||
|
svgrOptions: {
|
||||||
|
icon: true
|
||||||
|
// ...svgr options (https://react-svgr.com/docs/options/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function emitEditorStyles(): OutputPlugin {
|
||||||
|
return {
|
||||||
|
name: "rollup-plugin-emit-editor-styles",
|
||||||
|
generateBundle(options, bundle) {
|
||||||
|
for (const file in bundle) {
|
||||||
|
const chunk = bundle[file];
|
||||||
|
if (
|
||||||
|
chunk.type === "asset" &&
|
||||||
|
chunk.fileName.endsWith(".css") &&
|
||||||
|
typeof chunk.source === "string" &&
|
||||||
|
(chunk.source.includes("KaTeX_Fraktur-Bold-") ||
|
||||||
|
chunk.source.includes("Hack typeface"))
|
||||||
|
) {
|
||||||
|
this.emitFile({
|
||||||
|
type: "asset",
|
||||||
|
fileName: "assets/editor-styles.css",
|
||||||
|
name: "editor-styles.css",
|
||||||
|
source: chunk.source
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
135
apps/theme-builder/web-manifest.ts
Normal file
135
apps/theme-builder/web-manifest.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ManifestOptions } from "vite-plugin-pwa";
|
||||||
|
|
||||||
|
export const WEB_MANIFEST: Partial<ManifestOptions> = {
|
||||||
|
name: "Notesnook",
|
||||||
|
description:
|
||||||
|
"A fully open source & end-to-end encrypted note taking alternative to Evernote.",
|
||||||
|
short_name: "Notesnook",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: "New note",
|
||||||
|
url: "/#/notes/create",
|
||||||
|
description: "Create a new note",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New notebook",
|
||||||
|
url: "/#/notebooks/create",
|
||||||
|
description: "Create a new notebook",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "favicon-16x16.png",
|
||||||
|
sizes: "16x16",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "favicon-32x32.png",
|
||||||
|
sizes: "32x32",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "favicon-72x72.png",
|
||||||
|
sizes: "72x72",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/android-chrome-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
screenshots: [
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-1.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-2.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-3.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-4.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-5.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-6.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/screenshots/screenshot-7.jpg",
|
||||||
|
sizes: "1080x1920",
|
||||||
|
type: "image/jpeg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
related_applications: [
|
||||||
|
{
|
||||||
|
platform: "play",
|
||||||
|
url: "https://play.google.com/store/apps/details?id=com.streetwriters.notesnook",
|
||||||
|
id: "com.streetwriters.notesnook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: "itunes",
|
||||||
|
url: "https://apps.apple.com/us/app/notesnook-private-notes-app/id1544027013"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prefer_related_applications: true,
|
||||||
|
orientation: "any",
|
||||||
|
start_url: ".",
|
||||||
|
theme_color: "#01c352",
|
||||||
|
background_color: "#ffffff",
|
||||||
|
display: "standalone",
|
||||||
|
categories: ["productivity", "lifestyle", "education", "books"]
|
||||||
|
};
|
||||||
@@ -27,6 +27,8 @@ export type AccordionProps = {
|
|||||||
isClosed: boolean;
|
isClosed: boolean;
|
||||||
color?: SchemeColors;
|
color?: SchemeColors;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
|
buttonSx?: FlexProps["sx"];
|
||||||
|
titleSx?: FlexProps["sx"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Accordion(
|
export default function Accordion(
|
||||||
@@ -48,14 +50,15 @@ export default function Accordion(
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
bg: "var(--background-secondary)",
|
bg: "var(--background-secondary)",
|
||||||
p: 1,
|
p: 1,
|
||||||
borderRadius: "default"
|
borderRadius: "default",
|
||||||
|
...props.buttonSx
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsContentHidden((state) => !state);
|
setIsContentHidden((state) => !state);
|
||||||
}}
|
}}
|
||||||
data-test-id={testId}
|
data-test-id={testId}
|
||||||
>
|
>
|
||||||
<Text variant="subtitle" sx={{ color }}>
|
<Text variant="subtitle" sx={{ color, ...props.titleSx }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{isContentHidden ? (
|
{isContentHidden ? (
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"build:test:web": "nx build:test @notesnook/web",
|
"build:test:web": "nx build:test @notesnook/web",
|
||||||
"build:beta:web": "nx build:beta @notesnook/web",
|
"build:beta:web": "nx build:beta @notesnook/web",
|
||||||
"start:web": "nx start @notesnook/web",
|
"start:web": "nx start @notesnook/web",
|
||||||
|
"start:theme-builder": "nx start @notesnook/theme-builder",
|
||||||
|
"build:theme-builder": "nx build @notesnook/theme-builder",
|
||||||
"start:vericrypt": "nx start @notesnook/vericrypt",
|
"start:vericrypt": "nx start @notesnook/vericrypt",
|
||||||
"start:desktop": "nx start @notesnook/desktop",
|
"start:desktop": "nx start @notesnook/desktop",
|
||||||
"test:web": "nx test @notesnook/web",
|
"test:web": "nx test @notesnook/web",
|
||||||
|
|||||||
Reference in New Issue
Block a user