Repository: yonatanmgr/mathberet Branch: master Commit: 818c54c51df9 Files: 101 Total size: 294.4 KB Directory structure: gitextract_iwhcz0fy/ ├── .eslintrc ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .hintrc ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets/ │ ├── icons/ │ │ └── uicons.css │ └── webfonts/ │ └── OFL.txt ├── misc/ │ └── window/ │ ├── LICENSE │ ├── components/ │ │ ├── ControlButton.tsx │ │ ├── Titlebar.less │ │ ├── Titlebar.tsx │ │ ├── WindowControls.tsx │ │ └── WindowFrame.tsx │ ├── titlebarContext.ts │ ├── titlebarContextApi.ts │ ├── titlebarIPC.ts │ ├── titlebarMenus.ts │ └── windowPreload.ts ├── package.json ├── src/ │ ├── common/ │ │ ├── helpers.ts │ │ ├── i18n.ts │ │ ├── keybindings.ts │ │ ├── locals/ │ │ │ ├── ar.ts │ │ │ ├── en.ts │ │ │ ├── es.ts │ │ │ ├── fr.ts │ │ │ ├── he.ts │ │ │ ├── hi.ts │ │ │ ├── ru.ts │ │ │ └── zh.ts │ │ └── shortcuts.ts │ ├── main/ │ │ ├── Onboarding.js │ │ ├── app.ts │ │ └── appWindow.ts │ ├── renderer/ │ │ ├── app.html │ │ ├── appPreload.tsx │ │ ├── appRenderer.tsx │ │ ├── common/ │ │ │ └── types.ts │ │ ├── components/ │ │ │ ├── Application.scss │ │ │ ├── Application.tsx │ │ │ ├── CommandBar/ │ │ │ │ ├── CommandBar.scss │ │ │ │ └── CommandBar.tsx │ │ │ ├── FilesSidebar/ │ │ │ │ ├── FileSystem.scss │ │ │ │ ├── FileSystem.tsx │ │ │ │ ├── FileSystemHelpers.ts │ │ │ │ ├── FilesSidebar.scss │ │ │ │ ├── FilesSidebar.tsx │ │ │ │ ├── SidebarButton.tsx │ │ │ │ └── types.ts │ │ │ ├── Fonts.css │ │ │ ├── GeneralContext.tsx │ │ │ ├── Header/ │ │ │ │ ├── AddTag.tsx │ │ │ │ ├── FilePath.tsx │ │ │ │ ├── Header.scss │ │ │ │ ├── Header.tsx │ │ │ │ └── Tag.tsx │ │ │ ├── Icons.tsx │ │ │ ├── MathSidebar/ │ │ │ │ ├── ActionsGroup.tsx │ │ │ │ ├── MathSidebar.scss │ │ │ │ └── MathSidebar.tsx │ │ │ ├── Page/ │ │ │ │ ├── Grid/ │ │ │ │ │ ├── Blocks/ │ │ │ │ │ │ ├── Blocks.scss │ │ │ │ │ │ ├── DrawBlock.tsx │ │ │ │ │ │ ├── GraphBlock.tsx │ │ │ │ │ │ ├── MathBlock.tsx │ │ │ │ │ │ └── TextBlock.tsx │ │ │ │ │ ├── Grid.scss │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ └── GridElement.tsx │ │ │ │ ├── Page.scss │ │ │ │ ├── Page.tsx │ │ │ │ ├── PagePlaceholder.tsx │ │ │ │ └── ToolsPanel/ │ │ │ │ ├── Tool.tsx │ │ │ │ ├── ToolsPanel.scss │ │ │ │ └── ToolsPanel.tsx │ │ │ ├── Theme.scss │ │ │ └── common/ │ │ │ ├── Modals/ │ │ │ │ ├── ConfirmModal.scss │ │ │ │ ├── ConfirmModal.tsx │ │ │ │ ├── ErrorModal.scss │ │ │ │ └── ErrorModal.tsx │ │ │ ├── Notification.scss │ │ │ ├── Notification.tsx │ │ │ └── Shortcut.tsx │ │ └── hooks/ │ │ ├── useAddBlock.tsx │ │ ├── useDialog.tsx │ │ ├── useFileSaveLoad.tsx │ │ └── useSettings.tsx │ └── typings/ │ └── index.d.ts ├── tools/ │ ├── forge/ │ │ └── forge.config.js │ └── webpack/ │ ├── webpack.aliases.js │ ├── webpack.helpers.js │ ├── webpack.main.js │ ├── webpack.plugins.js │ ├── webpack.renderer.js │ └── webpack.rules.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:import/errors", "plugin:import/warnings" ], "parser": "@typescript-eslint/parser", "settings": { "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] }, "alias": { "map": [ ["@renderer", "./src/renderer"], ["@components", "./src/renderer/components"], ["@common", "./src/common"], ["@main", "./src/main"], ["@src", "./src"], ["@misc", "./misc"], ["@assets", "./assets"] ], "extensions": [".js", ".jsx", ".ts", ".tsx"] } }, "react": { "version": "latest" } }, "rules": { "react/prop-types": "off", "@typescript-eslint/no-var-requires": "off" } } ================================================ FILE: .gitattributes ================================================ *.scss linguist-detectable=false *.sass linguist-detectable=false ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows, macOS, Linux] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock .DS_Store # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Webpack .webpack/ # Electron-Forge out/ ================================================ FILE: .hintrc ================================================ { "extends": [ "development" ], "hints": { "axe/text-alternatives": [ "default", { "image-alt": "off" } ], "meta-viewport": "off", "no-inline-styles": "off", "axe/name-role-value": [ "default", { "button-name": "off" } ], "typescript-config/strict": "off" }, "browserslist": [ "defaults", "not ie 11", "not and_ff <= 107", "not firefox <= 109", "not ios_saf <= 16.2", "not safari <= 16.2" ] } ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "all", "jsxSingleQuote": true } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at Repository Discussions. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Mathberet Thank you for investing your time in contributing to our project! Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
## How to Contribute? You can contribute to our project by providing `features/bugfixes/improvements` related Pull Requests.
Just make sure to test all the changes you provide before submission anything into this project. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Mathberet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
mathberet logo

📝 Mathberet (Hebrew: מַתְבֶּרֶת) - A self-hosted digital mathematics notebook, written in React and built with Electron

🚧 In active development, open for contributions! 🚧

--- > Currently works in Hebrew, English, Arabic, Russian and Spanish, localized using [i18next](https://github.com/i18next/i18next). Still buggy. ## :white_check_mark: Features - [x] Block based drag-n-drop editor - [x] Text, Math, Graph and Drawing blocks are currently available - [x] LaTeX shortcuts and snippets - [x] Local files can be saved and loaded from the file system - [x] File tags (currently useless) - [x] Command bar (currently only used for user preferences) - [x] 6 color themes and light/dark theme - [ ] Customize LaTeX shortcuts - [ ] Shortcuts help menu - [ ] Searching from command bar - [ ] Adding points and polygons to graph blocks - [ ] Math memory sidebar (for variable assignments and quick functions) - [ ] Archive ## :camera_flash: Screenshots ### Hebrew ![image](https://user-images.githubusercontent.com/31913495/225077627-82fa032c-88e7-4e25-971f-98a37a436d40.jpg) ### English
Dark theme and purple accent color ![image](https://user-images.githubusercontent.com/31913495/225168731-13afd8f2-7e17-448d-a434-5b6bd1f43494.png)
Light theme and green accent color ![image](https://user-images.githubusercontent.com/31913495/225170025-65b7cde0-434d-4c66-8d9a-1c9237a92f3b.png)
Command bar (Ctrl+Shift+P) ![image](https://user-images.githubusercontent.com/31913495/225170120-e3dcdeb3-bdf2-4fa9-80dc-f2ebbfe2051b.png)
## :bricks: Built With - [ERWT](https://github.com/codesbiome/electron-react-webpack-typescript-2023): Electron + React apps boilerplate - [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout): Grid layout for the blocks - [slate](https://github.com/ianstormtaylor/slate): Text block component - [MathLive](https://cortexjs.io/mathlive/) ([react-math-view](https://github.com/arnog/react-mathlive)): Math block component - [Mafs](https://mafs.dev/): Graph block component - [tldraw](https://github.com/tldraw/tldraw): Drawing block component - [kbar](https://kbar.vercel.app/): Command bar - [react-complex-tree](https://rct.lukasbach.com/): Used for displaying the file system - [UIcons](https://github.com/freepik-company/flaticon-uicons): App icons ## :building_construction: Building from source 1. Clone the repository to a folder on your machine: ```bash git clone https://github.com/yonatanmgr/mathberet.git ``` 2. Run `npm install` in the project root folder 3. Run `npm start` in the project root folder ## :handshake: Contributing We welcome any positive contribution towards our project's growth! Whether you choose to work on a [listed feature](https://github.com/yonatanmgr/mathberet#white_check_mark-features) or create a new one, your help is appreciated. Simply submit a Pull Request after adding your code. To ensure a smooth process, please review our `CODE OF CONDUCT` and read the `CONTRIBUTING` guidelines for further details on submitting pull requests. Make sure to visit Mathberet's [project](https://github.com/users/yonatanmgr/projects/2) to view our roadmap and plans, and our [wiki](https://github.com/yonatanmgr/mathberet/wiki/%F0%9F%8F%A0-Home) to read the documentation! ## :balance_scale: Liscense This project is licensed under the MIT License - see the `LICENSE` file for details. ## :technologist: Contributors
Yonatan Magier
Yonatan Magier

🤔 💻 🚧 🌍 🎨
Erez Birenholz
Erez Birenholz

🧑‍🏫 💻 🚧 🌍
Nadav Magier
Nadav Magier

💻 🚧 🌍 🛡️
Ziv Nadel
Ziv Nadel

💻 🚧
================================================ FILE: assets/icons/uicons.css ================================================ @font-face{font-family:uicons-regular-rounded;src:url(../webfonts/uicons-regular-rounded-BG3GDTFR.eot) format("embedded-opentype"),url(../webfonts/uicons-regular-rounded-3OJX2N3K.woff2) format("woff2"),url(../webfonts/uicons-regular-rounded-ATYWIXGY.woff) format("woff")}i[class^=fi-rr-]:before,i[class*=" fi-rr-"]:before,span[class^=fi-rr-]:before,span[class*=fi-rr-]:before{font-family:uicons-regular-rounded!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fi-rr-0:before{content:"\f101"}.fi-rr-1:before{content:"\f102"}.fi-rr-2:before{content:"\f103"}.fi-rr-3:before{content:"\f104"}.fi-rr-4:before{content:"\f105"}.fi-rr-5:before{content:"\f106"}.fi-rr-6:before{content:"\f107"}.fi-rr-7:before{content:"\f108"}.fi-rr-8:before{content:"\f109"}.fi-rr-9:before{content:"\f10a"}.fi-rr-a:before{content:"\f10b"}.fi-rr-acorn:before{content:"\f10c"}.fi-rr-ad:before{content:"\f10d"}.fi-rr-add-document:before{content:"\f10e"}.fi-rr-add-folder:before{content:"\f10f"}.fi-rr-add:before{content:"\f110"}.fi-rr-address-book:before{content:"\f111"}.fi-rr-air-conditioner:before{content:"\f112"}.fi-rr-air-freshener:before{content:"\f113"}.fi-rr-alarm-clock:before{content:"\f114"}.fi-rr-alarm-exclamation:before{content:"\f115"}.fi-rr-alarm-plus:before{content:"\f116"}.fi-rr-alarm-snooze:before{content:"\f117"}.fi-rr-album-circle-plus:before{content:"\f118"}.fi-rr-album-circle-user:before{content:"\f119"}.fi-rr-album-collection:before{content:"\f11a"}.fi-rr-album:before{content:"\f11b"}.fi-rr-align-justify:before{content:"\f11c"}.fi-rr-align-left:before{content:"\f11d"}.fi-rr-ambulance:before{content:"\f11e"}.fi-rr-angle-circle-down:before{content:"\f11f"}.fi-rr-angle-circle-left:before{content:"\f120"}.fi-rr-angle-circle-right:before{content:"\f121"}.fi-rr-angle-circle-up:before{content:"\f122"}.fi-rr-angle-double-left:before{content:"\f123"}.fi-rr-angle-double-right:before{content:"\f124"}.fi-rr-angle-double-small-down:before{content:"\f125"}.fi-rr-angle-double-small-left:before{content:"\f126"}.fi-rr-angle-double-small-right:before{content:"\f127"}.fi-rr-angle-double-small-up:before{content:"\f128"}.fi-rr-angle-down:before{content:"\f129"}.fi-rr-angle-left:before{content:"\f12a"}.fi-rr-angle-right:before{content:"\f12b"}.fi-rr-angle-small-down:before{content:"\f12c"}.fi-rr-angle-small-left:before{content:"\f12d"}.fi-rr-angle-small-right:before{content:"\f12e"}.fi-rr-angle-small-up:before{content:"\f12f"}.fi-rr-angle-square-down:before{content:"\f130"}.fi-rr-angle-square-left:before{content:"\f131"}.fi-rr-angle-square-right:before{content:"\f132"}.fi-rr-angle-square-up:before{content:"\f133"}.fi-rr-angle-up:before{content:"\f134"}.fi-rr-angry:before{content:"\f135"}.fi-rr-animated-icon:before{content:"\f136"}.fi-rr-apartment:before{content:"\f137"}.fi-rr-api:before{content:"\f138"}.fi-rr-apple-crate:before{content:"\f139"}.fi-rr-apple-whole:before{content:"\f13a"}.fi-rr-apps-add:before{content:"\f13b"}.fi-rr-apps-delete:before{content:"\f13c"}.fi-rr-apps-sort:before{content:"\f13d"}.fi-rr-apps:before{content:"\f13e"}.fi-rr-archive:before{content:"\f13f"}.fi-rr-archway:before{content:"\f140"}.fi-rr-arrow-alt-circle-down:before{content:"\f141"}.fi-rr-arrow-alt-circle-left:before{content:"\f142"}.fi-rr-arrow-alt-circle-right:before{content:"\f143"}.fi-rr-arrow-alt-circle-up:before{content:"\f144"}.fi-rr-arrow-alt-down:before{content:"\f145"}.fi-rr-arrow-alt-from-bottom:before{content:"\f146"}.fi-rr-arrow-alt-from-left:before{content:"\f147"}.fi-rr-arrow-alt-from-right:before{content:"\f148"}.fi-rr-arrow-alt-from-top:before{content:"\f149"}.fi-rr-arrow-alt-left:before{content:"\f14a"}.fi-rr-arrow-alt-right:before{content:"\f14b"}.fi-rr-arrow-alt-square-down:before{content:"\f14c"}.fi-rr-arrow-alt-square-left:before{content:"\f14d"}.fi-rr-arrow-alt-square-right:before{content:"\f14e"}.fi-rr-arrow-alt-square-up:before{content:"\f14f"}.fi-rr-arrow-alt-to-bottom:before{content:"\f150"}.fi-rr-arrow-alt-to-left:before{content:"\f151"}.fi-rr-arrow-alt-to-right:before{content:"\f152"}.fi-rr-arrow-alt-to-top:before{content:"\f153"}.fi-rr-arrow-alt-up:before{content:"\f154"}.fi-rr-arrow-circle-down:before{content:"\f155"}.fi-rr-arrow-circle-left:before{content:"\f156"}.fi-rr-arrow-circle-right:before{content:"\f157"}.fi-rr-arrow-circle-up:before{content:"\f158"}.fi-rr-arrow-down-from-dotted-line:before{content:"\f159"}.fi-rr-arrow-down-small-big:before{content:"\f15a"}.fi-rr-arrow-down-to-dotted-line:before{content:"\f15b"}.fi-rr-arrow-down-to-square:before{content:"\f15c"}.fi-rr-arrow-down-triangle-square:before{content:"\f15d"}.fi-rr-arrow-down:before{content:"\f15e"}.fi-rr-arrow-from-bottom:before{content:"\f15f"}.fi-rr-arrow-from-left:before{content:"\f160"}.fi-rr-arrow-from-right:before{content:"\f161"}.fi-rr-arrow-from-top:before{content:"\f162"}.fi-rr-arrow-left-from-line:before{content:"\f163"}.fi-rr-arrow-left:before{content:"\f164"}.fi-rr-arrow-right-to-bracket:before{content:"\f165"}.fi-rr-arrow-right:before{content:"\f166"}.fi-rr-arrow-small-down:before{content:"\f167"}.fi-rr-arrow-small-left:before{content:"\f168"}.fi-rr-arrow-small-right:before{content:"\f169"}.fi-rr-arrow-small-up:before{content:"\f16a"}.fi-rr-arrow-square-down:before{content:"\f16b"}.fi-rr-arrow-square-left:before{content:"\f16c"}.fi-rr-arrow-square-right:before{content:"\f16d"}.fi-rr-arrow-square-up:before{content:"\f16e"}.fi-rr-arrow-to-bottom:before{content:"\f16f"}.fi-rr-arrow-to-left:before{content:"\f170"}.fi-rr-arrow-to-right:before{content:"\f171"}.fi-rr-arrow-to-top:before{content:"\f172"}.fi-rr-arrow-trend-down:before{content:"\f173"}.fi-rr-arrow-trend-up:before{content:"\f174"}.fi-rr-arrow-turn-down-left:before{content:"\f175"}.fi-rr-arrow-turn-down-right:before{content:"\f176"}.fi-rr-arrow-up-from-dotted-line:before{content:"\f177"}.fi-rr-arrow-up-from-square:before{content:"\f178"}.fi-rr-arrow-up-left-from-circle:before{content:"\f179"}.fi-rr-arrow-up-left:before{content:"\f17a"}.fi-rr-arrow-up-right-and-arrow-down-left-from-center:before{content:"\f17b"}.fi-rr-arrow-up-right-from-square:before{content:"\f17c"}.fi-rr-arrow-up-right:before{content:"\f17d"}.fi-rr-arrow-up-small-big:before{content:"\f17e"}.fi-rr-arrow-up-square-triangle:before{content:"\f17f"}.fi-rr-arrow-up-to-dotted-line:before{content:"\f180"}.fi-rr-arrow-up:before{content:"\f181"}.fi-rr-arrows-alt-h:before{content:"\f182"}.fi-rr-arrows-alt-v:before{content:"\f183"}.fi-rr-arrows-alt:before{content:"\f184"}.fi-rr-arrows-cross:before{content:"\f185"}.fi-rr-arrows-from-dotted-line:before{content:"\f186"}.fi-rr-arrows-from-line:before{content:"\f187"}.fi-rr-arrows-h-copy:before{content:"\f188"}.fi-rr-arrows-h:before{content:"\f189"}.fi-rr-arrows-repeat-1:before{content:"\f18a"}.fi-rr-arrows-repeat:before{content:"\f18b"}.fi-rr-arrows-retweet:before{content:"\f18c"}.fi-rr-arrows-to-dotted-line:before{content:"\f18d"}.fi-rr-arrows-to-line:before{content:"\f18e"}.fi-rr-arrows:before{content:"\f18f"}.fi-rr-assept-document:before{content:"\f190"}.fi-rr-assistive-listening-systems:before{content:"\f191"}.fi-rr-asterik:before{content:"\f192"}.fi-rr-at:before{content:"\f193"}.fi-rr-attribution-pen:before{content:"\f194"}.fi-rr-attribution-pencil:before{content:"\f195"}.fi-rr-aubergine:before{content:"\f196"}.fi-rr-audio-description-slash:before{content:"\f197"}.fi-rr-avocado:before{content:"\f198"}.fi-rr-b:before{content:"\f199"}.fi-rr-baby-carriage:before{content:"\f19a"}.fi-rr-background:before{content:"\f19b"}.fi-rr-backpack:before{content:"\f19c"}.fi-rr-bacon:before{content:"\f19d"}.fi-rr-badge-check:before{content:"\f19e"}.fi-rr-badge-dollar:before{content:"\f19f"}.fi-rr-badge-percent:before{content:"\f1a0"}.fi-rr-badge-sheriff:before{content:"\f1a1"}.fi-rr-badge:before{content:"\f1a2"}.fi-rr-bags-shopping:before{content:"\f1a3"}.fi-rr-bahai:before{content:"\f1a4"}.fi-rr-balance-scale-left:before{content:"\f1a5"}.fi-rr-balance-scale-right:before{content:"\f1a6"}.fi-rr-ball:before{content:"\f1a7"}.fi-rr-balloons:before{content:"\f1a8"}.fi-rr-ballot:before{content:"\f1a9"}.fi-rr-ban-bug:before{content:"\f1aa"}.fi-rr-ban:before{content:"\f1ab"}.fi-rr-band-aid:before{content:"\f1ac"}.fi-rr-bank:before{content:"\f1ad"}.fi-rr-barber-shop:before{content:"\f1ae"}.fi-rr-barcode-read:before{content:"\f1af"}.fi-rr-barcode-scan:before{content:"\f1b0"}.fi-rr-barcode:before{content:"\f1b1"}.fi-rr-bars-filter:before{content:"\f1b2"}.fi-rr-bars-progress:before{content:"\f1b3"}.fi-rr-bars-sort:before{content:"\f1b4"}.fi-rr-baseball-alt:before{content:"\f1b5"}.fi-rr-baseball:before{content:"\f1b6"}.fi-rr-basket:before{content:"\f1b7"}.fi-rr-basketball:before{content:"\f1b8"}.fi-rr-battery-bolt:before{content:"\f1b9"}.fi-rr-battery-empty:before{content:"\f1ba"}.fi-rr-battery-full:before{content:"\f1bb"}.fi-rr-battery-half:before{content:"\f1bc"}.fi-rr-battery-quarter:before{content:"\f1bd"}.fi-rr-battery-slash:before{content:"\f1be"}.fi-rr-battery-three-quarters:before{content:"\f1bf"}.fi-rr-beacon:before{content:"\f1c0"}.fi-rr-bed-alt:before{content:"\f1c1"}.fi-rr-bed:before{content:"\f1c2"}.fi-rr-beer:before{content:"\f1c3"}.fi-rr-bell-ring:before{content:"\f1c4"}.fi-rr-bell-school:before{content:"\f1c5"}.fi-rr-bell:before{content:"\f1c6"}.fi-rr-bells:before{content:"\f1c7"}.fi-rr-bench-tree:before{content:"\f1c8"}.fi-rr-betamax:before{content:"\f1c9"}.fi-rr-bike:before{content:"\f1ca"}.fi-rr-biking-mountain:before{content:"\f1cb"}.fi-rr-biking:before{content:"\f1cc"}.fi-rr-billiard:before{content:"\f1cd"}.fi-rr-binary-circle-check:before{content:"\f1ce"}.fi-rr-binary-lock:before{content:"\f1cf"}.fi-rr-binary-slash:before{content:"\f1d0"}.fi-rr-binary:before{content:"\f1d1"}.fi-rr-binoculars:before{content:"\f1d2"}.fi-rr-blinds-open:before{content:"\f1d3"}.fi-rr-blinds-raised:before{content:"\f1d4"}.fi-rr-blinds:before{content:"\f1d5"}.fi-rr-block-brick:before{content:"\f1d6"}.fi-rr-block:before{content:"\f1d7"}.fi-rr-blog-pencil:before{content:"\f1d8"}.fi-rr-blog-text:before{content:"\f1d9"}.fi-rr-bold:before{content:"\f1da"}.fi-rr-bolt:before{content:"\f1db"}.fi-rr-bomb:before{content:"\f1dc"}.fi-rr-bone:before{content:"\f1dd"}.fi-rr-book-alt:before{content:"\f1de"}.fi-rr-book-arrow-right:before{content:"\f1df"}.fi-rr-book-arrow-up:before{content:"\f1e0"}.fi-rr-book-atlas:before{content:"\f1e1"}.fi-rr-book-bookmark:before{content:"\f1e2"}.fi-rr-book-font:before{content:"\f1e3"}.fi-rr-book-open-cover:before{content:"\f1e4"}.fi-rr-book-open-reader:before{content:"\f1e5"}.fi-rr-book:before{content:"\f1e6"}.fi-rr-bookmark:before{content:"\f1e7"}.fi-rr-books:before{content:"\f1e8"}.fi-rr-border-all:before{content:"\f1e9"}.fi-rr-border-bottom:before{content:"\f1ea"}.fi-rr-border-center-h:before{content:"\f1eb"}.fi-rr-border-center-v:before{content:"\f1ec"}.fi-rr-border-inner:before{content:"\f1ed"}.fi-rr-border-left:before{content:"\f1ee"}.fi-rr-border-none:before{content:"\f1ef"}.fi-rr-border-outer:before{content:"\f1f0"}.fi-rr-border-right:before{content:"\f1f1"}.fi-rr-border-style-alt:before{content:"\f1f2"}.fi-rr-border-style:before{content:"\f1f3"}.fi-rr-border-top:before{content:"\f1f4"}.fi-rr-bottle:before{content:"\f1f5"}.fi-rr-bow-arrow:before{content:"\f1f6"}.fi-rr-bowling-ball:before{content:"\f1f7"}.fi-rr-bowling-pins:before{content:"\f1f8"}.fi-rr-bowling:before{content:"\f1f9"}.fi-rr-box-alt:before{content:"\f1fa"}.fi-rr-box-ballot:before{content:"\f1fb"}.fi-rr-box-check:before{content:"\f1fc"}.fi-rr-box-fragile:before{content:"\f1fd"}.fi-rr-box-open-full:before{content:"\f1fe"}.fi-rr-box-open:before{content:"\f1ff"}.fi-rr-box-tissue:before{content:"\f200"}.fi-rr-box-up:before{content:"\f201"}.fi-rr-box:before{content:"\f202"}.fi-rr-boxes:before{content:"\f203"}.fi-rr-boxing-glove:before{content:"\f204"}.fi-rr-bracket-curly-right:before{content:"\f205"}.fi-rr-bracket-curly:before{content:"\f206"}.fi-rr-bracket-round-right:before{content:"\f207"}.fi-rr-bracket-round:before{content:"\f208"}.fi-rr-bracket-square-right:before{content:"\f209"}.fi-rr-bracket-square:before{content:"\f20a"}.fi-rr-brackets-curly:before{content:"\f20b"}.fi-rr-brackets-round:before{content:"\f20c"}.fi-rr-brackets-square:before{content:"\f20d"}.fi-rr-braille:before{content:"\f20e"}.fi-rr-brain-circuit:before{content:"\f20f"}.fi-rr-brain:before{content:"\f210"}.fi-rr-bread-loaf:before{content:"\f211"}.fi-rr-bread-slice:before{content:"\f212"}.fi-rr-bread:before{content:"\f213"}.fi-rr-briefcase:before{content:"\f214"}.fi-rr-brightness-low:before{content:"\f215"}.fi-rr-brightness:before{content:"\f216"}.fi-rr-bring-forward:before{content:"\f217"}.fi-rr-bring-front:before{content:"\f218"}.fi-rr-broccoli:before{content:"\f219"}.fi-rr-broken-image:before{content:"\f21a"}.fi-rr-broom:before{content:"\f21b"}.fi-rr-browser:before{content:"\f21c"}.fi-rr-browsers:before{content:"\f21d"}.fi-rr-brush:before{content:"\f21e"}.fi-rr-bug-slash:before{content:"\f21f"}.fi-rr-bug:before{content:"\f220"}.fi-rr-building:before{content:"\f221"}.fi-rr-bulb:before{content:"\f222"}.fi-rr-bullet:before{content:"\f223"}.fi-rr-bullseye-pointer:before{content:"\f224"}.fi-rr-bullseye:before{content:"\f225"}.fi-rr-burger-alt:before{content:"\f226"}.fi-rr-burger-fries:before{content:"\f227"}.fi-rr-burger-glass:before{content:"\f228"}.fi-rr-burrito:before{content:"\f229"}.fi-rr-bus-alt:before{content:"\f22a"}.fi-rr-bus:before{content:"\f22b"}.fi-rr-business-time:before{content:"\f22c"}.fi-rr-butterfly:before{content:"\f22d"}.fi-rr-c:before{content:"\f22e"}.fi-rr-cabin:before{content:"\f22f"}.fi-rr-cake-birthday:before{content:"\f230"}.fi-rr-cake-wedding:before{content:"\f231"}.fi-rr-calculator:before{content:"\f232"}.fi-rr-calendar-check:before{content:"\f233"}.fi-rr-calendar-clock:before{content:"\f234"}.fi-rr-calendar-exclamation:before{content:"\f235"}.fi-rr-calendar-lines-pen:before{content:"\f236"}.fi-rr-calendar-lines:before{content:"\f237"}.fi-rr-calendar-minus:before{content:"\f238"}.fi-rr-calendar-pen:before{content:"\f239"}.fi-rr-calendar-plus:before{content:"\f23a"}.fi-rr-calendar:before{content:"\f23b"}.fi-rr-calendars:before{content:"\f23c"}.fi-rr-call-history:before{content:"\f23d"}.fi-rr-call-incoming:before{content:"\f23e"}.fi-rr-call-missed:before{content:"\f23f"}.fi-rr-call-outgoing:before{content:"\f240"}.fi-rr-camcorder:before{content:"\f241"}.fi-rr-camera:before{content:"\f242"}.fi-rr-camping:before{content:"\f243"}.fi-rr-candy-alt:before{content:"\f244"}.fi-rr-candy-cane:before{content:"\f245"}.fi-rr-candy-corn:before{content:"\f246"}.fi-rr-candy:before{content:"\f247"}.fi-rr-canned-food:before{content:"\f248"}.fi-rr-car-alt:before{content:"\f249"}.fi-rr-car-battery:before{content:"\f24a"}.fi-rr-car-bolt:before{content:"\f24b"}.fi-rr-car-building:before{content:"\f24c"}.fi-rr-car-bump:before{content:"\f24d"}.fi-rr-car-bus:before{content:"\f24e"}.fi-rr-car-circle-bolt:before{content:"\f24f"}.fi-rr-car-crash:before{content:"\f250"}.fi-rr-car-garage:before{content:"\f251"}.fi-rr-car-mechanic:before{content:"\f252"}.fi-rr-car-side-bolt:before{content:"\f253"}.fi-rr-car-side:before{content:"\f254"}.fi-rr-car-tilt:before{content:"\f255"}.fi-rr-car-wash:before{content:"\f256"}.fi-rr-car:before{content:"\f257"}.fi-rr-caravan-alt:before{content:"\f258"}.fi-rr-caravan:before{content:"\f259"}.fi-rr-caret-circle-down:before{content:"\f25a"}.fi-rr-caret-circle-right:before{content:"\f25b"}.fi-rr-caret-circle-up:before{content:"\f25c"}.fi-rr-caret-down:before{content:"\f25d"}.fi-rr-caret-left:before{content:"\f25e"}.fi-rr-caret-quare-up:before{content:"\f25f"}.fi-rr-caret-right:before{content:"\f260"}.fi-rr-caret-square-down:before{content:"\f261"}.fi-rr-caret-square-left_1:before{content:"\f262"}.fi-rr-caret-square-left:before{content:"\f263"}.fi-rr-caret-square-right:before{content:"\f264"}.fi-rr-caret-up:before{content:"\f265"}.fi-rr-carrot:before{content:"\f266"}.fi-rr-cars:before{content:"\f267"}.fi-rr-cash-register:before{content:"\f268"}.fi-rr-cassette-tape:before{content:"\f269"}.fi-rr-cassette-vhs:before{content:"\f26a"}.fi-rr-castle:before{content:"\f26b"}.fi-rr-chair-office:before{content:"\f26c"}.fi-rr-chair:before{content:"\f26d"}.fi-rr-charging-station:before{content:"\f26e"}.fi-rr-chart-area:before{content:"\f26f"}.fi-rr-chart-bullet:before{content:"\f270"}.fi-rr-chart-candlestick:before{content:"\f271"}.fi-rr-chart-connected:before{content:"\f272"}.fi-rr-chart-gantt:before{content:"\f273"}.fi-rr-chart-histogram:before{content:"\f274"}.fi-rr-chart-line-up:before{content:"\f275"}.fi-rr-chart-mixed:before{content:"\f276"}.fi-rr-chart-network:before{content:"\f277"}.fi-rr-chart-pie-alt:before{content:"\f278"}.fi-rr-chart-pie:before{content:"\f279"}.fi-rr-chart-pyramid:before{content:"\f27a"}.fi-rr-chart-radar:before{content:"\f27b"}.fi-rr-chart-scatter-3d:before{content:"\f27c"}.fi-rr-chart-scatter-bubble:before{content:"\f27d"}.fi-rr-chart-scatter:before{content:"\f27e"}.fi-rr-chart-set-theory:before{content:"\f27f"}.fi-rr-chart-tree:before{content:"\f280"}.fi-rr-chart-waterfall:before{content:"\f281"}.fi-rr-chat-arrow-down:before{content:"\f282"}.fi-rr-chat-arrow-grow:before{content:"\f283"}.fi-rr-check:before{content:"\f284"}.fi-rr-checkbox:before{content:"\f285"}.fi-rr-cheese-alt:before{content:"\f286"}.fi-rr-cheese:before{content:"\f287"}.fi-rr-cherry:before{content:"\f288"}.fi-rr-chess-bishop:before{content:"\f289"}.fi-rr-chess-board:before{content:"\f28a"}.fi-rr-chess-clock-alt:before{content:"\f28b"}.fi-rr-chess-clock:before{content:"\f28c"}.fi-rr-chess-king-alt:before{content:"\f28d"}.fi-rr-chess-king:before{content:"\f28e"}.fi-rr-chess-knight-alt:before{content:"\f28f"}.fi-rr-chess-knight:before{content:"\f290"}.fi-rr-chess-pawn-alt:before{content:"\f291"}.fi-rr-chess-piece:before{content:"\f292"}.fi-rr-chess-queen-alt:before{content:"\f293"}.fi-rr-chess-queen:before{content:"\f294"}.fi-rr-chess-rook-alt:before{content:"\f295"}.fi-rr-chess-rook:before{content:"\f296"}.fi-rr-chess:before{content:"\f297"}.fi-rr-chevron-double-down:before{content:"\f298"}.fi-rr-chevron-double-up:before{content:"\f299"}.fi-rr-child-head:before{content:"\f29a"}.fi-rr-chocolate:before{content:"\f29b"}.fi-rr-circle-0:before{content:"\f29c"}.fi-rr-circle-1:before{content:"\f29d"}.fi-rr-circle-2:before{content:"\f29e"}.fi-rr-circle-3:before{content:"\f29f"}.fi-rr-circle-4:before{content:"\f2a0"}.fi-rr-circle-5:before{content:"\f2a1"}.fi-rr-circle-6:before{content:"\f2a2"}.fi-rr-circle-7:before{content:"\f2a3"}.fi-rr-circle-8:before{content:"\f2a4"}.fi-rr-circle-9:before{content:"\f2a5"}.fi-rr-circle-dashed:before{content:"\f2a6"}.fi-rr-circle-envelope:before{content:"\f2a7"}.fi-rr-circle-half-stroke:before{content:"\f2a8"}.fi-rr-circle-heart:before{content:"\f2a9"}.fi-rr-circle-phone-flip:before{content:"\f2aa"}.fi-rr-circle-phone-hangup:before{content:"\f2ab"}.fi-rr-circle-phone:before{content:"\f2ac"}.fi-rr-circle-small:before{content:"\f2ad"}.fi-rr-circle:before{content:"\f2ae"}.fi-rr-city:before{content:"\f2af"}.fi-rr-clip:before{content:"\f2b0"}.fi-rr-clipboard-list-check:before{content:"\f2b1"}.fi-rr-clipboard-list:before{content:"\f2b2"}.fi-rr-clock-eight-thirty:before{content:"\f2b3"}.fi-rr-clock-eleven-thirty:before{content:"\f2b4"}.fi-rr-clock-eleven:before{content:"\f2b5"}.fi-rr-clock-five-thirty:before{content:"\f2b6"}.fi-rr-clock-five:before{content:"\f2b7"}.fi-rr-clock-four-thirty:before{content:"\f2b8"}.fi-rr-clock-nine-thirty:before{content:"\f2b9"}.fi-rr-clock-nine:before{content:"\f2ba"}.fi-rr-clock-one-thirty:before{content:"\f2bb"}.fi-rr-clock-one:before{content:"\f2bc"}.fi-rr-clock-seven-thirty:before{content:"\f2bd"}.fi-rr-clock-seven:before{content:"\f2be"}.fi-rr-clock-six-thirty:before{content:"\f2bf"}.fi-rr-clock-six:before{content:"\f2c0"}.fi-rr-clock-ten-thirty:before{content:"\f2c1"}.fi-rr-clock-ten:before{content:"\f2c2"}.fi-rr-clock-three-thirty:before{content:"\f2c3"}.fi-rr-clock-three:before{content:"\f2c4"}.fi-rr-clock-twelve-thirty:before{content:"\f2c5"}.fi-rr-clock-twelve:before{content:"\f2c6"}.fi-rr-clock-two-thirty:before{content:"\f2c7"}.fi-rr-clock-two:before{content:"\f2c8"}.fi-rr-clock:before{content:"\f2c9"}.fi-rr-closed-captioning-slash:before{content:"\f2ca"}.fi-rr-cloud-check:before{content:"\f2cb"}.fi-rr-cloud-disabled:before{content:"\f2cc"}.fi-rr-cloud-download-alt:before{content:"\f2cd"}.fi-rr-cloud-download:before{content:"\f2ce"}.fi-rr-cloud-drizzle:before{content:"\f2cf"}.fi-rr-cloud-hail-mixed:before{content:"\f2d0"}.fi-rr-cloud-hail:before{content:"\f2d1"}.fi-rr-cloud-moon-rain:before{content:"\f2d2"}.fi-rr-cloud-moon:before{content:"\f2d3"}.fi-rr-cloud-rain:before{content:"\f2d4"}.fi-rr-cloud-rainbow:before{content:"\f2d5"}.fi-rr-cloud-share:before{content:"\f2d6"}.fi-rr-cloud-showers-heavy:before{content:"\f2d7"}.fi-rr-cloud-showers:before{content:"\f2d8"}.fi-rr-cloud-sleet:before{content:"\f2d9"}.fi-rr-cloud-snow:before{content:"\f2da"}.fi-rr-cloud-sun-rain:before{content:"\f2db"}.fi-rr-cloud-sun:before{content:"\f2dc"}.fi-rr-cloud-upload-alt:before{content:"\f2dd"}.fi-rr-cloud-upload:before{content:"\f2de"}.fi-rr-cloud:before{content:"\f2df"}.fi-rr-clouds-moon:before{content:"\f2e0"}.fi-rr-clouds-sun:before{content:"\f2e1"}.fi-rr-clouds:before{content:"\f2e2"}.fi-rr-club:before{content:"\f2e3"}.fi-rr-cocktail-alt:before{content:"\f2e4"}.fi-rr-cocktail:before{content:"\f2e5"}.fi-rr-code-branch:before{content:"\f2e6"}.fi-rr-code-commit:before{content:"\f2e7"}.fi-rr-code-compare:before{content:"\f2e8"}.fi-rr-code-fork:before{content:"\f2e9"}.fi-rr-code-merge:before{content:"\f2ea"}.fi-rr-code-pull-request-closed:before{content:"\f2eb"}.fi-rr-code-pull-request-draft:before{content:"\f2ec"}.fi-rr-code-pull-request:before{content:"\f2ed"}.fi-rr-code-simple:before{content:"\f2ee"}.fi-rr-coffee-pot:before{content:"\f2ef"}.fi-rr-coffee:before{content:"\f2f0"}.fi-rr-coin:before{content:"\f2f1"}.fi-rr-coins:before{content:"\f2f2"}.fi-rr-comet:before{content:"\f2f3"}.fi-rr-comment-alt-middle-top:before{content:"\f2f4"}.fi-rr-comment-alt-middle:before{content:"\f2f5"}.fi-rr-comment-alt:before{content:"\f2f6"}.fi-rr-comment-arrow-down:before{content:"\f2f7"}.fi-rr-comment-arrow-up-right:before{content:"\f2f8"}.fi-rr-comment-arrow-up:before{content:"\f2f9"}.fi-rr-comment-check:before{content:"\f2fa"}.fi-rr-comment-code:before{content:"\f2fb"}.fi-rr-comment-dollar:before{content:"\f2fc"}.fi-rr-comment-exclamation:before{content:"\f2fd"}.fi-rr-comment-heart:before{content:"\f2fe"}.fi-rr-comment-image:before{content:"\f2ff"}.fi-rr-comment-info:before{content:"\f300"}.fi-rr-comment-pen:before{content:"\f301"}.fi-rr-comment-question:before{content:"\f302"}.fi-rr-comment-quote:before{content:"\f303"}.fi-rr-comment-slash:before{content:"\f304"}.fi-rr-comment-sms:before{content:"\f305"}.fi-rr-comment-text:before{content:"\f306"}.fi-rr-comment-user:before{content:"\f307"}.fi-rr-comment-xmark:before{content:"\f308"}.fi-rr-comment:before{content:"\f309"}.fi-rr-comments-dollar:before{content:"\f30a"}.fi-rr-comments-question-check:before{content:"\f30b"}.fi-rr-comments-question:before{content:"\f30c"}.fi-rr-comments:before{content:"\f30d"}.fi-rr-compress-alt:before{content:"\f30e"}.fi-rr-compress:before{content:"\f30f"}.fi-rr-computer:before{content:"\f310"}.fi-rr-concierge-bell:before{content:"\f311"}.fi-rr-confetti:before{content:"\f312"}.fi-rr-cookie:before{content:"\f313"}.fi-rr-copy-alt:before{content:"\f314"}.fi-rr-copy-image:before{content:"\f315"}.fi-rr-copy:before{content:"\f316"}.fi-rr-copyright:before{content:"\f317"}.fi-rr-corn:before{content:"\f318"}.fi-rr-cow:before{content:"\f319"}.fi-rr-cowbell-more:before{content:"\f31a"}.fi-rr-cowbell:before{content:"\f31b"}.fi-rr-cream:before{content:"\f31c"}.fi-rr-credit-card:before{content:"\f31d"}.fi-rr-cricket:before{content:"\f31e"}.fi-rr-croissant:before{content:"\f31f"}.fi-rr-cross-circle:before{content:"\f320"}.fi-rr-cross-religion:before{content:"\f321"}.fi-rr-cross-small:before{content:"\f322"}.fi-rr-cross:before{content:"\f323"}.fi-rr-crown:before{content:"\f324"}.fi-rr-crystal-ball:before{content:"\f325"}.fi-rr-cube:before{content:"\f326"}.fi-rr-cubes:before{content:"\f327"}.fi-rr-cupcake:before{content:"\f328"}.fi-rr-curling:before{content:"\f329"}.fi-rr-cursor-finger:before{content:"\f32a"}.fi-rr-cursor-plus:before{content:"\f32b"}.fi-rr-cursor-text-alt:before{content:"\f32c"}.fi-rr-cursor-text:before{content:"\f32d"}.fi-rr-cursor:before{content:"\f32e"}.fi-rr-d:before{content:"\f32f"}.fi-rr-dart:before{content:"\f330"}.fi-rr-dashboard:before{content:"\f331"}.fi-rr-data-transfer:before{content:"\f332"}.fi-rr-database:before{content:"\f333"}.fi-rr-delete-document:before{content:"\f334"}.fi-rr-delete-user:before{content:"\f335"}.fi-rr-delete:before{content:"\f336"}.fi-rr-democrat:before{content:"\f337"}.fi-rr-desktop-wallpaper:before{content:"\f338"}.fi-rr-devices:before{content:"\f339"}.fi-rr-dewpoint:before{content:"\f33a"}.fi-rr-diagram-cells:before{content:"\f33b"}.fi-rr-diagram-lean-canvas:before{content:"\f33c"}.fi-rr-diagram-nested:before{content:"\f33d"}.fi-rr-diagram-next:before{content:"\f33e"}.fi-rr-diagram-predecessor:before{content:"\f33f"}.fi-rr-diagram-previous:before{content:"\f340"}.fi-rr-diagram-project:before{content:"\f341"}.fi-rr-diagram-sankey:before{content:"\f342"}.fi-rr-diagram-subtask:before{content:"\f343"}.fi-rr-diagram-successor:before{content:"\f344"}.fi-rr-diamond-turn-right:before{content:"\f345"}.fi-rr-diamond:before{content:"\f346"}.fi-rr-dice-alt:before{content:"\f347"}.fi-rr-dice-d10:before{content:"\f348"}.fi-rr-dice-d12:before{content:"\f349"}.fi-rr-dice-d20:before{content:"\f34a"}.fi-rr-dice-d4:before{content:"\f34b"}.fi-rr-dice-d6:before{content:"\f34c"}.fi-rr-dice-d8:before{content:"\f34d"}.fi-rr-dice-four:before{content:"\f34e"}.fi-rr-dice-one:before{content:"\f34f"}.fi-rr-dice-six:before{content:"\f350"}.fi-rr-dice-three:before{content:"\f351"}.fi-rr-dice-two:before{content:"\f352"}.fi-rr-dice:before{content:"\f353"}.fi-rr-diploma:before{content:"\f354"}.fi-rr-disco-ball:before{content:"\f355"}.fi-rr-disk:before{content:"\f356"}.fi-rr-display-code:before{content:"\f357"}.fi-rr-distribute-spacing-horizontal:before{content:"\f358"}.fi-rr-distribute-spacing-vertical:before{content:"\f359"}.fi-rr-dizzy:before{content:"\f35a"}.fi-rr-doctor:before{content:"\f35b"}.fi-rr-document-signed:before{content:"\f35c"}.fi-rr-document:before{content:"\f35d"}.fi-rr-dollar:before{content:"\f35e"}.fi-rr-donate:before{content:"\f35f"}.fi-rr-door-closed:before{content:"\f360"}.fi-rr-door-open:before{content:"\f361"}.fi-rr-down-left-and-up-right-to-center:before{content:"\f362"}.fi-rr-down-left:before{content:"\f363"}.fi-rr-down-right:before{content:"\f364"}.fi-rr-down-to-line:before{content:"\f365"}.fi-rr-down:before{content:"\f366"}.fi-rr-download:before{content:"\f367"}.fi-rr-drafting-compass:before{content:"\f368"}.fi-rr-dreidel:before{content:"\f369"}.fi-rr-drink-alt:before{content:"\f36a"}.fi-rr-drumstick:before{content:"\f36b"}.fi-rr-dungeon:before{content:"\f36c"}.fi-rr-duplicate:before{content:"\f36d"}.fi-rr-e-learning:before{content:"\f36e"}.fi-rr-e:before{content:"\f36f"}.fi-rr-ear-deaf:before{content:"\f370"}.fi-rr-earnings:before{content:"\f371"}.fi-rr-eclipse-alt:before{content:"\f372"}.fi-rr-eclipse:before{content:"\f373"}.fi-rr-edit-alt:before{content:"\f374"}.fi-rr-edit:before{content:"\f375"}.fi-rr-egg-fried:before{content:"\f376"}.fi-rr-egg:before{content:"\f377"}.fi-rr-engine-warning:before{content:"\f378"}.fi-rr-enter:before{content:"\f379"}.fi-rr-envelope-ban:before{content:"\f37a"}.fi-rr-envelope-bulk:before{content:"\f37b"}.fi-rr-envelope-download:before{content:"\f37c"}.fi-rr-envelope-marker:before{content:"\f37d"}.fi-rr-envelope-open-dollar:before{content:"\f37e"}.fi-rr-envelope-open-text:before{content:"\f37f"}.fi-rr-envelope-open:before{content:"\f380"}.fi-rr-envelope-plus:before{content:"\f381"}.fi-rr-envelope:before{content:"\f382"}.fi-rr-equality:before{content:"\f383"}.fi-rr-euro:before{content:"\f384"}.fi-rr-exchange-alt:before{content:"\f385"}.fi-rr-exchange:before{content:"\f386"}.fi-rr-exclamation:before{content:"\f387"}.fi-rr-exit:before{content:"\f388"}.fi-rr-expand-arrows-alt:before{content:"\f389"}.fi-rr-expand-arrows:before{content:"\f38a"}.fi-rr-expand:before{content:"\f38b"}.fi-rr-eye-crossed:before{content:"\f38c"}.fi-rr-eye-dropper:before{content:"\f38d"}.fi-rr-eye:before{content:"\f38e"}.fi-rr-f:before{content:"\f38f"}.fi-rr-face-awesome:before{content:"\f390"}.fi-rr-face-head-bandage:before{content:"\f391"}.fi-rr-farm:before{content:"\f392"}.fi-rr-feather:before{content:"\f393"}.fi-rr-fence:before{content:"\f394"}.fi-rr-ferris-wheel:before{content:"\f395"}.fi-rr-fighter-jet:before{content:"\f396"}.fi-rr-file-ai:before{content:"\f397"}.fi-rr-file-binary:before{content:"\f398"}.fi-rr-file-chart-line:before{content:"\f399"}.fi-rr-file-chart-pie:before{content:"\f39a"}.fi-rr-file-code:before{content:"\f39b"}.fi-rr-file-eps:before{content:"\f39c"}.fi-rr-file-excel:before{content:"\f39d"}.fi-rr-file-export:before{content:"\f39e"}.fi-rr-file-invoice-dollar:before{content:"\f39f"}.fi-rr-file-invoice:before{content:"\f3a0"}.fi-rr-file-pdf:before{content:"\f3a1"}.fi-rr-file-powerpoint:before{content:"\f3a2"}.fi-rr-file-psd:before{content:"\f3a3"}.fi-rr-file-spreadsheet:before{content:"\f3a4"}.fi-rr-file-video:before{content:"\f3a5"}.fi-rr-file-word:before{content:"\f3a6"}.fi-rr-file:before{content:"\f3a7"}.fi-rr-fill:before{content:"\f3a8"}.fi-rr-film-slash:before{content:"\f3a9"}.fi-rr-film:before{content:"\f3aa"}.fi-rr-filter-slash:before{content:"\f3ab"}.fi-rr-filter:before{content:"\f3ac"}.fi-rr-filters:before{content:"\f3ad"}.fi-rr-fingerprint:before{content:"\f3ae"}.fi-rr-fire-flame-curved:before{content:"\f3af"}.fi-rr-fire-smoke:before{content:"\f3b0"}.fi-rr-fish:before{content:"\f3b1"}.fi-rr-fishing-rod:before{content:"\f3b2"}.fi-rr-flag-alt:before{content:"\f3b3"}.fi-rr-flag-checkered:before{content:"\f3b4"}.fi-rr-flag-usa:before{content:"\f3b5"}.fi-rr-flag:before{content:"\f3b6"}.fi-rr-flame:before{content:"\f3b7"}.fi-rr-flip-horizontal:before{content:"\f3b8"}.fi-rr-flower-bouquet:before{content:"\f3b9"}.fi-rr-flower-tulip:before{content:"\f3ba"}.fi-rr-flower:before{content:"\f3bb"}.fi-rr-flushed:before{content:"\f3bc"}.fi-rr-fog:before{content:"\f3bd"}.fi-rr-folder-download:before{content:"\f3be"}.fi-rr-folder-minus:before{content:"\f3bf"}.fi-rr-folder-times:before{content:"\f3c0"}.fi-rr-folder-tree:before{content:"\f3c1"}.fi-rr-folder-upload:before{content:"\f3c2"}.fi-rr-folder:before{content:"\f3c3"}.fi-rr-folders:before{content:"\f3c4"}.fi-rr-follow-folder:before{content:"\f3c5"}.fi-rr-followcollection:before{content:"\f3c6"}.fi-rr-following:before{content:"\f3c7"}.fi-rr-football:before{content:"\f3c8"}.fi-rr-fork:before{content:"\f3c9"}.fi-rr-form:before{content:"\f3ca"}.fi-rr-fort:before{content:"\f3cb"}.fi-rr-forward:before{content:"\f3cc"}.fi-rr-fox:before{content:"\f3cd"}.fi-rr-french-fries:before{content:"\f3ce"}.fi-rr-frown:before{content:"\f3cf"}.fi-rr-ftp:before{content:"\f3d0"}.fi-rr-funnel-dollar:before{content:"\f3d1"}.fi-rr-g:before{content:"\f3d2"}.fi-rr-gallery-thumbnails:before{content:"\f3d3"}.fi-rr-gallery:before{content:"\f3d4"}.fi-rr-game-board-alt:before{content:"\f3d5"}.fi-rr-gamepad:before{content:"\f3d6"}.fi-rr-garage-car:before{content:"\f3d7"}.fi-rr-garage-open:before{content:"\f3d8"}.fi-rr-garage:before{content:"\f3d9"}.fi-rr-garlic:before{content:"\f3da"}.fi-rr-gas-pump-alt:before{content:"\f3db"}.fi-rr-gas-pump-slash:before{content:"\f3dc"}.fi-rr-gas-pump:before{content:"\f3dd"}.fi-rr-gavel:before{content:"\f3de"}.fi-rr-gears:before{content:"\f3df"}.fi-rr-gem:before{content:"\f3e0"}.fi-rr-gif:before{content:"\f3e1"}.fi-rr-gift-card:before{content:"\f3e2"}.fi-rr-gift:before{content:"\f3e3"}.fi-rr-gifts:before{content:"\f3e4"}.fi-rr-gingerbread-man:before{content:"\f3e5"}.fi-rr-glass-cheers:before{content:"\f3e6"}.fi-rr-glass:before{content:"\f3e7"}.fi-rr-glasses:before{content:"\f3e8"}.fi-rr-globe-alt:before{content:"\f3e9"}.fi-rr-globe:before{content:"\f3ea"}.fi-rr-golf-ball:before{content:"\f3eb"}.fi-rr-golf:before{content:"\f3ec"}.fi-rr-graduation-cap:before{content:"\f3ed"}.fi-rr-grape:before{content:"\f3ee"}.fi-rr-graphic-style:before{content:"\f3ef"}.fi-rr-graphic-tablet:before{content:"\f3f0"}.fi-rr-grid-alt:before{content:"\f3f1"}.fi-rr-grid-dividers:before{content:"\f3f2"}.fi-rr-grid:before{content:"\f3f3"}.fi-rr-grill:before{content:"\f3f4"}.fi-rr-grimace:before{content:"\f3f5"}.fi-rr-grin-alt:before{content:"\f3f6"}.fi-rr-grin-beam-sweat:before{content:"\f3f7"}.fi-rr-grin-beam:before{content:"\f3f8"}.fi-rr-grin-hearts:before{content:"\f3f9"}.fi-rr-grin-squint-tears:before{content:"\f3fa"}.fi-rr-grin-squint:before{content:"\f3fb"}.fi-rr-grin-stars:before{content:"\f3fc"}.fi-rr-grin-tears:before{content:"\f3fd"}.fi-rr-grin-tongue-squint:before{content:"\f3fe"}.fi-rr-grin-tongue-wink:before{content:"\f3ff"}.fi-rr-grin-tongue:before{content:"\f400"}.fi-rr-grin-wink:before{content:"\f401"}.fi-rr-grin:before{content:"\f402"}.fi-rr-guitar:before{content:"\f403"}.fi-rr-gym:before{content:"\f404"}.fi-rr-h:before{content:"\f405"}.fi-rr-hamburger-soda:before{content:"\f406"}.fi-rr-hamburger:before{content:"\f407"}.fi-rr-hammer-crash:before{content:"\f408"}.fi-rr-hammer-war:before{content:"\f409"}.fi-rr-hammer:before{content:"\f40a"}.fi-rr-hand-holding-box:before{content:"\f40b"}.fi-rr-hand-holding-heart:before{content:"\f40c"}.fi-rr-hand-holding-seeding:before{content:"\f40d"}.fi-rr-hand-horns:before{content:"\f40e"}.fi-rr-hand-lizard:before{content:"\f40f"}.fi-rr-hand-peace:before{content:"\f410"}.fi-rr-hand:before{content:"\f411"}.fi-rr-hands-clapping:before{content:"\f412"}.fi-rr-handshake-angle:before{content:"\f413"}.fi-rr-handshake:before{content:"\f414"}.fi-rr-hastag:before{content:"\f415"}.fi-rr-hat-birthday:before{content:"\f416"}.fi-rr-hat-chef:before{content:"\f417"}.fi-rr-hdd:before{content:"\f418"}.fi-rr-head-side-brain:before{content:"\f419"}.fi-rr-head-side-cough-slash:before{content:"\f41a"}.fi-rr-head-side-cough:before{content:"\f41b"}.fi-rr-head-side-heart:before{content:"\f41c"}.fi-rr-head-side-mask:before{content:"\f41d"}.fi-rr-head-side-medical:before{content:"\f41e"}.fi-rr-head-side-thinking:before{content:"\f41f"}.fi-rr-head-side-virus:before{content:"\f420"}.fi-rr-head-side:before{content:"\f421"}.fi-rr-head-vr:before{content:"\f422"}.fi-rr-headphones:before{content:"\f423"}.fi-rr-headset:before{content:"\f424"}.fi-rr-heart-arrow:before{content:"\f425"}.fi-rr-heart-crack:before{content:"\f426"}.fi-rr-heart:before{content:"\f427"}.fi-rr-heat:before{content:"\f428"}.fi-rr-helicopter-side:before{content:"\f429"}.fi-rr-highlighter-line:before{content:"\f42a"}.fi-rr-highlighter:before{content:"\f42b"}.fi-rr-hiking:before{content:"\f42c"}.fi-rr-hockey-puck:before{content:"\f42d"}.fi-rr-hockey-sticks:before{content:"\f42e"}.fi-rr-home-location-alt:before{content:"\f42f"}.fi-rr-home-location:before{content:"\f430"}.fi-rr-home:before{content:"\f431"}.fi-rr-horizontal-rule:before{content:"\f432"}.fi-rr-hot-tub:before{content:"\f433"}.fi-rr-hotdog:before{content:"\f434"}.fi-rr-hotel:before{content:"\f435"}.fi-rr-hourglass-end:before{content:"\f436"}.fi-rr-hourglass:before{content:"\f437"}.fi-rr-house-blank:before{content:"\f438"}.fi-rr-house-building:before{content:"\f439"}.fi-rr-house-chimney-blank:before{content:"\f43a"}.fi-rr-house-chimney-crack:before{content:"\f43b"}.fi-rr-house-chimney-medical:before{content:"\f43c"}.fi-rr-house-chimney-window:before{content:"\f43d"}.fi-rr-house-chimney:before{content:"\f43e"}.fi-rr-house-crack:before{content:"\f43f"}.fi-rr-house-flood:before{content:"\f440"}.fi-rr-house-medical:before{content:"\f441"}.fi-rr-house-tree:before{content:"\f442"}.fi-rr-house-turret:before{content:"\f443"}.fi-rr-house-window:before{content:"\f444"}.fi-rr-hryvnia:before{content:"\f445"}.fi-rr-humidity:before{content:"\f446"}.fi-rr-hurricane:before{content:"\f447"}.fi-rr-i:before{content:"\f448"}.fi-rr-ice-cream:before{content:"\f449"}.fi-rr-ice-skate:before{content:"\f44a"}.fi-rr-icon-star:before{content:"\f44b"}.fi-rr-id-badge:before{content:"\f44c"}.fi-rr-igloo:before{content:"\f44d"}.fi-rr-images:before{content:"\f44e"}.fi-rr-inbox-in:before{content:"\f44f"}.fi-rr-inbox-out:before{content:"\f450"}.fi-rr-inbox:before{content:"\f451"}.fi-rr-incognito:before{content:"\f452"}.fi-rr-indent:before{content:"\f453"}.fi-rr-industry-windows:before{content:"\f454"}.fi-rr-infinity:before{content:"\f455"}.fi-rr-info:before{content:"\f456"}.fi-rr-interactive:before{content:"\f457"}.fi-rr-interlining:before{content:"\f458"}.fi-rr-interrogation:before{content:"\f459"}.fi-rr-island-tropical:before{content:"\f45a"}.fi-rr-italic:before{content:"\f45b"}.fi-rr-j:before{content:"\f45c"}.fi-rr-jam:before{content:"\f45d"}.fi-rr-jpg:before{content:"\f45e"}.fi-rr-jug:before{content:"\f45f"}.fi-rr-k:before{content:"\f460"}.fi-rr-kerning:before{content:"\f461"}.fi-rr-key-skeleton-left-right:before{content:"\f462"}.fi-rr-key:before{content:"\f463"}.fi-rr-keyboard-brightness-low:before{content:"\f464"}.fi-rr-keyboard-brightness:before{content:"\f465"}.fi-rr-keyboard:before{content:"\f466"}.fi-rr-keynote:before{content:"\f467"}.fi-rr-kiss-beam:before{content:"\f468"}.fi-rr-kiss-wink-heart:before{content:"\f469"}.fi-rr-kiss:before{content:"\f46a"}.fi-rr-kite:before{content:"\f46b"}.fi-rr-knife:before{content:"\f46c"}.fi-rr-l:before{content:"\f46d"}.fi-rr-label:before{content:"\f46e"}.fi-rr-landmark-alt:before{content:"\f46f"}.fi-rr-laptop-code:before{content:"\f470"}.fi-rr-laptop-mobile:before{content:"\f471"}.fi-rr-laptop:before{content:"\f472"}.fi-rr-lasso-sparkles:before{content:"\f473"}.fi-rr-lasso:before{content:"\f474"}.fi-rr-laugh-beam:before{content:"\f475"}.fi-rr-laugh-squint:before{content:"\f476"}.fi-rr-laugh-wink:before{content:"\f477"}.fi-rr-laugh:before{content:"\f478"}.fi-rr-layer-minus:before{content:"\f479"}.fi-rr-layer-plus:before{content:"\f47a"}.fi-rr-layers:before{content:"\f47b"}.fi-rr-layout-fluid:before{content:"\f47c"}.fi-rr-leaf:before{content:"\f47d"}.fi-rr-lemon:before{content:"\f47e"}.fi-rr-letter-case:before{content:"\f47f"}.fi-rr-lettuce:before{content:"\f480"}.fi-rr-level-down-alt:before{content:"\f481"}.fi-rr-level-down:before{content:"\f482"}.fi-rr-level-up-alt:before{content:"\f483"}.fi-rr-level-up:before{content:"\f484"}.fi-rr-license:before{content:"\f485"}.fi-rr-life-ring:before{content:"\f486"}.fi-rr-light-switch-off:before{content:"\f487"}.fi-rr-light-switch-on:before{content:"\f488"}.fi-rr-light-switch:before{content:"\f489"}.fi-rr-lightbulb-dollar:before{content:"\f48a"}.fi-rr-line-width:before{content:"\f48b"}.fi-rr-link-alt:before{content:"\f48c"}.fi-rr-link-horizontal-slash:before{content:"\f48d"}.fi-rr-link-horizontal:before{content:"\f48e"}.fi-rr-link-slash-alt:before{content:"\f48f"}.fi-rr-link-slash:before{content:"\f490"}.fi-rr-link:before{content:"\f491"}.fi-rr-lipstick:before{content:"\f492"}.fi-rr-lira-sign:before{content:"\f493"}.fi-rr-list-check:before{content:"\f494"}.fi-rr-list:before{content:"\f495"}.fi-rr-loading:before{content:"\f496"}.fi-rr-location-alt:before{content:"\f497"}.fi-rr-location-crosshairs-slash:before{content:"\f498"}.fi-rr-location-crosshairs:before{content:"\f499"}.fi-rr-location-dot-slash:before{content:"\f49a"}.fi-rr-lock-alt:before{content:"\f49b"}.fi-rr-lock:before{content:"\f49c"}.fi-rr-luchador:before{content:"\f49d"}.fi-rr-luggage-cart:before{content:"\f49e"}.fi-rr-luggage-rolling:before{content:"\f49f"}.fi-rr-m:before{content:"\f4a0"}.fi-rr-magic-wand:before{content:"\f4a1"}.fi-rr-mailbox:before{content:"\f4a2"}.fi-rr-makeup-brush:before{content:"\f4a3"}.fi-rr-man-head:before{content:"\f4a4"}.fi-rr-map-marker-check:before{content:"\f4a5"}.fi-rr-map-marker-cross:before{content:"\f4a6"}.fi-rr-map-marker-edit:before{content:"\f4a7"}.fi-rr-map-marker-home:before{content:"\f4a8"}.fi-rr-map-marker-minus:before{content:"\f4a9"}.fi-rr-map-marker-plus:before{content:"\f4aa"}.fi-rr-map-marker-question:before{content:"\f4ab"}.fi-rr-map-marker-slash:before{content:"\f4ac"}.fi-rr-map-marker-smile:before{content:"\f4ad"}.fi-rr-map-marker:before{content:"\f4ae"}.fi-rr-map-pin:before{content:"\f4af"}.fi-rr-map:before{content:"\f4b0"}.fi-rr-marker-time:before{content:"\f4b1"}.fi-rr-marker:before{content:"\f4b2"}.fi-rr-mars-double:before{content:"\f4b3"}.fi-rr-mars:before{content:"\f4b4"}.fi-rr-mask-carnival:before{content:"\f4b5"}.fi-rr-mask:before{content:"\f4b6"}.fi-rr-medicine:before{content:"\f4b7"}.fi-rr-megaphone:before{content:"\f4b8"}.fi-rr-meh-blank:before{content:"\f4b9"}.fi-rr-meh-rolling-eyes:before{content:"\f4ba"}.fi-rr-meh:before{content:"\f4bb"}.fi-rr-melon:before{content:"\f4bc"}.fi-rr-memory:before{content:"\f4bd"}.fi-rr-menu-burger:before{content:"\f4be"}.fi-rr-menu-dots-vertical:before{content:"\f4bf"}.fi-rr-menu-dots:before{content:"\f4c0"}.fi-rr-message-code:before{content:"\f4c1"}.fi-rr-meteor:before{content:"\f4c2"}.fi-rr-microchip:before{content:"\f4c3"}.fi-rr-microphone-alt:before{content:"\f4c4"}.fi-rr-microphone:before{content:"\f4c5"}.fi-rr-mind-share:before{content:"\f4c6"}.fi-rr-minus-small:before{content:"\f4c7"}.fi-rr-minus:before{content:"\f4c8"}.fi-rr-mobile-button:before{content:"\f4c9"}.fi-rr-mobile-notch:before{content:"\f4ca"}.fi-rr-mobile:before{content:"\f4cb"}.fi-rr-mockup:before{content:"\f4cc"}.fi-rr-mode-landscape:before{content:"\f4cd"}.fi-rr-mode-portrait:before{content:"\f4ce"}.fi-rr-money-bill-wave-alt:before{content:"\f4cf"}.fi-rr-money-bill-wave:before{content:"\f4d0"}.fi-rr-money-check-edit-alt:before{content:"\f4d1"}.fi-rr-money-check-edit:before{content:"\f4d2"}.fi-rr-money-check:before{content:"\f4d3"}.fi-rr-money:before{content:"\f4d4"}.fi-rr-monument:before{content:"\f4d5"}.fi-rr-moon-stars:before{content:"\f4d6"}.fi-rr-moon:before{content:"\f4d7"}.fi-rr-motorcycle:before{content:"\f4d8"}.fi-rr-mountains:before{content:"\f4d9"}.fi-rr-mouse:before{content:"\f4da"}.fi-rr-move-to-folder-2:before{content:"\f4db"}.fi-rr-move-to-folder:before{content:"\f4dc"}.fi-rr-mug-alt:before{content:"\f4dd"}.fi-rr-mug-hot-alt:before{content:"\f4de"}.fi-rr-mug-hot:before{content:"\f4df"}.fi-rr-mug-tea:before{content:"\f4e0"}.fi-rr-mug:before{content:"\f4e1"}.fi-rr-mushroom:before{content:"\f4e2"}.fi-rr-music-alt:before{content:"\f4e3"}.fi-rr-music-file:before{content:"\f4e4"}.fi-rr-music:before{content:"\f4e5"}.fi-rr-n:before{content:"\f4e6"}.fi-rr-navigation:before{content:"\f4e7"}.fi-rr-network-cloud:before{content:"\f4e8"}.fi-rr-network:before{content:"\f4e9"}.fi-rr-no-people:before{content:"\f4ea"}.fi-rr-noodles:before{content:"\f4eb"}.fi-rr-notdef:before{content:"\f4ec"}.fi-rr-notebook:before{content:"\f4ed"}.fi-rr-o:before{content:"\f4ee"}.fi-rr-object-exclude:before{content:"\f4ef"}.fi-rr-object-intersect:before{content:"\f4f0"}.fi-rr-object-subtract:before{content:"\f4f1"}.fi-rr-object-union:before{content:"\f4f2"}.fi-rr-octagon:before{content:"\f4f3"}.fi-rr-oil-can:before{content:"\f4f4"}.fi-rr-oil-temp:before{content:"\f4f5"}.fi-rr-olive-oil:before{content:"\f4f6"}.fi-rr-olives:before{content:"\f4f7"}.fi-rr-onion:before{content:"\f4f8"}.fi-rr-opacity:before{content:"\f4f9"}.fi-rr-overline:before{content:"\f4fa"}.fi-rr-p:before{content:"\f4fb"}.fi-rr-package:before{content:"\f4fc"}.fi-rr-page-break:before{content:"\f4fd"}.fi-rr-paint-brush:before{content:"\f4fe"}.fi-rr-paint-roller:before{content:"\f4ff"}.fi-rr-palette:before{content:"\f500"}.fi-rr-pan:before{content:"\f501"}.fi-rr-paper-plane:before{content:"\f502"}.fi-rr-parking-circle-slash:before{content:"\f503"}.fi-rr-parking-circle:before{content:"\f504"}.fi-rr-parking-slash:before{content:"\f505"}.fi-rr-parking:before{content:"\f506"}.fi-rr-passport:before{content:"\f507"}.fi-rr-password:before{content:"\f508"}.fi-rr-paste:before{content:"\f509"}.fi-rr-pattern:before{content:"\f50a"}.fi-rr-pause:before{content:"\f50b"}.fi-rr-paw:before{content:"\f50c"}.fi-rr-peach:before{content:"\f50d"}.fi-rr-pencil-ruler:before{content:"\f50e"}.fi-rr-pencil:before{content:"\f50f"}.fi-rr-pennant:before{content:"\f510"}.fi-rr-people-arrows-left-right:before{content:"\f511"}.fi-rr-people-carry-box:before{content:"\f512"}.fi-rr-people-dress:before{content:"\f513"}.fi-rr-people-pants:before{content:"\f514"}.fi-rr-people-poll:before{content:"\f515"}.fi-rr-people:before{content:"\f516"}.fi-rr-pepper-hot:before{content:"\f517"}.fi-rr-pepper:before{content:"\f518"}.fi-rr-percentage:before{content:"\f519"}.fi-rr-person-walking-with-cane:before{content:"\f51a"}.fi-rr-pharmacy:before{content:"\f51b"}.fi-rr-phone-call:before{content:"\f51c"}.fi-rr-phone-cross:before{content:"\f51d"}.fi-rr-phone-office:before{content:"\f51e"}.fi-rr-phone-pause:before{content:"\f51f"}.fi-rr-phone-slash:before{content:"\f520"}.fi-rr-photo-film-music:before{content:"\f521"}.fi-rr-photo-video:before{content:"\f522"}.fi-rr-physics:before{content:"\f523"}.fi-rr-picnic:before{content:"\f524"}.fi-rr-picpeople-filled:before{content:"\f525"}.fi-rr-picpeople:before{content:"\f526"}.fi-rr-picture:before{content:"\f527"}.fi-rr-pie:before{content:"\f528"}.fi-rr-piece:before{content:"\f529"}.fi-rr-piggy-bank:before{content:"\f52a"}.fi-rr-pineapple:before{content:"\f52b"}.fi-rr-ping-pong:before{content:"\f52c"}.fi-rr-pizza-slice:before{content:"\f52d"}.fi-rr-plane-alt:before{content:"\f52e"}.fi-rr-plane-arrival:before{content:"\f52f"}.fi-rr-plane-departure:before{content:"\f530"}.fi-rr-plane-prop:before{content:"\f531"}.fi-rr-plane-tail:before{content:"\f532"}.fi-rr-plane:before{content:"\f533"}.fi-rr-plate:before{content:"\f534"}.fi-rr-play-alt:before{content:"\f535"}.fi-rr-play-pause:before{content:"\f536"}.fi-rr-play:before{content:"\f537"}.fi-rr-playing-cards:before{content:"\f538"}.fi-rr-plus-small:before{content:"\f539"}.fi-rr-plus:before{content:"\f53a"}.fi-rr-podium-star:before{content:"\f53b"}.fi-rr-podium:before{content:"\f53c"}.fi-rr-poker-chip:before{content:"\f53d"}.fi-rr-poo:before{content:"\f53e"}.fi-rr-popcorn:before{content:"\f53f"}.fi-rr-portrait:before{content:"\f540"}.fi-rr-pot:before{content:"\f541"}.fi-rr-pound:before{content:"\f542"}.fi-rr-power:before{content:"\f543"}.fi-rr-presentation:before{content:"\f544"}.fi-rr-print:before{content:"\f545"}.fi-rr-projector:before{content:"\f546"}.fi-rr-protractor:before{content:"\f547"}.fi-rr-pulse:before{content:"\f548"}.fi-rr-pumpkin:before{content:"\f549"}.fi-rr-puzzle:before{content:"\f54a"}.fi-rr-pyramid:before{content:"\f54b"}.fi-rr-q:before{content:"\f54c"}.fi-rr-qrcode:before{content:"\f54d"}.fi-rr-question-square:before{content:"\f54e"}.fi-rr-question:before{content:"\f54f"}.fi-rr-quote-right:before{content:"\f550"}.fi-rr-r:before{content:"\f551"}.fi-rr-racquet:before{content:"\f552"}.fi-rr-radish:before{content:"\f553"}.fi-rr-rainbow:before{content:"\f554"}.fi-rr-raindrops:before{content:"\f555"}.fi-rr-ramp-loading:before{content:"\f556"}.fi-rr-rec:before{content:"\f557"}.fi-rr-receipt:before{content:"\f558"}.fi-rr-record-vinyl:before{content:"\f559"}.fi-rr-rectabgle-vertical:before{content:"\f55a"}.fi-rr-rectangle-barcode:before{content:"\f55b"}.fi-rr-rectangle-code:before{content:"\f55c"}.fi-rr-rectangle-horizontal:before{content:"\f55d"}.fi-rr-rectangle-panoramic:before{content:"\f55e"}.fi-rr-rectangle-xmark:before{content:"\f55f"}.fi-rr-recycle:before{content:"\f560"}.fi-rr-redo-alt:before{content:"\f561"}.fi-rr-redo:before{content:"\f562"}.fi-rr-reflect-horizontal:before{content:"\f563"}.fi-rr-reflect-vertical:before{content:"\f564"}.fi-rr-reflect:before{content:"\f565"}.fi-rr-refresh:before{content:"\f566"}.fi-rr-registered:before{content:"\f567"}.fi-rr-remove-folder:before{content:"\f568"}.fi-rr-remove-user:before{content:"\f569"}.fi-rr-replace:before{content:"\f56a"}.fi-rr-reply-all:before{content:"\f56b"}.fi-rr-republican:before{content:"\f56c"}.fi-rr-resize:before{content:"\f56d"}.fi-rr-resources:before{content:"\f56e"}.fi-rr-restaurant:before{content:"\f56f"}.fi-rr-rewind:before{content:"\f570"}.fi-rr-rhombus:before{content:"\f571"}.fi-rr-rings-wedding:before{content:"\f572"}.fi-rr-road:before{content:"\f573"}.fi-rr-rocket-lunch:before{content:"\f574"}.fi-rr-rocket:before{content:"\f575"}.fi-rr-roller-coaster:before{content:"\f576"}.fi-rr-room-service:before{content:"\f577"}.fi-rr-rotate-left:before{content:"\f578"}.fi-rr-rotate-right:before{content:"\f579"}.fi-rr-route-highway:before{content:"\f57a"}.fi-rr-route-interstate:before{content:"\f57b"}.fi-rr-route:before{content:"\f57c"}.fi-rr-ruble-sign:before{content:"\f57d"}.fi-rr-rugby:before{content:"\f57e"}.fi-rr-ruler-combined:before{content:"\f57f"}.fi-rr-ruler-horizontal:before{content:"\f580"}.fi-rr-ruler-triangle:before{content:"\f581"}.fi-rr-ruler-vertical:before{content:"\f582"}.fi-rr-running:before{content:"\f583"}.fi-rr-rupee-sign:before{content:"\f584"}.fi-rr-rv:before{content:"\f585"}.fi-rr-s:before{content:"\f586"}.fi-rr-sack-dollar:before{content:"\f587"}.fi-rr-sack:before{content:"\f588"}.fi-rr-sad-cry:before{content:"\f589"}.fi-rr-sad-tear:before{content:"\f58a"}.fi-rr-sad:before{content:"\f58b"}.fi-rr-salad:before{content:"\f58c"}.fi-rr-salt-pepper:before{content:"\f58d"}.fi-rr-sandwich:before{content:"\f58e"}.fi-rr-sauce:before{content:"\f58f"}.fi-rr-sausage:before{content:"\f590"}.fi-rr-scale:before{content:"\f591"}.fi-rr-school-bus:before{content:"\f592"}.fi-rr-school:before{content:"\f593"}.fi-rr-scissors:before{content:"\f594"}.fi-rr-screen:before{content:"\f595"}.fi-rr-search-alt:before{content:"\f596"}.fi-rr-search-dollar:before{content:"\f597"}.fi-rr-search-heart:before{content:"\f598"}.fi-rr-search-location:before{content:"\f599"}.fi-rr-search:before{content:"\f59a"}.fi-rr-seat-airline:before{content:"\f59b"}.fi-rr-security:before{content:"\f59c"}.fi-rr-sensor-alert:before{content:"\f59d"}.fi-rr-sensor-fire:before{content:"\f59e"}.fi-rr-sensor-on:before{content:"\f59f"}.fi-rr-sensor-smoke:before{content:"\f5a0"}.fi-rr-sensor:before{content:"\f5a1"}.fi-rr-settings-sliders:before{content:"\f5a2"}.fi-rr-settings:before{content:"\f5a3"}.fi-rr-share:before{content:"\f5a4"}.fi-rr-shekel-sign:before{content:"\f5a5"}.fi-rr-shield-check:before{content:"\f5a6"}.fi-rr-shield-exclamation:before{content:"\f5a7"}.fi-rr-shield-interrogation:before{content:"\f5a8"}.fi-rr-shield-plus:before{content:"\f5a9"}.fi-rr-shield:before{content:"\f5aa"}.fi-rr-ship-side:before{content:"\f5ab"}.fi-rr-ship:before{content:"\f5ac"}.fi-rr-shoe-prints:before{content:"\f5ad"}.fi-rr-shop-lock:before{content:"\f5ae"}.fi-rr-shop:before{content:"\f5af"}.fi-rr-shopping-bag-add:before{content:"\f5b0"}.fi-rr-shopping-bag:before{content:"\f5b1"}.fi-rr-shopping-basket:before{content:"\f5b2"}.fi-rr-shopping-cart-add:before{content:"\f5b3"}.fi-rr-shopping-cart-check:before{content:"\f5b4"}.fi-rr-shopping-cart:before{content:"\f5b5"}.fi-rr-shrimp:before{content:"\f5b6"}.fi-rr-shuffle:before{content:"\f5b7"}.fi-rr-shuttle-van:before{content:"\f5b8"}.fi-rr-shuttlecock:before{content:"\f5b9"}.fi-rr-sidebar-flip:before{content:"\f5ba"}.fi-rr-sidebar:before{content:"\f5bb"}.fi-rr-sign-hanging:before{content:"\f5bc"}.fi-rr-sign-in-alt:before{content:"\f5bd"}.fi-rr-sign-out-alt:before{content:"\f5be"}.fi-rr-signal-alt-1:before{content:"\f5bf"}.fi-rr-signal-alt-2:before{content:"\f5c0"}.fi-rr-signal-alt:before{content:"\f5c1"}.fi-rr-signal-stream-slash:before{content:"\f5c2"}.fi-rr-signal-stream:before{content:"\f5c3"}.fi-rr-sitemap:before{content:"\f5c4"}.fi-rr-skateboard:before{content:"\f5c5"}.fi-rr-skating:before{content:"\f5c6"}.fi-rr-skewer:before{content:"\f5c7"}.fi-rr-ski-jump:before{content:"\f5c8"}.fi-rr-ski-lift:before{content:"\f5c9"}.fi-rr-skiing-nordic:before{content:"\f5ca"}.fi-rr-skiing:before{content:"\f5cb"}.fi-rr-slash:before{content:"\f5cc"}.fi-rr-sledding:before{content:"\f5cd"}.fi-rr-sleigh:before{content:"\f5ce"}.fi-rr-smartphone:before{content:"\f5cf"}.fi-rr-smile-beam:before{content:"\f5d0"}.fi-rr-smile-wink:before{content:"\f5d1"}.fi-rr-smile:before{content:"\f5d2"}.fi-rr-smiley-comment-alt:before{content:"\f5d3"}.fi-rr-smog:before{content:"\f5d4"}.fi-rr-smoke:before{content:"\f5d5"}.fi-rr-snow-blowing:before{content:"\f5d6"}.fi-rr-snowboarding:before{content:"\f5d7"}.fi-rr-snowflake:before{content:"\f5d8"}.fi-rr-snowflakes:before{content:"\f5d9"}.fi-rr-snowmobile:before{content:"\f5da"}.fi-rr-snowplow:before{content:"\f5db"}.fi-rr-soap:before{content:"\f5dc"}.fi-rr-social-network:before{content:"\f5dd"}.fi-rr-sort-alpha-down-alt:before{content:"\f5de"}.fi-rr-sort-alpha-down:before{content:"\f5df"}.fi-rr-sort-alpha-up-alt:before{content:"\f5e0"}.fi-rr-sort-alpha-up:before{content:"\f5e1"}.fi-rr-sort-alt:before{content:"\f5e2"}.fi-rr-sort-amount-down-alt:before{content:"\f5e3"}.fi-rr-sort-amount-down:before{content:"\f5e4"}.fi-rr-sort-amount-up-alt:before{content:"\f5e5"}.fi-rr-sort-amount-up:before{content:"\f5e6"}.fi-rr-sort-down:before{content:"\f5e7"}.fi-rr-sort-numeric-down-alt:before{content:"\f5e8"}.fi-rr-sort-numeric-down:before{content:"\f5e9"}.fi-rr-sort-shapes-down:before{content:"\f5ea"}.fi-rr-sort-shapes-up:before{content:"\f5eb"}.fi-rr-sort-size-down:before{content:"\f5ec"}.fi-rr-sort-size-up:before{content:"\f5ed"}.fi-rr-sort:before{content:"\f5ee"}.fi-rr-soup:before{content:"\f5ef"}.fi-rr-spa:before{content:"\f5f0"}.fi-rr-space-shuttle:before{content:"\f5f1"}.fi-rr-spade:before{content:"\f5f2"}.fi-rr-sparkles:before{content:"\f5f3"}.fi-rr-speaker:before{content:"\f5f4"}.fi-rr-sphere:before{content:"\f5f5"}.fi-rr-spinner:before{content:"\f5f6"}.fi-rr-spoon:before{content:"\f5f7"}.fi-rr-sport:before{content:"\f5f8"}.fi-rr-square-0:before{content:"\f5f9"}.fi-rr-square-1:before{content:"\f5fa"}.fi-rr-square-2:before{content:"\f5fb"}.fi-rr-square-3:before{content:"\f5fc"}.fi-rr-square-4:before{content:"\f5fd"}.fi-rr-square-5:before{content:"\f5fe"}.fi-rr-square-6:before{content:"\f5ff"}.fi-rr-square-7:before{content:"\f600"}.fi-rr-square-8:before{content:"\f601"}.fi-rr-square-9:before{content:"\f602"}.fi-rr-square-code:before{content:"\f603"}.fi-rr-square-heart:before{content:"\f604"}.fi-rr-square-info:before{content:"\f605"}.fi-rr-square-plus:before{content:"\f606"}.fi-rr-square-root:before{content:"\f607"}.fi-rr-square-terminal:before{content:"\f608"}.fi-rr-square:before{content:"\f609"}.fi-rr-squircle:before{content:"\f60a"}.fi-rr-stamp:before{content:"\f60b"}.fi-rr-star-comment-alt:before{content:"\f60c"}.fi-rr-star-octogram:before{content:"\f60d"}.fi-rr-star:before{content:"\f60e"}.fi-rr-starfighter:before{content:"\f60f"}.fi-rr-stars:before{content:"\f610"}.fi-rr-stats:before{content:"\f611"}.fi-rr-steak:before{content:"\f612"}.fi-rr-steering-wheel:before{content:"\f613"}.fi-rr-stethoscope:before{content:"\f614"}.fi-rr-sticker:before{content:"\f615"}.fi-rr-stop:before{content:"\f616"}.fi-rr-stopwatch:before{content:"\f617"}.fi-rr-store-alt:before{content:"\f618"}.fi-rr-store-lock:before{content:"\f619"}.fi-rr-strawberry:before{content:"\f61a"}.fi-rr-street-view:before{content:"\f61b"}.fi-rr-subtitles:before{content:"\f61c"}.fi-rr-subway:before{content:"\f61d"}.fi-rr-suitcase-alt:before{content:"\f61e"}.fi-rr-summer:before{content:"\f61f"}.fi-rr-sun:before{content:"\f620"}.fi-rr-sunrise-alt:before{content:"\f621"}.fi-rr-sunrise:before{content:"\f622"}.fi-rr-sunset:before{content:"\f623"}.fi-rr-surfing:before{content:"\f624"}.fi-rr-surprise:before{content:"\f625"}.fi-rr-sushi:before{content:"\f626"}.fi-rr-swimmer:before{content:"\f627"}.fi-rr-sword:before{content:"\f628"}.fi-rr-symbol:before{content:"\f629"}.fi-rr-syringe:before{content:"\f62a"}.fi-rr-t:before{content:"\f62b"}.fi-rr-table-columns:before{content:"\f62c"}.fi-rr-table-layout:before{content:"\f62d"}.fi-rr-table-picnic:before{content:"\f62e"}.fi-rr-table-pivot:before{content:"\f62f"}.fi-rr-table-rows:before{content:"\f630"}.fi-rr-table-tree:before{content:"\f631"}.fi-rr-tablet:before{content:"\f632"}.fi-rr-tachometer-alt-average:before{content:"\f633"}.fi-rr-tachometer-alt-fastest:before{content:"\f634"}.fi-rr-tachometer-alt-slow:before{content:"\f635"}.fi-rr-tachometer-alt-slowest:before{content:"\f636"}.fi-rr-tachometer-average:before{content:"\f637"}.fi-rr-tachometer-fast:before{content:"\f638"}.fi-rr-tachometer-fastest:before{content:"\f639"}.fi-rr-tachometer-slow:before{content:"\f63a"}.fi-rr-tachometer-slowest:before{content:"\f63b"}.fi-rr-tachometer:before{content:"\f63c"}.fi-rr-taco:before{content:"\f63d"}.fi-rr-tags:before{content:"\f63e"}.fi-rr-tally:before{content:"\f63f"}.fi-rr-target:before{content:"\f640"}.fi-rr-taxi-bus:before{content:"\f641"}.fi-rr-taxi:before{content:"\f642"}.fi-rr-temperature-down:before{content:"\f643"}.fi-rr-temperature-frigid:before{content:"\f644"}.fi-rr-temperature-high:before{content:"\f645"}.fi-rr-temperature-low:before{content:"\f646"}.fi-rr-temperature-up:before{content:"\f647"}.fi-rr-template-alt:before{content:"\f648"}.fi-rr-template:before{content:"\f649"}.fi-rr-tenge:before{content:"\f64a"}.fi-rr-tennis:before{content:"\f64b"}.fi-rr-terminal:before{content:"\f64c"}.fi-rr-terrace:before{content:"\f64d"}.fi-rr-test-tube:before{content:"\f64e"}.fi-rr-test:before{content:"\f64f"}.fi-rr-text-check:before{content:"\f650"}.fi-rr-text-height:before{content:"\f651"}.fi-rr-text-shadow:before{content:"\f652"}.fi-rr-text-size:before{content:"\f653"}.fi-rr-text-slash:before{content:"\f654"}.fi-rr-text-width:before{content:"\f655"}.fi-rr-text:before{content:"\f656"}.fi-rr-thermometer-half:before{content:"\f657"}.fi-rr-thumbtack:before{content:"\f658"}.fi-rr-thunderstorm-moon:before{content:"\f659"}.fi-rr-thunderstorm-sun:before{content:"\f65a"}.fi-rr-thunderstorm:before{content:"\f65b"}.fi-rr-ticket-alt:before{content:"\f65c"}.fi-rr-ticket:before{content:"\f65d"}.fi-rr-time-add:before{content:"\f65e"}.fi-rr-time-check:before{content:"\f65f"}.fi-rr-time-delete:before{content:"\f660"}.fi-rr-time-fast:before{content:"\f661"}.fi-rr-time-forward-sixty:before{content:"\f662"}.fi-rr-time-forward-ten:before{content:"\f663"}.fi-rr-time-forward:before{content:"\f664"}.fi-rr-time-half-past:before{content:"\f665"}.fi-rr-time-oclock:before{content:"\f666"}.fi-rr-time-past:before{content:"\f667"}.fi-rr-time-quarter-past:before{content:"\f668"}.fi-rr-time-quarter-to:before{content:"\f669"}.fi-rr-time-twenty-four:before{content:"\f66a"}.fi-rr-tire-flat:before{content:"\f66b"}.fi-rr-tire-pressure-warning:before{content:"\f66c"}.fi-rr-tire-rugged:before{content:"\f66d"}.fi-rr-tire:before{content:"\f66e"}.fi-rr-tired:before{content:"\f66f"}.fi-rr-toilet-paper-blank:before{content:"\f670"}.fi-rr-tomato:before{content:"\f671"}.fi-rr-tool-box:before{content:"\f672"}.fi-rr-tool-crop:before{content:"\f673"}.fi-rr-tool-marquee:before{content:"\f674"}.fi-rr-tooth:before{content:"\f675"}.fi-rr-tornado:before{content:"\f676"}.fi-rr-tower-control:before{content:"\f677"}.fi-rr-tractor:before{content:"\f678"}.fi-rr-traffic-light-go:before{content:"\f679"}.fi-rr-traffic-light-slow:before{content:"\f67a"}.fi-rr-traffic-light-stop:before{content:"\f67b"}.fi-rr-traffic-light:before{content:"\f67c"}.fi-rr-trailer:before{content:"\f67d"}.fi-rr-train-side:before{content:"\f67e"}.fi-rr-train-subway-tunnel:before{content:"\f67f"}.fi-rr-train-tram:before{content:"\f680"}.fi-rr-train:before{content:"\f681"}.fi-rr-tram:before{content:"\f682"}.fi-rr-transform:before{content:"\f683"}.fi-rr-trash:before{content:"\f684"}.fi-rr-treasure-chest:before{content:"\f685"}.fi-rr-treatment:before{content:"\f686"}.fi-rr-tree-christmas:before{content:"\f687"}.fi-rr-tree-deciduous:before{content:"\f688"}.fi-rr-tree:before{content:"\f689"}.fi-rr-triangle-warning:before{content:"\f68a"}.fi-rr-triangle:before{content:"\f68b"}.fi-rr-trophy-star:before{content:"\f68c"}.fi-rr-trophy:before{content:"\f68d"}.fi-rr-truck-container:before{content:"\f68e"}.fi-rr-truck-couch:before{content:"\f68f"}.fi-rr-truck-loading:before{content:"\f690"}.fi-rr-truck-monster:before{content:"\f691"}.fi-rr-truck-moving:before{content:"\f692"}.fi-rr-truck-pickup:before{content:"\f693"}.fi-rr-truck-plow:before{content:"\f694"}.fi-rr-truck-ramp:before{content:"\f695"}.fi-rr-truck-side:before{content:"\f696"}.fi-rr-tty:before{content:"\f697"}.fi-rr-turkey:before{content:"\f698"}.fi-rr-tv-music:before{content:"\f699"}.fi-rr-typewriter:before{content:"\f69a"}.fi-rr-u:before{content:"\f69b"}.fi-rr-umbrella-beach:before{content:"\f69c"}.fi-rr-umbrella:before{content:"\f69d"}.fi-rr-underline:before{content:"\f69e"}.fi-rr-undo-alt:before{content:"\f69f"}.fi-rr-undo:before{content:"\f6a0"}.fi-rr-universal-access:before{content:"\f6a1"}.fi-rr-unlock:before{content:"\f6a2"}.fi-rr-up-left:before{content:"\f6a3"}.fi-rr-up-right:before{content:"\f6a4"}.fi-rr-up:before{content:"\f6a5"}.fi-rr-upload:before{content:"\f6a6"}.fi-rr-usb-pendrive:before{content:"\f6a7"}.fi-rr-usd-circle:before{content:"\f6a8"}.fi-rr-usd-square:before{content:"\f6a9"}.fi-rr-user-add:before{content:"\f6aa"}.fi-rr-user-time:before{content:"\f6ab"}.fi-rr-user:before{content:"\f6ac"}.fi-rr-users-alt:before{content:"\f6ad"}.fi-rr-users:before{content:"\f6ae"}.fi-rr-utensils:before{content:"\f6af"}.fi-rr-v:before{content:"\f6b0"}.fi-rr-vector-alt:before{content:"\f6b1"}.fi-rr-vector:before{content:"\f6b2"}.fi-rr-venus-double:before{content:"\f6b3"}.fi-rr-venus-mars:before{content:"\f6b4"}.fi-rr-venus:before{content:"\f6b5"}.fi-rr-vest-patches:before{content:"\f6b6"}.fi-rr-vest:before{content:"\f6b7"}.fi-rr-video-arrow-down-left:before{content:"\f6b8"}.fi-rr-video-arrow-up-right:before{content:"\f6b9"}.fi-rr-video-camera-alt:before{content:"\f6ba"}.fi-rr-video-camera:before{content:"\f6bb"}.fi-rr-video-plus:before{content:"\f6bc"}.fi-rr-video-slash:before{content:"\f6bd"}.fi-rr-volcano:before{content:"\f6be"}.fi-rr-volleyball:before{content:"\f6bf"}.fi-rr-volume:before{content:"\f6c0"}.fi-rr-w:before{content:"\f6c1"}.fi-rr-wagon-covered:before{content:"\f6c2"}.fi-rr-wallet:before{content:"\f6c3"}.fi-rr-warehouse-alt:before{content:"\f6c4"}.fi-rr-water-bottle:before{content:"\f6c5"}.fi-rr-water-lower:before{content:"\f6c6"}.fi-rr-water-rise:before{content:"\f6c7"}.fi-rr-water:before{content:"\f6c8"}.fi-rr-watermelon:before{content:"\f6c9"}.fi-rr-wheat:before{content:"\f6ca"}.fi-rr-wheelchair-move:before{content:"\f6cb"}.fi-rr-wheelchair:before{content:"\f6cc"}.fi-rr-whistle:before{content:"\f6cd"}.fi-rr-wifi-1:before{content:"\f6ce"}.fi-rr-wifi-2:before{content:"\f6cf"}.fi-rr-wifi-alt:before{content:"\f6d0"}.fi-rr-wifi-exclamation:before{content:"\f6d1"}.fi-rr-wifi-slash:before{content:"\f6d2"}.fi-rr-wifi:before{content:"\f6d3"}.fi-rr-wind-warning:before{content:"\f6d4"}.fi-rr-wind:before{content:"\f6d5"}.fi-rr-window-alt:before{content:"\f6d6"}.fi-rr-window-maximize:before{content:"\f6d7"}.fi-rr-window-minimize:before{content:"\f6d8"}.fi-rr-window-restore:before{content:"\f6d9"}.fi-rr-windsock:before{content:"\f6da"}.fi-rr-wine-glass-crack:before{content:"\f6db"}.fi-rr-woman-head:before{content:"\f6dc"}.fi-rr-world:before{content:"\f6dd"}.fi-rr-wrench-simple:before{content:"\f6de"}.fi-rr-x:before{content:"\f6df"}.fi-rr-y:before{content:"\f6e0"}.fi-rr-yen:before{content:"\f6e1"}.fi-rr-yin-yang:before{content:"\f6e2"}.fi-rr-z:before{content:"\f6e3"}.fi-rr-zoom-in:before{content:"\f6e4"}.fi-rr-zoom-out:before{content:"\f6e5"} /*# sourceMappingURL=rounded.css.map */ ================================================ FILE: assets/webfonts/OFL.txt ================================================ Copyright 2019 The Baloo 2 Project Authors (https://github.com/EkType/Baloo2) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: misc/window/LICENSE ================================================ Copyright (c) 2021-2022 Guasam This software is provided "as-is", without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ================================================ FILE: misc/window/components/ControlButton.tsx ================================================ import classNames from 'classnames'; import React from 'react'; interface IControlButtonProps { readonly name: string; readonly path: string; readonly title: string; } const ControlButton: React.FC< IControlButtonProps & React.HTMLAttributes > = (props) => { const { name, path, title, ...rest } = props; const { onClick } = rest; const className = classNames('control', name); return (
); }; export default ControlButton; ================================================ FILE: misc/window/components/Titlebar.less ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Window Components Stylesheet */ // @import '/src/renderer/components/Theme.scss'; @titlebar-baseSize: 16px; @titlebar-height: 28px; @titlebar-bg: #171b21; @titlebar-iconSize: 16px; @em: @titlebar-baseSize*1em; //----------------------------------------- // Mixins //----------------------------------------- .flex-strech() { display: flex; align-items: stretch; } .flex-align-center() { display: flex; align-items: center; } //----------------------------------------- // Stylesheet //----------------------------------------- .window-content { position: relative; overflow: auto; flex: 1; } .window-titlebar { .flex-strech(); display: none; font-size: @titlebar-baseSize; height: @titlebar-height; background: @titlebar-bg; // -webkit-app-region: drag; // Draggable user-select: none; position: relative; & > section { .flex-align-center(); } &-content { flex: 1; font-size: calc(@titlebar-baseSize - 3px); color: #a9b0bb; &.centered { width: 100%; height: 100%; position: absolute; justify-content: center; } } // Titlebar icon &-icon { padding: 0 0.75em; img { width: @titlebar-iconSize; height: @titlebar-iconSize; } } // Titlebar Menu &-menu { flex: 1; } .menu { &-item { position: relative; } &-item.active { .menu-title { background: #3c4043; color: #bfbfbf; } } &-title { padding: 4px 10px; font-size: 0.8125rem; text-shadow: 0px 1px 1px black; -webkit-app-region: no-drag; color: #97a0b1; &:hover { background-color: #1f252c; } } &-popup { display: none; position: fixed; background: #292a2d; min-width: 70px; border: 1px solid #3c4043; padding: 0.25rem 0; box-shadow: 2px 1px 4px hsla(0, 0%, 0%, 0.5); z-index: 10000; &.active { display: block; } &-item { display: flex; justify-content: space-between; font-size: 0.8125rem; text-shadow: 0px 1px 1px black; padding: 0.375rem 1rem; &:hover { background: #1761cb; .popup-item-shortcut { color: #8cbbff; } } } } } .popup-item { &-name { padding-right: 2rem; color: #d8d8d8; } &-shortcut { color: #73757c; text-shadow: none; } &-separator { height: 1px; background: #3c4043; margin: 4px 0; } } // Titlebar controls &-controls { .flex-strech(); position: absolute; right: 0; top: 0; bottom: 0; color: hsla(var(--app-text-color), 0.5); &.type-windows { .control { padding: 0 1.15em; font-size: 0.875em; display: flex; height: 100%; align-items: center; -webkit-app-region: no-drag; } } &.type-mac { display: none !important; .control { width: 16px; height: 16px; background: #0e0e0e99; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 0.675rem; color: transparent; -webkit-app-region: no-drag; opacity: 0.8; &:hover { opacity: 1; } } .control.close { background: #f46d60; } .control.maximize { background: #59ca56; } .control.minimize { background: #f9c04e; } } } } ================================================ FILE: misc/window/components/Titlebar.tsx ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Window Titlebar (Component) */ import React, { createRef, useContext, useEffect, useRef, useState } from 'react'; import titlebarMenus from '../titlebarMenus'; import classNames from 'classnames'; import WindowControls from './WindowControls'; import context from '../titlebarContextApi'; import { WindowContext } from './WindowFrame'; import './Titlebar.less'; type Props = { title: string; mode: 'centered-title'; icon?: string; }; const Titlebar: React.FC = (props) => { const activeMenuIndex = useRef(null); const menusRef = titlebarMenus.map(() => createRef()); const [menusVisible, setMenusVisible] = useState(true); const windowContext = useContext(WindowContext); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.repeat) return; // Prevent repeatation of toggle when key holding if (e.altKey && e.ctrlKey) { // Hiding menus? close active menu popup if (menusVisible) { closeActiveMenu(); } setMenusVisible(!menusVisible); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [menusVisible, menusRef]); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (activeMenuIndex.current != null) { if ( menusRef[activeMenuIndex.current].current && !menusRef[activeMenuIndex.current].current?.contains(event.target as Node) ) { // console.log('You clicked outside of me!'); closeActiveMenu(); } } } if (activeMenuIndex != null) { document.addEventListener('mousedown', handleClickOutside); // console.log('added event'); } return () => { document.removeEventListener('mousedown', handleClickOutside); // console.log('remove event'); }; }, [activeMenuIndex, menusRef]); function showMenu(index: number, e: React.MouseEvent) { e.stopPropagation(); e.preventDefault(); if (menusRef[index].current?.classList.contains('active')) { // close.. closeActiveMenu(); } else { // open.. menusRef[index].current?.classList.add('active'); activeMenuIndex.current = index; menusRef[index].current?.parentElement?.classList.add('active'); } } function onMenuHover(index: number) { if (activeMenuIndex.current != null) { menusRef[activeMenuIndex.current].current?.classList.toggle('active'); menusRef[index].current?.classList.toggle('active'); menusRef[index].current?.parentElement?.classList.toggle('active'); menusRef[activeMenuIndex.current].current?.parentElement?.classList.toggle( 'active', ); activeMenuIndex.current = index; } } function closeActiveMenu() { if (activeMenuIndex.current != null) { menusRef[activeMenuIndex.current].current?.classList.remove('active'); menusRef[activeMenuIndex.current]?.current?.parentElement?.classList.remove('active'); activeMenuIndex.current = null; } } function handleAction(action?: string, value?: string | number) { closeActiveMenu(); const c: Record = context; if (action) { if (typeof c[action] === 'function') { c[action](value); } else { console.log(`action [${action}] is not available in titlebar context`); } } } return (
{props.icon ? (
titlebar icon
) : ( '' )}
{menusVisible ? '' :
{props.title}
}
{titlebarMenus.map((item, menuIndex) => { return (
showMenu(menuIndex, e)} onMouseEnter={() => onMenuHover(menuIndex)} onMouseDown={(e) => e.preventDefault()} > {item.name}
{item.items?.map((menuItem, menuItemIndex) => { if (menuItem.name === '__') { return (
); } return (
handleAction(menuItem.action, menuItem.value) } onMouseDown={(e) => e.preventDefault()} >
{menuItem.name}
{menuItem.shortcut}
); })}
); })}
); }; export default Titlebar; ================================================ FILE: misc/window/components/WindowControls.tsx ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Window Controls - Close, Minimize, Maximize (Component) */ import classNames from 'classnames'; import React from 'react'; import context from '../titlebarContextApi'; import ControlButton from './ControlButton'; type Props = { platform: string; tooltips?: boolean; }; const closePath = 'M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z'; const maximizePath = 'M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z'; const minimizePath = 'M 0,5 10,5 10,6 0,6 Z'; const WindowControls: React.FC = (props) => { return (
context.minimize()} path={minimizePath} title={props.tooltips ? 'Minimize' : null} /> context.toggle_maximize()} path={maximizePath} title={props.tooltips ? 'Maximize' : null} /> context.exit()} path={closePath} title={props.tooltips ? 'Close' : null} />
); }; export default WindowControls; ================================================ FILE: misc/window/components/WindowFrame.tsx ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Window Frame (Component) */ import React, { useEffect, useRef } from 'react'; import Titlebar from './Titlebar'; const logo = require('@assets/images/logo.png') type Props = { title?: string; borderColor?: string; platform: 'windows' | 'mac'; children: React.ReactNode; }; type Context = { platform: 'windows' | 'mac'; }; export const WindowContext = React.createContext({ platform: 'windows', }); const WindowFrame: React.FC = (props) => { const itsRef = useRef(null); useEffect(() => { const { parentElement } = itsRef.current; parentElement.classList.add('has-electron-window'); parentElement.classList.add('has-border'); // Apply border color if prop given if (props.borderColor) { parentElement.style.borderColor = props.borderColor; } }, []); return ( {/* Reference creator */}
{/* Window Titlebar */} {/* Window Content (Application to render) */}
{props.children}
); }; export default WindowFrame; ================================================ FILE: misc/window/titlebarContext.ts ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Titlebar IPC (Renderer Process) */ import { ipcRenderer } from 'electron'; const titlebarContext = { exit() { ipcRenderer.invoke('window-close'); }, undo() { ipcRenderer.invoke('web-undo'); }, redo() { ipcRenderer.invoke('web-redo'); }, cut() { ipcRenderer.invoke('web-cut'); }, copy() { ipcRenderer.invoke('web-copy'); }, paste() { ipcRenderer.invoke('web-paste'); }, delete() { ipcRenderer.invoke('web-delete'); }, select_all() { ipcRenderer.invoke('web-select-all'); }, reload() { ipcRenderer.invoke('web-reload'); }, force_reload() { ipcRenderer.invoke('web-force-reload'); }, toggle_devtools() { ipcRenderer.invoke('web-toggle-devtools'); }, actual_size() { ipcRenderer.invoke('web-actual-size'); }, zoom_in() { ipcRenderer.invoke('web-zoom-in'); }, zoom_out() { ipcRenderer.invoke('web-zoom-out'); }, toggle_fullscreen() { ipcRenderer.invoke('web-toggle-fullscreen'); }, minimize() { ipcRenderer.invoke('window-minimize'); }, toggle_maximize() { ipcRenderer.invoke('window-toggle-maximize'); }, open_url(url: string) { ipcRenderer.invoke('open-url', url); }, }; export type TitlebarContextApi = typeof titlebarContext; export default titlebarContext; ================================================ FILE: misc/window/titlebarContextApi.ts ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Titlebar Context API */ import { TitlebarContextApi } from './titlebarContext'; const context: TitlebarContextApi = (window as any).api?.titlebar; export default context; ================================================ FILE: misc/window/titlebarIPC.ts ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Titlebar IPC (Main Process) */ import { BrowserWindow, ipcMain, shell } from 'electron'; export const registerTitlebarIpc = (mainWindow: BrowserWindow) => { ipcMain.handle('window-minimize', () => { mainWindow.minimize(); }); ipcMain.handle('window-maximize', () => { mainWindow.maximize(); }); ipcMain.handle('window-toggle-maximize', () => { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow.maximize(); } }); ipcMain.handle('window-close', () => { mainWindow.close(); }); ipcMain.handle('web-undo', () => { mainWindow.webContents.undo(); }); ipcMain.handle('web-redo', () => { mainWindow.webContents.redo(); }); ipcMain.handle('web-cut', () => { mainWindow.webContents.cut(); }); ipcMain.handle('web-copy', () => { mainWindow.webContents.copy(); }); ipcMain.handle('web-paste', () => { mainWindow.webContents.paste(); }); ipcMain.handle('web-delete', () => { mainWindow.webContents.delete(); }); ipcMain.handle('web-select-all', () => { mainWindow.webContents.selectAll(); }); ipcMain.handle('web-reload', () => { mainWindow.webContents.reload(); }); ipcMain.handle('web-force-reload', () => { mainWindow.webContents.reloadIgnoringCache(); }); ipcMain.handle('web-toggle-devtools', () => { mainWindow.webContents.toggleDevTools(); }); ipcMain.handle('web-actual-size', () => { mainWindow.webContents.setZoomLevel(0); }); ipcMain.handle('web-zoom-in', () => { mainWindow.webContents.setZoomLevel(mainWindow.webContents.zoomLevel + 0.5); }); ipcMain.handle('web-zoom-out', () => { mainWindow.webContents.setZoomLevel(mainWindow.webContents.zoomLevel - 0.5); }); ipcMain.handle('web-toggle-fullscreen', () => { mainWindow.setFullScreen(!mainWindow.fullScreen); }); ipcMain.handle('open-url', (e, url) => { shell.openExternal(url); }); }; ================================================ FILE: misc/window/titlebarMenus.ts ================================================ /** * Copyright (c) 2021, Guasam * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * Read the LICENSE file for more details. * * @author : guasam * @project : Electron Window * @package : Titlebar Menu Items */ export type TitlebarMenuItem = { name: string; action?: string; shortcut?: string; value?: string | number; items?: TitlebarMenuItem[]; }; export type TitlebarMenu = { name: string; items: TitlebarMenuItem[]; }; const titlebarMenus: TitlebarMenu[] = [ { name: 'File', items: [ { name: 'Exit', action: 'exit', }, ], }, { name: 'Edit', items: [ { name: 'Undo', action: 'undo', shortcut: 'Ctrl+Z', }, { name: 'Redo', action: 'redo', shortcut: 'Ctrl+Y', }, { name: '__', }, { name: 'Cut', action: 'cut', shortcut: 'Ctrl+X', }, { name: 'Copy', action: 'copy', shortcut: 'Ctrl+C', }, { name: 'Paste', action: 'paste', shortcut: 'Ctrl+V', }, { name: 'Delete', action: 'delete', }, { name: '__', }, { name: 'Select All', action: 'select_all', shortcut: 'Ctrl+A', }, ], }, { name: 'View', items: [ { name: 'Reload', action: 'reload', shortcut: 'Ctrl+R', }, { name: 'Force Reload', action: 'force_reload', shortcut: 'Ctrl+Shift+R', }, { name: 'Toogle Developer Tools', action: 'toggle_devtools', shortcut: 'Ctrl+Shift+I', }, { name: '__', }, { name: 'Actual Size', action: 'actual_size', shortcut: 'Ctrl+0', }, { name: 'Zoom In', action: 'zoom_in', shortcut: 'Ctrl++', }, { name: 'Zoom Out', action: 'zoom_out', shortcut: 'Ctrl+-', }, { name: '__', }, { name: 'Toggle Fullscreen', action: 'toggle_fullscreen', shortcut: 'F11', }, ], }, { name: 'Window', items: [ { name: 'Minimize', action: 'minimize', shortcut: 'Ctrl+M', }, { name: 'Close', action: 'exit', shortcut: 'Ctrl+W', }, ], }, { name: 'Author', items: [ { name: 'Guasam', action: 'open_url', value: 'https://github.com/guasam', shortcut: '@guasam', }, ], }, ]; export default titlebarMenus; ================================================ FILE: misc/window/windowPreload.ts ================================================ import { contextBridge, ipcRenderer } from 'electron'; import titlebarContext from './titlebarContext'; contextBridge.exposeInMainWorld('api', { titlebar: titlebarContext, send: (channel: string, data: string) => { // whitelist channels const validChannels = ['toMain']; if (validChannels.includes(channel)) { ipcRenderer.send(channel, data); } }, receive: (channel: string, func: () => void) => { const validChannels = [ 'gotLoadedDataX', 'Home', 'Picture', 'gotAllPictures', 'gotPicture', 'Shortcuts', 'gotAllBlocks', 'openArchive', 'gotPageStyle', 'gotUserTheme', 'gotArchive', 'gotUserColor', 'gotOS', 'openFiles', 'newFile', 'Save', 'fromMain', 'Text', 'Graph', 'Math', 'Group', 'gotNotebooks', 'toggleNotebooks', 'Search', ]; if (validChannels.includes(channel)) { // Deliberately strip event as it includes `sender` ipcRenderer.on(channel, (event, ...args) => func(...(args as []))); } }, getNotebooks: () => { ipcRenderer.send('getNotebooks'); }, delete: (path: string, isFolder: boolean) => { ipcRenderer.send('delete', path, isFolder); }, newFile: (filePath: string) => { ipcRenderer.send('newFile', filePath); }, newFolder: (folderPath: string) => { ipcRenderer.send('newFolder', folderPath); }, openFiles: () => { ipcRenderer.send('openFiles'); }, move: (oldPath: string, newPath: string) => { ipcRenderer.send('move', oldPath, newPath); }, save: (data: string, file: string, newName: string) => { ipcRenderer.send('save', data, file, newName); }, load: (file: string) => { ipcRenderer.send('load', file); }, saveX: (data: string, filePath: string) => { ipcRenderer.send('saveX', data, filePath); }, loadX: (filePath: string) => { ipcRenderer.send('loadX',filePath); }, getOS: () => { ipcRenderer.send('getOS'); }, maximize: () => { ipcRenderer.send('maximize'); }, unmaximize: () => { ipcRenderer.send('unmaximize'); }, minimize: () => { ipcRenderer.send('minimize'); }, close: () => { ipcRenderer.send('close'); }, setUserColor: (color: string) => { ipcRenderer.send('setUserColor', color); }, setPageStyle: (style: string) => { ipcRenderer.send('setPageStyle', style); }, toggle: () => { ipcRenderer.send('dark-mode'); }, getUserColor: () => { ipcRenderer.send('getUserColor'); }, getPageStyle: () => { ipcRenderer.send('getPageStyle'); }, getUserTheme: () => { ipcRenderer.send('getUserTheme'); }, getPicture: (id: string) => { ipcRenderer.send('getPicture', id); }, getAllPictures: (path: string) => { ipcRenderer.send('getAllPictures', path); }, getArchive: () => { ipcRenderer.send('getArchive'); }, startSearch: () => { ipcRenderer.send('startSearch'); }, }); ================================================ FILE: package.json ================================================ { "name": "mathberet", "productName": "Mathberet", "version": "1.0.0", "description": "Digital Mathematics Notebook", "main": ".webpack/main", "scripts": { "start": "cross-env NODE_ENV=development electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", "lint": "eslint src/ --ext .ts,.js,.tsx,.jsx" }, "keywords": [ "react", "javascript", "typescript", "math", "mathematics", "notebook", "notes" ], "author": { "name": "yonatanmgr", "url": "https://github.com/yonatanmgr" }, "contributors": [ { "name": "Erez Birenholz", "url": "https://github.com/ErezBiren" }, { "name": "Ziv Nadel", "url": "https://github.com/zivnadel" }, { "name": "Nadav Magier", "url": "https://github.com/Nadav0077" } ], "repository": { "type": "git", "url": "https://github.com/yonatanmgr/mathberet" }, "license": "MIT", "config": { "forge": "./tools/forge/forge.config.js" }, "devDependencies": { "@electron-forge/cli": "6.0.5", "@electron-forge/maker-deb": "6.0.5", "@electron-forge/maker-rpm": "6.0.5", "@electron-forge/maker-squirrel": "6.0.5", "@electron-forge/maker-zip": "6.0.5", "@electron-forge/plugin-webpack": "6.0.5", "@marshallofsound/webpack-asset-relocator-loader": "^0.5.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@types/node": "^18.13.0", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@types/react-grid-layout": "^1.3.2", "@types/webpack-env": "^1.18.0", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "classnames": "^2.3.2", "cross-env": "^7.0.3", "css-loader": "^6.7.3", "electron": "^23.0.0", "eslint": "^8.33.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^7.3.0", "less": "^4.1.3", "less-loader": "11.1.0", "node-loader": "^2.0.0", "react-refresh": "^0.14.0", "sass": "^1.58.0", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "ts-loader": "9.4.2", "typescript": "^4.9.5", "webpack": "^5.75.0" }, "dependencies": { "@cortex-js/compute-engine": "^0.12.2", "@flaticon/flaticon-uicons": "^2.0.1", "@tldraw/tldraw": "^1.28.0", "electron-squirrel-startup": "^1.0.0", "electron-store": "^8.1.0", "gridstack": "^7.2.3", "i18next": "^22.4.11", "kbar": "^0.1.0-beta.40", "keyboard-css": "^1.2.4", "mafs": "^0.15.2", "mathjs": "^11.6.0", "mathlive": "^0.87.1", "react": "^18.2.0", "react-complex-tree": "^2.1.1", "react-dom": "^18.2.0", "react-grid-layout": "^1.3.4", "react-i18next": "^12.2.0", "react-math-view": "^1.3.2", "react-mathlive": "^3.0.5-preview.1", "react-resizable": "^3.0.4", "slate": "^0.91.4", "slate-history": "^0.86.0", "slate-react": "^0.91.8", "tex-math-parser": "^2.0.4" } } ================================================ FILE: src/common/helpers.ts ================================================ /** * Checks if process NODE_ENV in 'development' mode */ export function inDev(): boolean { return process.env.NODE_ENV == 'development'; } ================================================ FILE: src/common/i18n.ts ================================================ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import { HE_TRANSLATION } from './locals/he'; import { EN_TRANSLATION } from './locals/en'; import { RU_TRANSLATION } from './locals/ru'; import { AR_TRANSLATION } from './locals/ar'; import { ES_TRANSLATION } from './locals/es'; import { ZH_TRANSLATION } from './locals/zh'; import { FR_TRANSLATION } from './locals/fr'; import { HI_TRANSLATION } from './locals/hi'; const resources = { ar: { translation: AR_TRANSLATION }, en: { translation: EN_TRANSLATION }, es: { translation: ES_TRANSLATION }, fr: { translation: FR_TRANSLATION }, he: { translation: HE_TRANSLATION }, hi: { translation: HI_TRANSLATION }, ru: { translation: RU_TRANSLATION }, zh: { translation: ZH_TRANSLATION }, }; i18n.use(initReactI18next).init({ resources, lng: 'en', fallbackLng: ['en', 'he'], interpolation: { escapeValue: false, }, }); export default i18n; ================================================ FILE: src/common/keybindings.ts ================================================ import { Keybinding } from "mathlive" const DEFAULT_KEYBINDINGS: Array = [ { key: "left", command: "moveToPreviousChar" }, { key: "right", command: "moveToNextChar" }, { key: "up", command: "moveUp" }, { key: "down", command: "moveDown" }, { key: "shift+[ArrowLeft]", command: "extendSelectionBackward" }, { key: "shift+[ArrowRight]", command: "extendSelectionForward" }, { key: "shift+[ArrowUp]", command: "extendSelectionUpward" }, { key: "shift+[ArrowDown]", command: "extendSelectionDownward" }, { key: "[Backspace]", command: "deleteBackward" }, { key: "alt+[Delete]", command: "deleteBackward" }, { key: "[Delete]", command: "deleteForward" }, { key: "alt+[Backspace]", command: "deleteForward" }, { key: "alt+[ArrowLeft]", command: "moveToPreviousWord" }, { key: "alt+[ArrowRight]", command: "moveToNextWord" }, { key: "shift+alt+[ArrowLeft]", command: "extendToPreviousWord" }, { key: "shift+alt+[ArrowRight]", command: "extendToNextWord" }, { key: "ctrl+[ArrowLeft]", command: "moveToGroupStart" }, { key: "ctrl+[ArrowRight]", command: "moveToGroupEnd" }, { key: "shift+ctrl+[ArrowLeft]", command: "extendToGroupStart" }, { key: "shift+ctrl+[ArrowRight]", command: "extendToGroupEnd" }, { key: "[Space]", ifMode: "math", command: "moveAfterParent" }, { key: "shift+[Space]", ifMode: "math", command: "moveBeforeParent" }, { key: "[Home]", command: "moveToMathfieldStart" }, { key: "cmd+[ArrowLeft]", command: "moveToMathfieldStart" }, { key: "shift+[Home]", command: "extendToMathFieldStart" }, { key: "shift+cmd+[ArrowLeft]", command: "extendToMathFieldStart" }, { key: "[End]", command: "moveToMathfieldEnd" }, { key: "cmd+[ArrowRight]", command: "moveToMathfieldEnd" }, { key: "shift+[End]", command: "extendToMathFieldEnd" }, { key: "shift+cmd+[ArrowRight]", command: "extendToMathFieldEnd" }, { key: "[Pageup]", command: "moveToGroupStart" }, { key: "[Pagedown]", command: "moveToGroupEnd" }, { key: "[Tab]", ifMode: "math", command: "moveToNextPlaceholder" }, { key: "shift+[Tab]", ifMode: "math", command: "moveToPreviousPlaceholder" }, { key: "[Tab]", ifMode: "text", command: "moveToNextPlaceholder" }, { key: "shift+[Tab]", ifMode: "text", command: "moveToPreviousPlaceholder" }, { key: "[Escape]", ifMode: "math", command: ["switchMode", "latex"] }, { key: "[Escape]", ifMode: "text", command: ["switchMode", "latex"] }, { key: "[Escape]", ifMode: "latex", command: ["complete", "complete", { selectItem: "true" }] }, // Accept the entry (without the suggestion) and select { key: "\\", ifMode: "math", command: ["switchMode", "latex", "\\"] }, // { key: '[Backslash]', ifMode: 'math', command: ['switchMode', 'latex'] }, { key: "[IntlBackslash]", ifMode: "math", command: ["switchMode", "latex", "\\"] }, // On UK QWERTY keyboards { key: "[Tab]", ifMode: "latex", command: ["complete", "accept-suggestion"] }, // Complete the suggestion { key: "[Return]", ifMode: "latex", command: "complete" }, { key: "[Enter]", ifMode: "latex", command: "complete" }, { key: "shift+[Escape]", ifMode: "latex", command: ["complete", "reject"] }, // Some keyboards can't generate // this combination, for example in 60% keyboards it is mapped to ~ { key: "[ArrowDown]", ifMode: "latex", command: "nextSuggestion" }, // { key: 'ios:command:[Tab]', ifMode: 'latex',command: 'nextSuggestion' }, { key: "[ArrowUp]", ifMode: "latex", command: "previousSuggestion" }, { key: "ctrl+a", ifPlatform: "!macos", command: "selectAll" }, { key: "cmd+a", command: "selectAll" }, // Rare keys on some extended keyboards { key: "[Cut]", command: "cutToClipboard" }, { key: "[Copy]", command: "copyToClipboard" }, { key: "[Paste]", command: "pasteFromClipboard" }, { key: "[Clear]", command: "deleteBackward" }, { key: "ctrl+z", ifPlatform: "!macos", command: "undo" }, { key: "cmd+z", command: "undo" }, { key: "[Undo]", command: "undo" }, { key: "ctrl+y", ifPlatform: "!macos", command: "redo" }, // ARIA recommendation { key: "shift+cmd+y", command: "redo" }, { key: "shift+ctrl+z", ifPlatform: "!macos", command: "redo" }, { key: "shift+cmd+z", command: "redo" }, { key: "[Redo]", command: "redo" }, { key: "[EraseEof]", command: "deleteToGroupEnd" }, // EMACS/MACOS BINDINGS { key: "ctrl+b", ifPlatform: "macos", command: "moveToPreviousChar" }, { key: "ctrl+f", ifPlatform: "macos", command: "moveToNextChar" }, { key: "ctrl+p", ifPlatform: "macos", command: "moveUp" }, { key: "ctrl+n", ifPlatform: "macos", command: "moveDown" }, { key: "ctrl+a", ifPlatform: "macos", command: "moveToMathfieldStart" }, { key: "ctrl+e", ifPlatform: "macos", command: "moveToMathfieldEnd" }, { key: "shift+ctrl+b", ifPlatform: "macos", command: "extendSelectionBackward" }, { key: "shift+ctrl+f", ifPlatform: "macos", command: "extendSelectionForward" }, { key: "shift+ctrl+p", ifPlatform: "macos", command: "extendSelectionUpward" }, { key: "shift+ctrl+n", ifPlatform: "macos", command: "extendSelectionDownward" }, { key: "shift+ctrl+a", ifPlatform: "macos", command: "extendToMathFieldStart" }, { key: "shift+ctrl+e", ifPlatform: "macos", command: "extendToMathFieldEnd" }, { key: "alt+ctrl+b", ifPlatform: "macos", command: "moveToPreviousWord" }, { key: "alt+ctrl+f", ifPlatform: "macos", command: "moveToNextWord" }, { key: "shift+alt+ctrl+b", ifPlatform: "macos", command: "extendToPreviousWord" }, { key: "shift+alt+ctrl+f", ifPlatform: "macos", command: "extendToNextWord" }, { key: "ctrl+h", ifPlatform: "macos", command: "deleteBackward" }, { key: "ctrl+d", ifPlatform: "macos", command: "deleteForward" }, { key: "ctrl+l", ifPlatform: "macos", command: "scrollIntoView" }, // { key: 'ctrl+t', ifPlatform: 'macos', command: 'transpose' }, // WOLFRAM MATHEMATICA BINDINGS { key: "ctrl+[Digit2]", ifMode: "math", command: ["insert", "\\sqrt{#0}"] }, { key: "ctrl+[Digit5]", ifMode: "math", command: "moveToOpposite" }, { key: "ctrl+[Digit6]", ifMode: "math", command: "moveToSuperscript" }, { key: "ctrl+[Return]", ifMode: "math", command: "addRowAfter" }, { key: "ctrl+[Enter]", ifMode: "math", command: "addRowAfter" }, { key: "cmd+[Return]", ifMode: "math", command: "addRowAfter" }, { key: "cmd+[Enter]", ifMode: "math", command: "addRowAfter" }, // Excel keybindings: // shift+space: select entire row, ctrl+space: select an entire column // shift+ctrl++ or ctrl+numpad+ // ctrl+- to delete a row or columns // MATHLIVE BINDINGS // { key: 'alt+a', command: ['insert', '\\theta'] }, { key: "alt+p", ifMode: "math", command: ["insert", "\\pi"] }, { key: "alt+v", ifMode: "math", command: ["insert", "\\sqrt{#0}"] }, { key: "alt+w", ifMode: "math", command: ["insert", "\\sum_{i=#?}^{#?}"] }, { key: "alt+b", command: ["insert", "\\int_{#?}^{#?}"] }, { key: "alt+u", ifMode: "math", command: ["insert", "\\cup"] }, { key: "alt+n", ifMode: "math", command: ["insert", "\\cap"] }, { key: "alt+o", ifMode: "math", command: ["insert", "\\emptyset"] }, { key: "alt+d", ifMode: "math", command: ["insert", "\\differentialD"] }, { key: "shift+alt+o", ifMode: "math", command: ["insert", "\\varnothing"] }, { key: "shift+alt+d", ifMode: "math", command: ["insert", "\\partial"] }, { key: "shift+alt+p", ifMode: "math", command: ["insert", "\\prod_{i=#?}^{#?}"] }, { key: "shift+alt+u", ifMode: "math", command: ["insert", "\\bigcup"] }, { key: "shift+alt+n", ifMode: "math", command: ["insert", "\\bigcap"] }, { key: "shift+alt+a", ifMode: "math", command: ["insert", "\\forall"] }, { key: "shift+alt+e", ifMode: "math", command: ["insert", "\\exists"] }, { key: "alt+[Backslash]", ifMode: "math", command: ["insert", "\\backslash"] }, // "|" key} override command mode { key: "[NumpadDivide]", ifMode: "math", command: ["insert", "\\frac{#@}{#?}"] }, // ?? { key: "alt+[NumpadDivide]", ifMode: "math", command: ["insert", "\\frac{#?}{#@}"] }, // ?? // Accessibility { key: "alt+k", command: "toggleVirtualKeyboard" }, // Note: On Mac OS (as of 10.12), there is a bug/behavior that causes // a beep to be generated with certain command+control key combinations. // The workaround is to create a default binding file to silence them. // In ~/Library/KeyBindings/DefaultKeyBinding.dict add these entries: // // { // "^@\UF701" = "noop:"; // "^@\UF702" = "noop:"; // "^@\UF703" = "noop:"; // } // { // key: "alt+ctrl+[ArrowUp]", // command: ["speak", "all", { withHighlighting: false }] // }, // { // key: "alt+ctrl+[ArrowDown]", // command: ["speak", "selection", { withHighlighting: false }] // }, // // Punctuations and some non-alpha key combinations // only work with specific keyboard layouts // { key: "alt+[Equal]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["applyStyle", { mode: "text" }] }, { key: "alt+[Equal]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "text", command: ["applyStyle", { mode: "math" }] }, { key: "shift+[Quote]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["switchMode", "text", "", ""] }, { key: "shift+[Quote]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "text", command: ["switchMode", "math", "", ""] }, { key: "/", ifMode: "math", command: ["insert", "\\frac{#@}{#?}"] }, { key: "alt+/", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\/"] }, { key: "alt+[BracketLeft]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\left\\lbrack #0 \\right\\rbrack"] }, // ?? // { // key: "ctrl+[Minus]", // ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], // ifMode: "math", // command: "moveToSubscript" // }, // ?? { key: "shift+alt+[BracketLeft]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\left\\lbrace #0 \\right\\rbrace"] }, // ?? { key: "ctrl+;", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addRowAfter" }, { key: "cmd+;", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addRowAfter" }, { key: "shift+ctrl+;", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addRowBefore" }, { key: "shift+cmd+;", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addRowBefore" }, { key: "ctrl+[Backspace]", ifMode: "math", command: "removeRow" }, { key: "cmd+[Backspace]", ifMode: "math", command: "removeRow" }, { key: "ctrl+[Comma]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addColumnAfter" }, { key: "cmd+[Comma]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addColumnAfter" }, { key: "shift+ctrl+[Comma]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addColumnBefore" }, { key: "shift+cmd+[Comma]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: "addColumnBefore" }, { key: "shift+[Backspace]", ifMode: "math", command: "removeColumn" }, { key: "alt+[Digit5]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "$\\infty"] }, // "%" key { key: "alt+[Digit6]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\wedge"] }, // "^" key { key: "shift+alt+[Digit6]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\vee"] }, // "^" key { key: "alt+[Digit9]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "("] }, // "(" key} override smartFence { key: "alt+[Digit0]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", ")"] }, // ")" key} override smartFence { key: "alt+|", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "|"] }, // "|" key} override smartFence { key: "shift+[Backquote]", ifLayout: ["apple.en-intl", "windows.en-intl", "linux.en"], ifMode: "math", command: ["insert", "\\~"] }, // ?? { key: "[Backquote]", ifLayout: ["windows.french", "linux.french"], ifMode: "math", command: ["insert", "^2"] } ] const CUSTOM_KEYBINDINGS: Array = [ { key: 'alt+[Equal]', ifMode: 'math', command: ['insert', '\\approx'] }, { key: 'alt+[Comma]', ifMode: 'math', command: ['insert', '\\measuredangle'] }, { key: 'alt+[Digit0]', ifMode: 'math', command: ['insert', '\\emptyset'] }, { key: 'alt+[Enter]', ifMode: 'math', command: ['insert', '\\begin{gathered} {#?} \\end{gathered}'] }, { key: 'alt+shift+c', ifMode: 'math', command: ['insert', '\\complement'] }, { key: 'alt+shift+t', ifMode: 'math', command: ['insert', '\\triangle'] }, { key: 'shift+[Enter]', ifMode: 'math', command: 'addRowAfter' }, { key: 'ctrl+[Equal]', ifMode: 'math', command: 'addColumnAfter' }, { key: 'ctrl+[Minus]', ifMode: 'math', command: 'removeColumn' }, ] const ML_KEYBINDINGS = [DEFAULT_KEYBINDINGS, CUSTOM_KEYBINDINGS].flat() export default ML_KEYBINDINGS ================================================ FILE: src/common/locals/ar.ts ================================================ export const AR_TRANSLATION = { 'My Notebooks': 'دفاتري', 'Command Bar': 'شريط الأوامر', 'Math Panel': 'لوحة الرياضيات', Archive: 'الأرشيف', 'New Folder': 'مجلد جديد', 'New File': 'ملف جديد', 'Notebooks Tooltip': 'انقر مرتين لفتح الموقع', Variables: 'متغيرات', Functions: 'وظائف', Prompt: 'اكتب أمرًا أو ابحث...', Preferences: 'تفضيلات', Theme: 'الوضع', Color: 'لون التمييز', Dark: 'مظلم', Light: 'فاتح', Language: 'اللغة 🌍', Hebrew: 'العبرية - עברית', Russian: 'الروسية - Русский', 'Mandarin Chinese': 'الصينية الماندرين - 中文普通话', Hindi: 'هندي - हिंदी', French: 'فرنسي - Française', English: 'الإنجليزية - English', Arabic: 'العربية', Spanish: 'الأسبانية - Español', Red: 'أحمر', Blue: 'أزرق', Green: 'أخضر', Yellow: 'أصفر', Purple: 'أرجواني', Pink: 'وردي', 'Text Block': 'إضافة كتلة نصية', 'Math Block': 'إضافة كتلة رياضية', 'Graph Block': 'إضافة كتلة رسم بياني', 'Draw Block': 'إضافة كتلة رسم', Divider: 'إضافة فاصل', Clear: 'مسح الصفحة', Save: 'حفظ', 'Add Tag': 'إضافة وسم', 'Placeholder 1': 'للبدء', 'Placeholder 2': 'انقر على رمز الدفتر على اليمين', 'Placeholder 3': 'وافتح / أنشئ ملفًا', 'Placeholder Or': 'أو', 'Modal 1': 'محو محتوى الصفحة؟', 'Modal 2': 'يوجد ملف جديد بالفعل', 'Modal 3': 'يوجد مجلد جديد بالفعل', 'Modal 4': 'هذا الاسم موجود بالفعل', 'Modal 5': 'لا يمكنك نقل هذا الملف هنا!', Cancel: 'إلغاء', OK: 'موافق', Saved: 'تم حفظ الملف!', 'Save Error': 'خطأ أثناء حفظ الملف', 'Text Placeholder': 'إدراج نص...', Press: 'اضغط على', 'Delete Item': 'على العنصر لحذفه', 'Rename Item': 'على العنصر لإعادة تسميته', }; ================================================ FILE: src/common/locals/en.ts ================================================ export const EN_TRANSLATION = { 'My Notebooks': 'Notebooks', 'Command Bar': 'Command Bar', 'Math Panel': 'Math Panel', Archive: 'Archive', 'New Folder': 'New Folder', 'New File': 'New File', 'Notebooks Tooltip': 'Double click to open location', Variables: 'Variables', Functions: 'Functions', Prompt: 'Type a command or search...', Preferences: 'Preferences', Theme: 'Theme', Color: 'Accent Color', Dark: 'Dark Theme', Light: 'Light Theme', Language: 'Language 🌍', Hebrew: 'Hebrew - עברית', Russian: 'Russian - Русский', English: 'English', Arabic: 'Arabic - العربية', Spanish: 'Spanish - Español', Hindi: 'Hindi - हिंदी', 'Mandarin Chinese': 'Mandarin Chinese - 中文普通话', French: 'French - Française', Red: 'Red', Blue: 'Blue', Green: 'Green', Yellow: 'Yellow', Purple: 'Purple', Pink: 'Pink', 'Text Block': 'Add Text Block', 'Math Block': 'Add Math Block', 'Graph Block': 'Add Graph Block', 'Draw Block': 'Add Draw Block', Divider: 'Add Divider', Clear: 'Clear Page', Save: 'Save', 'Add Tag': 'Add Tag', 'Placeholder 1': 'To begin', 'Placeholder 2': 'Click on the notebook icon on your left', 'Placeholder 3': 'and open/create a file', 'Placeholder Or': 'or', 'Modal 1': 'Clear page content?', 'Modal 2': 'A new file already exists', 'Modal 3': 'A new folder already exists', 'Modal 4': 'This name already exists', 'Modal 5': 'You cannot move this file here!', Cancel: 'Cancel', OK: 'OK', Saved: 'File saved!', 'Save Error': 'Error while saving file', 'Text Placeholder': 'Insert text...', Press: 'Press', 'Delete Item': 'on item to delete it', 'Rename Item': 'on item to rename it', }; ================================================ FILE: src/common/locals/es.ts ================================================ export const ES_TRANSLATION = { 'My Notebooks': 'Mis Cuadernos', 'Command Bar': 'Barra de Comandos', 'Math Panel': 'Panel de Matemáticas', Archive: 'Archivo', 'New Folder': 'Nueva Carpeta', 'New File': 'Nuevo Archivo', 'Notebooks Tooltip': 'Haz doble clic para abrir la ubicación', Variables: 'Variables', Functions: 'Funciones', Prompt: 'Escriba un comando o busque...', Preferences: 'Preferencias', Theme: 'Tema', Color: 'Color de Acento', Dark: 'Tema Oscuro', Light: 'Tema Claro', Language: 'Idioma 🌍', Hebrew: 'Hebreo - עברית', Russian: 'Ruso - Русский', English: 'Inglés - English', Arabic: 'Árabe - العربية', Spanish: 'Español', French: 'Francés - Française', Hindi: 'hindi - हिंदी', 'Mandarin Chinese': 'Chino mandarín - 中文普通话', Red: 'Rojo', Blue: 'Azul', Green: 'Verde', Yellow: 'Amarillo', Purple: 'Morado', Pink: 'Rosado', 'Text Block': 'Agregar Bloque de Texto', 'Math Block': 'Agregar Bloque de Matemáticas', 'Graph Block': 'Agregar Bloque de Gráfico', 'Draw Block': 'Agregar Bloque de Dibujo', Divider: 'Agregar Divisor', Clear: 'Limpiar Página', Save: 'Guardar', 'Add Tag': 'Agregar Etiqueta', 'Placeholder 1': 'Para comenzar', 'Placeholder 2': 'Haz clic en el icono del cuaderno a tu izquierda', 'Placeholder 3': 'y abre/crea un archivo', 'Placeholder Or': 'o', 'Modal 1': '¿Borrar contenido de la página?', 'Modal 2': 'Ya existe un nuevo archivo', 'Modal 3': 'Ya existe una nueva carpeta', 'Modal 4': 'Este nombre ya existe', 'Modal 5': '¡No puede mover este archivo aquí!', Cancel: 'Cancelar', OK: 'OK', Saved: '¡Archivo guardado!', 'Save Error': 'Error al guardar archivo', 'Text Placeholder': 'Inserte texto...', Press: 'Prensa', 'Delete Item': 'en el elemento para eliminarlo', 'Rename Item': 'en el elemento para cambiarle el nombre', }; ================================================ FILE: src/common/locals/fr.ts ================================================ export const FR_TRANSLATION = { 'My Notebooks': 'Mes carnets', 'Command Bar': 'Barre de commande', 'Math Panel': 'Panneau mathématique', Archive: 'Archivo', 'New Folder': 'Nouveau dossier', 'New File': 'Nouveau fichier', 'Notebooks Tooltip': `Double-cliquez pour ouvrir l'emplacement`, Variables: 'Variables', Functions: 'Les fonctions', Prompt: 'Tapez une commande ou recherchez...', Preferences: 'Préférences', Theme: 'Thème', Color: `Couleur d'accentuation`, Dark: 'Mode sombre', Light: 'Mode lumière', Language: 'Langue 🌍', Hebrew: 'Hébreu - עברית', Russian: 'Russe - Русский', English: 'Anglaise - English', Arabic: 'Arabe - العربية', Hindi: 'hindi - हिंदी', 'Mandarin Chinese': 'Chinois Mandarin - 中文普通话', French: 'Française', Red: 'Rouge', Blue: 'Bleu', Green: 'Vert', Yellow: 'Jaune', Purple: 'Violet', Pink: 'Rose', 'Text Block': 'Ajouter un bloc de texte', 'Math Block': 'Ajouter un bloc mathématique', 'Graph Block': 'Ajouter un bloc graphique', 'Draw Block': 'Ajouter un bloc de dessin', Divider: 'Ajouter un diviseur', Clear: 'Effacer la page', Save: 'Enregistrer le fichier', 'Add Tag': 'Ajouter une étiquette', 'Placeholder 1': 'Pour commencer', 'Placeholder 2': `Cliquez sur l'icône du bloc-notes à votre gauche`, 'Placeholder 3': 'et ouvrir/créer un fichier', 'Placeholder Or': 'ou', 'Modal 1': 'Effacer le contenu de la page ?', 'Modal 2': 'Un nouveau fichier existe déjà', 'Modal 3': 'Un nouveau dossier existe déjà', 'Modal 4': 'Ce nom existe déjà', 'Modal 5': 'Vous ne pouvez pas déplacer ce fichier ici !', Cancel: 'Annuler', OK: 'OK', Saved: 'Fichier enregistré !', 'Save Error': `Erreur lors de l'enregistrement du fichier`, 'Text Placeholder': 'Insérer du texte...', Press: 'Appuyez', 'Delete Item': "sur l'élément pour le supprimer", 'Rename Item': "sur l'élément pour le renommer", }; ================================================ FILE: src/common/locals/he.ts ================================================ export const HE_TRANSLATION = { 'My Notebooks': 'המחברות שלי', 'Command Bar': 'חיפוש ופקודות', 'Math Panel': 'זיכרון מתמטי', Archive: 'הארכיון שלי', 'New Folder': 'תיקייה חדשה', 'New File': 'קובץ חדש', 'Notebooks Tooltip': 'לחצו פעמיים כדי לפתוח את התיקייה', Variables: 'משתנים', Functions: 'פונקציות', Prompt: 'הקלידו פקודה או חפשו...', Preferences: 'העדפות משתמש', Theme: 'ערכת נושא', Color: 'צבע נושא', Dark: 'מצב חושך', Light: 'מצב אור', Language: 'שפה 🌍', Hebrew: 'עברית', Russian: 'רוסית - Русский', English: 'אנגלית - English', Spanish: 'ספרדית - Español', Arabic: 'ערבית - العربية', Hindi: 'הינדי - हिंदी', 'Mandarin Chinese': 'סינית מנדרינית - 中文普通话', French: 'צרפתית - Française', Red: 'אדום', Blue: 'כחול', Green: 'ירוק', Yellow: 'צהוב', Purple: 'סגול', Pink: 'ורוד', 'Text Block': 'הוספת בלוק טקסט', 'Math Block': 'הוספת בלוק מתמטי', 'Graph Block': 'הוספת בלוק גרפי', 'Draw Block': 'הוספת בלוק ציור', Divider: 'הוספת קו מפריד', Clear: 'ניקוי הדף', Save: 'שמירה', 'Add Tag': 'הוספת תגית', 'Placeholder 1': 'כדי להתחיל', 'Placeholder 2': 'לחצו על כפתור המחברות בצד ימין', 'Placeholder 3': 'ובחרו/צרו קובץ', 'Placeholder Or': 'או', 'Modal 1': 'לנקות את כל תוכן הדף?', 'Modal 2': 'קובץ חדש כבר קיים', 'Modal 3': 'תיקייה חדשה כבר קיימת', 'Modal 4': 'השם הזה כבר קיים', 'Modal 5': 'לא ניתן לגרור קובץ זה לכאן!', Cancel: 'ביטול', OK: 'אישור', Saved: 'הקובץ נשמר!', 'Save Error': 'התרחשה שגיאה בשמירת הקובץ', 'Text Placeholder': 'כתבו טקסט כאן...', Press: 'לחצו', 'Delete Item': 'על פריט כדי למחוק אותו', 'Rename Item': 'על פריט כדי לשנות את שמו', }; ================================================ FILE: src/common/locals/hi.ts ================================================ export const HI_TRANSLATION = { 'My Notebooks': 'नोटबुक', 'Command Bar': 'कमांड बार', 'Math Panel': 'गणित पैनल', Archive: 'आर्काइव', 'New Folder': 'नया फ़ोल्डर', 'New File': 'नया फ़ाइल', 'Notebooks Tooltip': 'जगह खोलने के लिए दोहराएँ क्लिक करें', Variables: 'वेरिएबल्स', Functions: 'फंक्शन्स', Prompt: 'कमांड टाइप करें या खोजें...', Preferences: 'प्राथमिकताएं', Theme: 'थीम', Color: 'एक्सेंट रंग', Dark: 'डार्क थीम', Light: 'लाइट थीम', Language: 'भाषा 🌍', Hebrew: 'हिब्रू - עברית', Russian: 'रूसी - Русский', English: 'अंग्रेज़ी - English', Arabic: 'अरबी - العربية', Spanish: 'स्पेनिश - Español', Hindi: 'हिंदी', 'Mandarin Chinese': 'मंडारिन चाइनीज़ - 中文普通话', French: 'फ़्रेंच - Française', Red: 'लाल', Blue: 'नीला', Green: 'हरा', Yellow: 'पीला', Purple: 'बैंगनी', Pink: 'गुलाबी', 'Text Block': 'टेक्स्ट ब्लॉक जोड़ें', 'Math Block': 'गणित ब्लॉक जोड़ें', 'Graph Block': 'ग्राफ़ ब्लॉक जोड़ें', 'Draw Block': 'ड्रॉ ब्लॉक जोड़ें', Divider: 'डिवाइडर जोड़ें', Clear: 'पृष्ठ साफ करें', Save: 'सहेजें', 'Add Tag': 'टैग जोड़ें', 'Placeholder 1': 'शुरू करने के लिए', 'Placeholder 2': 'अपने बाएँ नोटबुक', 'Placeholder 3': 'और फ़ाइल खोलें/बनाएं', 'Placeholder Or': 'या', 'Modal 1': 'पृष्ठ साफ़ करें?', 'Modal 2': 'एक नया फ़ाइल पहले से ही मौजूद है', 'Modal 3': 'एक नया फ़ोल्डर पहले से ही मौजूद है', 'Modal 4': 'यह नाम पहले से ही मौजूद है', 'Modal 5': 'आप इस फाइल को यहां नहीं ले जा सकते!', Cancel: 'रद्द करें', OK: 'ठीक है', Saved: 'फ़ाइल सहेजी गई!', 'Save Error': 'फ़ाइल सहेजते समय त्रुटि', 'Text Placeholder': 'टेक्स्ट डालें...', Press: 'प्रेस', 'Delete Item': 'इसे हटाने के लिए आइटम पर', 'Rename Item': 'इसका नाम बदलने के लिए आइटम पर', }; ================================================ FILE: src/common/locals/ru.ts ================================================ export const RU_TRANSLATION = { 'My Notebooks': 'Мои блокноты', 'Command Bar': 'Поиск и команды', 'Math Panel': 'Математическая панель', Archive: 'Архив', 'New Folder': 'Новая папка', 'New File': 'Новый файл', 'Notebooks Tooltip': 'Дважды щелкните, чтобы открыть папку', Variables: 'Переменные', Functions: 'Функции', Prompt: 'Введите команду или искать...', Preferences: 'Настройки пользователя', Theme: 'Тема', Color: 'Цвет темы', Dark: 'Темный режим', Light: 'Светлый режим', Language: 'Язык 🌍', Hebrew: 'Иврит - עברית', English: 'Английский - English', Russian: 'Русский', Arabic: 'арабский - العربية', Spanish: 'испанский - Español', Hindi: 'хинди - हिंदी', 'Mandarin Chinese': 'Мандариновый китайский - 中文普通话', French: 'Французский - Française', Red: 'Красный', Blue: 'Синий', Green: 'Зеленый', Yellow: 'Желтый', Purple: 'Фиолетовый', Pink: 'Розовый', 'Text Block': 'Добавить блок текста', 'Math Block': 'Добавить математический блок', 'Graph Block': 'Добавить блок графика', 'Draw Block': 'Добавить блок рисунка', Divider: 'Добавить разделитель', Clear: 'Очистить страницу', Save: 'Сохранить', 'Add Tag': 'Добавить тег', 'Placeholder 1': 'Чтобы начать', 'Placeholder 2': 'Нажмите на кнопку блокнотов в правой панели', 'Placeholder 3': 'Выберите/создайте файл', 'Placeholder Or': 'или', 'Modal 1': 'Очистить всё содержимое страницы?', 'Modal 2': 'Такой файл уже существует', 'Modal 3': 'Такая папка уже существует', 'Modal 4': 'Это имя уже используется', 'Modal 5': 'Вы не можете переместить этот файл сюда!', Cancel: 'Отмена', OK: 'ОК', Saved: 'Файл сохранен!', 'Save Error': 'Произошла ошибка при сохранении файла', 'Text Placeholder': 'Введите текст здесь...', Press: 'Нажмите', 'Delete Item': 'на элементе, чтобы удалить его', 'Rename Item': 'на элементе, чтобы переименовать его', }; ================================================ FILE: src/common/locals/zh.ts ================================================ //Mandarin Chinese export const ZH_TRANSLATION = { 'My Notebooks': '笔记本', 'Command Bar': '命令栏', 'Math Panel': '数学面板', Archive: '存档', 'New Folder': '新建文件夹', 'New File': '新建文件', 'Notebooks Tooltip': '双击打开位置', Variables: '变量', Functions: '函数', Prompt: '输入命令或搜索...', Preferences: '首选项', Theme: '主题', Color: '强调颜色', Dark: '暗色主题', Light: '亮色主题', Language: '语言 🌍', Hebrew: '希伯来语 - עברית', Russian: '俄语 - Русский', English: '英语 - English', Arabic: '阿拉伯语 - العربية', Spanish: '西班牙语 - Español', French: '法语 - Française', 'Mandarin Chinese': '中文普通话', Hindi: '印地语 - हिंदी', Red: '红色', Blue: '蓝色', Green: '绿色', Yellow: '黄色', Purple: '紫色', Pink: '粉红色', 'Text Block': '添加文本块', 'Math Block': '添加数学块', 'Graph Block': '添加图表块', 'Draw Block': '添加绘图块', Divider: '添加分隔线', Clear: '清空页面', Save: '保存', 'Add Tag': '添加标签', 'Placeholder 1': '开始', 'Placeholder 2': '单击左侧的笔记本图标', 'Placeholder 3': '并打开/创建文件', 'Placeholder Or': '或', 'Modal 1': '清空页面内容?', 'Modal 2': '一个新文件已经存在', 'Modal 3': '一个新文件夹已经存在', 'Modal 4': '此名称已经存在', 'Modal 5': '您不能在此处移动此文件', Cancel: '取消', OK: '确定', Saved: '文件已保存!', 'Save Error': '保存文件时出错', 'Text Placeholder': '插入文本...', Press: '按', 'Delete Item': '在项目上删除它', 'Rename Item': '在项目上重命名它', }; ================================================ FILE: src/common/shortcuts.ts ================================================ import { InlineShortcutDefinition } from "mathlive"; const ML_SHORTCUTS: Record = { 'sr': {after: 'letter+digit', value: '^2'}, 'cu': {after: 'letter+digit', value: '^3'}, '&': '\\&', '%': '\\%', '@': '\\degree', 'nCk': '\\binom', 'vec': '\\vec{#?}', 'gg': '\\hat{#?}', '^^': '\\land', 'tbl': '\\begin{array}{|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|} {#?} & {#?} \\\\ {#?} & {#?} \\end{array}', 'mx': '\\begin{matrix} {#?} & {#?} \\\\ {#?} & {#?} \\end{matrix}', 'mx22': '\\begin{matrix} {#?} & {#?} \\\\ {#?} & {#?} \\end{matrix}', 'mx32': '\\begin{matrix} {#?} & {#?} \\\\ {#?} & {#?} \\\\ {#?} & {#?} \\end{matrix}', 'mx42': '\\begin{matrix} {#?} & {#?} \\\\ {#?} & {#?} \\\\ {#?} & {#?} \\\\ {#?} & {#?} \\end{matrix}', 'mx23': '\\begin{matrix} {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\end{matrix}', 'mx33': '\\begin{matrix} {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\end{matrix}', 'mx43': '\\begin{matrix} {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} \\end{matrix}', 'mx24': '\\begin{matrix} {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\end{matrix}', 'mx34': '\\begin{matrix} {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\end{matrix}', 'mx44': '\\begin{matrix} {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\\\ {#?} & {#?} & {#?} & {#?} \\end{matrix}', 'cas': '\\begin{cases} {#?} & {#?} \\\\ {#?} & {#?} \\end{cases}', 'mul': '\\begin{gather} {#?} \\end{gather}', // Primes "''": '^{\\doubleprime}', '&a': '\\alpha', '&b': '\\beta', '&g': '\\gamma', '&d': '\\delta', '&e': '\\varepsilon', '&z': '\\zeta', '&et': '\\eta', '&t': '\\theta', '&i': '\\iota', '&k': '\\kappa', '&l': '\\lambda', '&m': '\\mu', '&n': '\\nu', '&x': '\\xi', '&o': '\\omicron', '&p': '\\pi', '&r': '\\rho', '&s': '\\sigma', '&ta': '\\tau', '&u': '\\upsilon', '&ph': '\\phi', '&c': '\\chi', '&ps': '\\psi', '&om': '\\omega', '&A': '\\Alpha', '&B': '\\Beta', '&G': '\\Gamma', '&D': '\\Delta', '&E': '\\Epsilon', '&Z': '\\Zeta', '&Et': '\\Eta', '&T': '\\Theta', '&I': '\\Iota', '&K': '\\Kappa', '&L': '\\Lambda', '&M': '\\Mu', '&N': '\\Nu', '&X': '\\Xi', '&O': '\\Omicron', '&P': '\\Pi', '&R': '\\Rho', '&S': '\\Sigma', '&Ta': '\\Tau', '&U': '\\Upsilon', '&Ph': '\\Phi', '&C': '\\Chi', '&Ps': '\\Psi', '&Om': '\\Omega', // Greek letters 'alpha': '\\alpha', 'delta': '\\delta', 'Delta': '\\Delta', 'pi': '\\pi', 'Pi': '\\Pi', 'theta': '\\theta', 'Theta': '\\Theta', '0': {after: 'letter', value: '_0'}, '1': {after: 'letter', value: '_1'}, '2': {after: 'letter', value: '_2'}, '3': {after: 'letter', value: '_3'}, '4': {after: 'letter', value: '_4'}, '5': {after: 'letter', value: '_5'}, '6': {after: 'letter', value: '_6'}, '7': {after: 'letter', value: '_7'}, '8': {after: 'letter', value: '_8'}, '9': {after: 'letter', value: '_9'}, 'x': {after: 'function', value: '(x)'}, 'an': 'a_n', 'ann': '\\{ a_n \\} _{n=1}^{\\infty}', // Letter-like 'ii': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\imaginaryI', }, 'jj': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\imaginaryJ', }, 'ee': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\exponentialE', }, 'nabla': '\\nabla', 'grad': '\\nabla', 'del': '\\partial', 'deg': { after: 'digit+space', value: '\\degree' }, 'infty': '\\infty', '\u221E': '\\infty', // @TODO: doesn't work // '∞': '\\infty', // '∞': '\\infty', 'oo': { after: 'nothing+digit+frac+surd+binop+relop+punct+array+openfence+closefence+space', value: '\\infty', }, // Big operators '∑': '\\sum', 'abs': '||{#?}||', 'sum': '\\sum_{#?}^{#?}', 'intt': '\\int', 'int': '\\int_{#?}^{#?}', 'intfx': '\\int_{#?}^{#?} f(x) \\differentialD x =', 'intgx': '\\int_{#?}^{#?} g(x) \\differentialD x =', 'inthx': '\\int_{#?}^{#?} h(x) \\differentialD x =', 'prod': '\\prod_{#?}^{#?}', 'nv': '\\sqrt[#?]{#?}', // '∫': '\\int', // There's a alt-B command for this '∆': '\\differentialD', // @TODO: is \\diffD most common? '∂': '\\differentialD', // Functions 'arcsin': '\\arcsin', 'arccos': '\\arccos', 'arctan': '\\arctan', 'arcsec': '\\arcsec', 'arccsc': '\\arccsc', 'arsinh': '\\arsinh', 'arcosh': '\\arcosh', 'artanh': '\\artanh', 'arcsech': '\\arcsech', 'arccsch': '\\arccsch', 'arg': '\\arg', 'ch': '\\ch', 'cosec': '\\cosec', 'cosh': '\\cosh', 'cot': '\\cot', 'cotg': '\\cotg', 'coth': '\\coth', 'csc': '\\csc', 'ctg': '\\ctg', 'cth': '\\cth', 'sec': '\\sec', 'sinh': '\\sinh', 'sh': '\\sh', 'tanh': '\\tanh', 'tg': '\\tg', 'th': '\\th', 'sin': '\\sin', 'cos': '\\cos', 'tan': '\\tan', 'lg': '\\lg', 'lb': '\\lb', 'log': '\\log', 'ln': '\\ln', 'exp': '\\exp', 'lim': '\\lim_{#?}', 'limfx': '\\lim_{x \\to a} f(x)=', 'limgx': '\\lim_{x \\to a} g(x)=', 'limhx': '\\lim_{x \\to a} h(x)=', // Differentials // According to ISO31/XI (ISO 80000-2), differentials should be upright 'dx': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\differentialD x', }, 'dy': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\differentialD y', }, 'dt': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\differentialD t', }, // Logic 'AA': '\\forall', 'EE': '\\exists', '!EE': '\\nexists', '&&': '\\land', 'VV': '\\lor', // The shortcut for the greek letter "xi" is interfering with "x in" 'xin': { after: 'nothing+text+relop+punct+openfence+space', value: 'x \\in', }, 'in': { after: 'nothing+letter+closefence', value: '\\in', }, '!in': '\\notin', 'sub': '\\subset', 'subb': '\\subseteq', 'set': '\\Set{ {#?} | {#?} }', // Sets 'NN': '\\mathbb{N}', // Natural numbers 'ZZ': '\\Z', // Integers 'QQ': '\\Q', // Rational numbers 'RR': '\\R', // Real numbers 'CC': '\\C', // Complex numbers // Operators 'xx': '\\times', '+-': '\\pm', '-+': '\\mp', // Relational operators '≠': '\\ne', '!=': '\\ne', '\u2265': '\\ge', '>=': '\\ge', '\u2264': '\\le', '<=': '\\le', '<<': '\\ll', '>>': '\\gg', '~~': '\\approx', // More operators '≈': '\\approx', '?=': '\\questeq', '÷': '\\div', '¬': '\\neg', 'not': '\\neg', ':=': '\\coloneq', '::': '\\coloneq', 'TT': '\\perp', 'II': '\\parallel', // Fences '(:': '\\langle', ':)': '\\rangle', // More Greek letters 'beta': '\\beta', 'chi': '\\chi', 'eps': '\\epsilon', 'varepsilon': '\\varepsilon', 'eta': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\eta', }, 'gamma': '\\gamma', 'Gamma': '\\Gamma', 'iota': '\\iota', 'kappa': '\\kappa', 'lambda': '\\lambda', 'Lambda': '\\Lambda', 'mu': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\mu', }, 'nu': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\nu', }, 'µ': '\\mu', // @TODO: or micro? 'phi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\phi', }, 'Phi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\Phi', }, 'varphi': '\\varphi', 'psi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\psi', }, 'Psi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\Psi', }, 'rho': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\rho', }, 'sigma': '\\sigma', 'Sigma': '\\Sigma', 'tau': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\tau', }, 'vartheta': '\\vartheta', 'ups': '\\upsilon', 'xi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space', value: '\\xi', }, 'Xi': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text', value: '\\Xi', }, 'zeta': '\\zeta', 'omega': '\\omega', 'Omega': '\\Omega', 'Ω': '\\omega', // @TODO: or ohm? // More Logic 'forall': '\\forall', 'exists': '\\exists', '!exists': '\\nexists', ':.': '\\therefore', '.:': '\\because', // MORE FUNCTIONS // 'arg': '\\arg', 'liminf': '\\liminf_{#?}', 'limsup': '\\limsup_{#?}', 'argmin': '\\operatorname*{arg~min}_{#?}', 'argmax': '\\operatorname*{arg~max}_{#?}', 'det': '\\det', 'mod': '\\mod', 'max': '\\max', 'min': '\\min', 'erf': '\\operatorname{erf}', 'erfc': '\\operatorname{erfc}', 'bessel': '\\operatorname{bessel}', 'mean': '\\operatorname{mean}', 'median': '\\operatorname{median}', 'fft': '\\operatorname{fft}', 'lcm': '\\operatorname{lcm}', 'gcd': '\\operatorname{gcd}', 'randomReal': '\\operatorname{randomReal}', 'randomInteger': '\\operatorname{randomInteger}', 'Re': '\\operatorname{Re}', 'Im': '\\operatorname{Im}', // UNITS 'mm': { after: 'nothing+digit+operator', value: '\\operatorname{mm}', // Millimeter }, 'cm': { after: 'nothing+digit+operator', value: '\\operatorname{cm}', // Centimeter }, 'km': { after: 'nothing+digit+operator', value: '\\operatorname{km}', // Kilometer }, 'kg': { after: 'nothing+digit+operator', value: '\\operatorname{kg}', // Kilogram }, // '||': '\\lor', '...': '\\ldots', // In general, use \ldots '+...': '+\\cdots', // ... but use \cdots after + ... '-...': '-\\cdots', // ... - and ... '->...': '\\to\\cdots', // -> 'to': '\\to', '->': '\\to', '|->': '\\mapsto', '-->': '\\longrightarrow', 'up': '\\nearrow', 'down': '\\searrow', // '<-': '\\leftarrow', '<--': '\\longleftarrow', '=>': '\\Rightarrow', '==': '\\equiv', '==>': '\\Longrightarrow', // '<=': '\\Leftarrow', // CONFLICTS WITH LESS THAN OR EQUAL '<=>': '\\Leftrightarrow', '<->': '\\leftrightarrow', '(.)': '\\odot', '(+)': '\\oplus', '(/)': '\\oslash', '(*)': '\\otimes', '(-)': '\\ominus', // '(-)': '\\circleddash', '||': '\\Vert', '{': '\\{', '}': '\\}', '*': '\\cdot', /* // // ASCIIIMath // // Binary operation symbols '**': '\\ast', '***': '\\star', '//': '\\slash', '\\\\': '\\backslash', 'setminus': '\\backslash', '|><': '\\ltimes', '><|': '\\rtimes', '|><|': '\\bowtie', '-:': '\\div', 'divide': '\\div', '@': '\\circ', 'o+': '\\oplus', 'ox': '\\otimes', 'o.': '\\odot', '^^': '\\wedge', '^^^': '\\bigwedge', 'vv': '\\vee', 'vvv': '\\bigvee', 'nn': '\\cap', 'nnn': '\\bigcap', 'uu': '\\cup', 'uuu': '\\bigcup', // Binary relation symbols '-=': '\\equiv', '~=': '\\cong', 'lt': '<', 'lt=': '\\leq', 'gt': '>', 'gt=': '\\geq', '-<': '\\prec', '-lt': '\\prec', '-<=': '\\preceq', // '>-': '\\succ', '>-=': '\\succeq', 'prop': '\\propto', 'diamond': '\\diamond', 'square': '\\square', 'iff': '\\iff', 'sub': '\\subset', 'sup': '\\supset', 'sube': '\\subseteq', 'supe': '\\supseteq', 'uarr': '\\uparrow', 'darr': '\\downarrow', 'rarr': '\\rightarrow', 'rArr': '\\Rightarrow', 'larr': '\\leftarrow', 'lArr': '\\Leftarrow', 'harr': '\\leftrightarrow', 'hArr': '\\Leftrightarrow', 'aleph': '\\aleph', // Logic 'and': '\\land', 'or': '\\lor', 'not': '\\neg', '_|_': '\\bot', 'TT': '\\top', '|--': '\\vdash', '|==': '\\models', // Other functions '|__': '\\lfloor', '__|': '\\rfloor', '|~': '\\lceil', '~|': '\\rceil', // Arrows '>->': '\\rightarrowtail', '->>': '\\twoheadrightarrow', '>->>': '\\twoheadrightarrowtail' */ }; export default ML_SHORTCUTS ================================================ FILE: src/main/Onboarding.js ================================================ const onboardingContent = { "blocks": [ { "w": 8, "h": 2, "x": 0, "y": 0, "i": "de34c56c-ffa2-467a-a769-f5827c8c8802", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-one", "children": [{ "text": "Welcome to Mathberet! 👋" }] } ] } }, { "w": 8, "h": 5, "x": 0, "y": 2, "i": "a1ec4ffb-20d8-4972-b5bb-9ef3efcc5711", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "paragraph", "children": [ { "text": "Mathberet is an open-source digital mathematics notebook written in React and TypeScript, designed for math students who need a platform for graphing, sketching, and writing in LaTeX. The goal here is to create a learning platform that allows students to write mathematical statements fluently and easily, in addition to sketching, graphing and arranging notes as freely as in a real-life notebook. We're also trying to keep the UI as clean and user-friendly as possible, allowing students to make the most out of their learning without thinking about the app itself. As a self-hosted application, Mathberet is both free and customizable, providing a reliable and efficient tool that works anywhere and anytime." } ] }, { "type": "paragraph", "children": [{ "text": "Important note:" }] }, { "type": "paragraph", "children": [ { "text": "🚧 Mathberet is still in development! 🚧 - If you encounter a bug, please open an issue on our GitHub repo!" } ] } ] } }, { "w": 3, "h": 4, "x": 0, "y": 10, "i": "4c8f483d-88a3-43a3-ac4c-5ed69a8af189", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-two", "children": [{ "text": "How do I add blocks?" }] }, { "type": "bulleted-list", "children": [ { "type": "list-item", "children": [ { "text": "Move your mouse to the bottom of this page to view the toolbar." } ] }, { "type": "list-item", "children": [{ "text": "Click on any of the buttons." }] } ] } ] } }, { "w": 8, "h": 1, "x": 0, "y": 7, "i": "b7fb5109-713d-4ee2-aa4b-d2734b87e466", "minW": 1, "maxW": 8, "minH": 1, "maxH": 1, "moved": false, "static": false, "type": 0, "metaData": {} }, { "w": 3, "h": 4, "x": 0, "y": 18, "i": "348f049f-3c34-4da9-bab4-6c49e637a494", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-two", "children": [{ "text": "How do I save files?" }] }, { "type": "bulleted-list", "children": [ { "type": "list-item", "children": [ { "text": "Move your mouse to the bottom of this page to view the toolbar." } ] }, { "type": "list-item", "children": [{ "text": "Click on the save button." }] } ] } ] } }, { "w": 3, "h": 4, "x": 0, "y": 14, "i": "9249ad8b-238a-4020-a521-c41f961c5973", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-two", "children": [{ "text": "How do I clear the page?" }] }, { "type": "bulleted-list", "children": [ { "type": "list-item", "children": [ { "text": "Move your mouse to the bottom of this page to view the toolbar." } ] }, { "type": "list-item", "children": [{ "text": "Click on the trash button." }] } ] } ] } }, { "w": 5, "h": 2, "x": 3, "y": 8, "i": "d90c4f52-e515-4017-8927-19c1e9c02640", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-one", "children": [{ "text": "Block Examples" }] } ] } }, { "w": 5, "h": 2, "x": 3, "y": 15, "i": "65e61c01-c999-411c-8c4d-4c7242d1a274", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 4, "metaData": { "content": "x_{1,2}=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}" } }, { "w": 5, "h": 3, "x": 3, "y": 12, "i": "630fc019-caff-43f3-a629-d91ffa34e635", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-three", "children": [{ "text": "Math block" }] }, { "type": "paragraph", "children": [ { "text": "In addition to writing in LaTeX, we've added dozens of keyboard shortcuts to help you write faster in math blocks. For example, try writing \"intfx\" in a math block. Keyboard shortcuts help menu coming soon!" } ] } ] } }, { "w": 5, "h": 3, "x": 3, "y": 17, "i": "997ff1a3-8919-4302-8d6a-47e3d8fb397d", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-three", "children": [{ "text": "Graph Block" }] }, { "type": "paragraph", "children": [ { "text": "Currently, the only functions supported in the graph block are trigonometric and polynomial functions. More functions to be supported soon!" } ] } ] } }, { "w": 5, "h": 5, "x": 3, "y": 20, "i": "4455135f-af3c-454d-bb87-d393b7814b1f", "minW": 2, "maxW": 8, "minH": 2, "maxH": 100, "moved": false, "static": false, "type": 1, "metaData": { "content": ["\\sin(x)\\cdot\\tan(x)"] } }, { "w": 5, "h": 4, "x": 3, "y": 27, "i": "2c7648cb-bad0-4d17-8101-b6044f2f12d3", "minW": 4, "maxW": 8, "minH": 4, "maxH": 100, "moved": false, "static": false, "type": 6, "metaData": { "content": { "shapes": [], "bindings": [], "assets": [] } } }, { "w": 5, "h": 2, "x": 3, "y": 25, "i": "bbd2df59-7777-4353-8ee2-c6e1942fba57", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-three", "children": [{ "text": "Drawing Block" }] }, { "type": "paragraph", "children": [ { "text": "Based on tldraw. Draw to your heart content!" } ] } ] } }, { "w": 5, "h": 2, "x": 3, "y": 10, "i": "64fbccd1-e25d-4a01-8749-aa4a23507519", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-three", "children": [{ "text": "Text Block" }] }, { "type": "paragraph", "children": [ { "text": "Written in markdown (currently limited to headers and unordered lists)." } ] } ] } }, { "w": 3, "h": 4, "x": 0, "y": 22, "i": "c0e9fa2b-6b44-44bc-a7b4-564ae5dbc6b5", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-two", "children": [{ "text": "How do I create a file/folder?" }] }, { "type": "bulleted-list", "children": [ { "type": "list-item", "children": [ { "text": "Click on the notebooks button in the left sidebar." } ] }, { "type": "list-item", "children": [ { "text": "Click on the create file/folder button." } ] } ] } ] } }, { "w": 3, "h": 4, "x": 0, "y": 26, "i": "6900c23a-d4b5-42c5-92a5-01c5b6f32e89", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [ { "type": "heading-two", "children": [{ "text": "How do I change themes?" }] }, { "type": "bulleted-list", "children": [ { "type": "list-item", "children": [ { "text": "Click on the command button in the left sidebar (or press Ctrl+Shift+P)." } ] }, { "type": "list-item", "children": [{ "text": "Click on Preferences -> Theme." }] } ] } ] } }, { "w": 3, "h": 2, "x": 0, "y": 8, "i": "d76c816e-9e3b-4f29-83b8-4f5c2104ef81", "minW": 1, "maxW": 8, "minH": 1, "maxH": 100, "moved": false, "static": false, "type": 3, "metaData": { "content": [{ "type": "heading-one", "children": [{ "text": "FAQ" }] }] } } ], "tags": [], "mathMemory": {} } export default onboardingContent; ================================================ FILE: src/main/app.ts ================================================ import { app, BrowserWindow } from 'electron'; import { createAppWindow } from './appWindow'; /** Handle creating/removing shortcuts on Windows when installing/uninstalling. */ if (require('electron-squirrel-startup')) { app.quit(); } /** * This method will be called when Electron has finished * initialization and is ready to create browser windows. * Some APIs can only be used after this event occurs. */ app.on('ready', createAppWindow); /** * Emitted when the application is activated. Various actions can * trigger this event, such as launching the application for the first time, * attempting to re-launch the application when it's already running, * or clicking on the application's dock or taskbar icon. */ app.on('activate', () => { /** * On OS X it's common to re-create a window in the app when the * dock icon is clicked and there are no other windows open. */ if (BrowserWindow.getAllWindows().length === 0) { createAppWindow(); } }); /** * Emitted when all windows have been closed. */ app.on('window-all-closed', () => { /** * On OS X it is common for applications and their menu bar * to stay active until the user quits explicitly with Cmd + Q */ if (process.platform !== 'darwin') { app.quit(); } }); /** * In this file you can include the rest of your app's specific main process code. * You can also put them in separate files and import them here. */ ================================================ FILE: src/main/appWindow.ts ================================================ import { app, BrowserWindow, ipcMain, shell } from 'electron'; import path from 'path'; import { registerTitlebarIpc } from '@misc/window/titlebarIPC'; import * as fs from 'fs'; const { resolve } = require('path'); const os = require('os'); import onboardingContent from './Onboarding'; // Electron Forge automatically creates these entry points declare const APP_WINDOW_WEBPACK_ENTRY: string; declare const APP_WINDOW_PRELOAD_WEBPACK_ENTRY: string; let appWindow: BrowserWindow; /** * Create Application Window * @returns {BrowserWindow} Application Window Instance */ export function createAppWindow(): BrowserWindow { // Create new window instance appWindow = new BrowserWindow({ width: 800, height: 600, minWidth: 700, minHeight: 400, backgroundColor: '#202020', show: false, autoHideMenuBar: true, frame: false, titleBarStyle: 'hidden', icon: resolve('assets/images/appIcon.ico'), webPreferences: { nodeIntegration: false, contextIsolation: true, nodeIntegrationInWorker: false, nodeIntegrationInSubFrames: false, preload: APP_WINDOW_PRELOAD_WEBPACK_ENTRY, sandbox: false, }, }); // Load the index.html of the app window. appWindow.loadURL(APP_WINDOW_WEBPACK_ENTRY); // Show window when its ready to appWindow.on('ready-to-show', () => appWindow.show()); // Register Inter Process Communication for main process registerMainIPC(); // Close all windows when main window is closed appWindow.on('close', () => { appWindow = null; app.quit(); }); return appWindow; } ipcMain.on('getOS', () => { let OS = ''; switch (os.platform()) { case "darwin": OS = "mac"; break; case "win32": OS = "windows" break; case "linux": OS = "linux" break; default: break; } appWindow.webContents.send('gotOS', OS); }); ipcMain.on('saveX', (event, data, filePath) => { const filesPath = path.join(app.getPath('documents'), 'Mathberet', 'files'); if (fs.existsSync(filesPath)) { fs.writeFileSync(path.join(filePath), data, 'utf-8'); } else { fs.mkdirSync(filesPath, { recursive: true }); fs.writeFileSync(path.join(filePath), data, 'utf-8'); } }); ipcMain.on('loadX', (event, filePath) => { if (fs.existsSync(filePath)) { fs.readFile(filePath, 'utf-8', (error, data) => { error ? console.error('Error Reading file: ', error) : appWindow.webContents.send('gotLoadedDataX', data); }); } else { console.error('File not found!'); } }); ipcMain.on('openFiles', () => { shell .openPath(path.join(app.getPath('documents'), 'Mathberet', 'files')) .catch((error) => { console.error(error); }); }); ipcMain.on('move', (event, oldDir, newDir) => { fs.renameSync(oldDir, newDir); }); ipcMain.on('delete', (event, path, isFolder) => { isFolder ? fs.rmSync(path, { recursive: true, force: true }) : fs.rmSync(path); }); ipcMain.on('load', (event, file) => { fs.readFile(file, 'utf-8', (error, data) => { appWindow.webContents.send('fromMain', data); }); }); ipcMain.on('newFile', (event, filePath) => { fs.writeFileSync(filePath, '{}'); }); ipcMain.on('newFolder', (event, folderPath) => { fs.mkdirSync(folderPath); }); let firstTime = true; function buildTree(dir: string, root: any) { const stats = fs.statSync(dir); let name = path.basename(dir).split('.')[0]; let key = dir; if (firstTime) { name = 'root'; key = 'root'; firstTime = false; } if (!stats.isDirectory()) { root[key] = { index: key, data: name, children: [], path: dir, isFolder: false, }; return key; } const children = fs .readdirSync(dir) .map((child) => buildTree(path.join(dir, child), root)); root[key] = { index: key, isFolder: true, data: name, children, path: dir, }; return key; } ipcMain.on('getNotebooks', () => { const filesPath = path.join(app.getPath('documents'), 'Mathberet', 'files'); if (!fs.existsSync(filesPath)) { fs.mkdirSync(filesPath, {recursive: true}); fs.writeFileSync(path.join(filesPath, "Welcome to Mathberet!.json"), JSON.stringify(onboardingContent)) } const root = {}; buildTree(filesPath, root); firstTime = true; appWindow.webContents.send('gotNotebooks', { filesPath, root }); }); ipcMain.on('getPicture', (event, id) => { const allPics = fs.readdirSync(path.join(__dirname, '..', 'attachments'), { withFileTypes: true, }); let foundPath, b64; for (const picture of allPics) { if (picture.name.split('.')[0] == id.toString()) { foundPath = path.join(__dirname, '..', 'attachments', picture.name); b64 = fs.readFileSync(foundPath, 'base64'); break; } } appWindow.webContents.send('gotPicture', `data:image/png;base64,${b64}`); return; }); ipcMain.on('getArchive', () => { const filesPath = path.join(__dirname, '..', 'files'); const groupsToFilter: [] = []; function getAllGroups() { const allGroups = []; const allFiles = fs.readdirSync(filesPath, { withFileTypes: true }); for (const file of allFiles) { if (file.isDirectory()) { const subFiles = fs.readdirSync(path.join(filesPath, file.name), { withFileTypes: true, }); for (const subfile of subFiles) { if (subfile.name.split('.')[1] == 'json') { const readFile = fs.readFileSync( path.join(filesPath, file.name, subfile.name), 'utf-8', ); for (const block of JSON.parse(readFile)) { if (block.type == 'Group') { allGroups.push(block); } } } } } else { if (file.name.split('.')[1] == 'json') { const readFile = fs.readFileSync( path.join(filesPath, file.name), 'utf-8', ); for (const block of JSON.parse(readFile)) { if (block.type == 'Group') { allGroups.push(block); } } } } } return allGroups; } const allGroups = getAllGroups(); function removeDups(arr: any[]) { const uniqueIds: any[] = []; const unique = arr.filter((element) => { const isDuplicate = uniqueIds.includes(element); if (!isDuplicate) { uniqueIds.push(element); return true; } return false; }); return unique.map( (groupTitle) => (groupTitle = { groupName: groupTitle, subGroups: [] }), ); } const finalArr = []; for (const group of removeDups(groupsToFilter)) { if (group.groupName != 'קבוצה') { for (const subGroup of allGroups) { if (subGroup.groupTitle == group.groupName) { group.subGroups.push(subGroup); } } finalArr.push(group); } } appWindow.webContents.send('gotArchive', finalArr); }); ipcMain.on('startSearch', (event, args) => { const filesPath = path.join(__dirname, '..', 'files'); function getAllBlocks() { const allGroups = []; const allFiles = fs.readdirSync(filesPath, { withFileTypes: true }); for (const file of allFiles) { if (file.isDirectory()) { const subFiles = fs.readdirSync(path.join(filesPath, file.name), { withFileTypes: true, }); for (const subfile of subFiles) { const readFile = fs.readFileSync( path.join(filesPath, file.name, subfile.name), 'utf-8', ); allGroups.push({ filePath: path.join(filesPath, file.name, subfile.name), fileName: subfile.name.replace('.json', ''), blocks: JSON.parse(readFile), }); } } else { const readFile = fs.readFileSync( path.join(filesPath, file.name), 'utf-8', ); allGroups.push({ filePath: path.join(filesPath, file.name), fileName: file.name.replace('.json', ''), blocks: JSON.parse(readFile), }); } } return allGroups; } const allGroups = getAllBlocks(); appWindow.webContents.send('gotAllBlocks', allGroups); }); /** * Register Inter Process Communication */ function registerMainIPC() { /** * Here you can assign IPC related codes for the application window * to Communicate asynchronously from the main process to renderer processes. */ registerTitlebarIpc(appWindow); } ================================================ FILE: src/renderer/app.html ================================================ מתברת
================================================ FILE: src/renderer/appPreload.tsx ================================================ import '@misc/window/windowPreload'; // Say something console.log('[ERWT] : Preload execution started'); // Get versions window.addEventListener('DOMContentLoaded', () => { const { env } = process; const versions: Record = {}; // ERWT Package version versions['erwt'] = env['npm_package_version']; versions['license'] = env['npm_package_license']; // Process versions for (const type of ['chrome', 'node', 'electron']) { versions[type] = process.versions[type].replace('+', ''); } // NPM deps versions for (const type of ['react']) { const v = env['npm_package_dependencies_' + type]; if (v) versions[type] = v.replace('^', ''); } // NPM @dev deps versions for (const type of ['webpack', 'typescript']) { const v = env['npm_package_devDependencies_' + type]; if (v) versions[type] = v.replace('^', ''); } // Set versions to app data // app.setAttribute('data-versions', JSON.stringify(versions)); }); ================================================ FILE: src/renderer/appRenderer.tsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import WindowFrame from '@misc/window/components/WindowFrame'; import Application from '@components/Application'; // Say something console.log('[ERWT] : Renderer execution started'); // Application to Render const app = (
); // Render application in DOM createRoot(document.getElementById('app')).render(app); ================================================ FILE: src/renderer/common/types.ts ================================================ import { TDShape, TDBinding, TDAsset } from '@tldraw/tldraw'; // eslint-disable-next-line import/named import { Descendant } from 'slate'; export enum WidgetType { Divider, Graph, Group, Text, Math, Picture, Draw, } export type canvasProps = { shapes: TDShape[]; bindings?: TDBinding[]; assets?: TDAsset[]; }; export type ValueProps = { blockStateFunction: (...args: unknown[]) => unknown; content: string | string[] | Descendant[] | canvasProps; }; export type BlockState = { id: string, metaData: {content: string | string[] | Descendant[] | canvasProps}, } export type FileStructure = { tags: string[]; blocks: Array; mathMemory: object; }; export type BlockElement = { type: WidgetType; metaData?: ValueProps; } & ReactGridLayout.Layout; export type newWidgetRequest = { widgetType: WidgetType; }; export type PageGridState = { items: BlockElement[]; breakpoint: string; cols: number; }; ================================================ FILE: src/renderer/components/Application.scss ================================================ /** * Licensed under the MIT License. See LICENSE file in the project root for license information. * Copyright (c) 2022 Codesbiome, guasam * * @author : guasam * @project : ERWT Boilerplate * @package : Application Sass Stylesheet */ @import '../../../assets/icons/uicons.css'; @import './Theme.scss'; @import './Fonts.css'; ::selection { background: var(--selection-bgcolor); color: var(--selection-color); } ::-webkit-scrollbar { width: var(--scroll-width); &-track { background: transparent; } &-thumb { background: var(--scroll-thumb-bgcolor); border-radius: 50px; } &-thumb:hover { background: var(--scroll-thumb-hover-bgcolor); } } html, body, #app { transition: 0.3s; height: 100%; overflow: hidden; direction: var(--app-direction); } body { margin: 0; margin-right: 0 !important; font-size: var(--app-font-size); font-family: var(--app-font-family); color: var(--app-color); background: var(--app-bgcolor); line-height: 1.5; } h1 { margin: 0; } #app { display: flex; flex-direction: column; box-sizing: border-box; -webkit-user-select: none; user-select: none; &.has-border { border: var(--app-border-color); } } button { background: var(--button-bgcolor); color: var(--button-color); font-weight: normal; text-shadow: 0px 1px var(--button-shadow-color); font-family: var(--app-font-family); border: none; padding: 0.5rem 1rem; font-size: 0.875rem; cursor: pointer; transition: 0.1s; display: inline-flex; justify-content: space-around; align-items: center; outline: none; &.tool { background-color: #1a2428; } &:hover { background: var(--button-hover-bgcolor); } &:active { background: var(--button-active-bgcolor); } } .rotate { animation: rotate 4.5s linear infinite; } @keyframes rotate { to { transform: rotate(360deg); } } .hidden { display: none !important; } .center { text-align: center; } #main-app { display: flex; flex-direction: column; height: 100%; gap: 5px; overflow-x: hidden; .workspace { width: calc(100% - 20px); align-self: center; height: 100%; display: flex; flex-direction: row; direction: var(--app-direction); justify-content: center; gap: 10px; .math-sidebar.open { width: 100%; min-width: 230px; max-width: 15vw; } .math-sidebar { transition: 0.2s; width: 45px; min-width: 45px; max-width: 45px; height: 100%; display: flex; flex-direction: column; gap: 20px; } } } ================================================ FILE: src/renderer/components/Application.tsx ================================================ import React from 'react'; import '../../../node_modules/react-grid-layout/css/styles.css'; import '../../../node_modules/react-resizable/css/styles.css'; import './Application.scss'; import { CommandBar } from './CommandBar/CommandBar'; import FilesSidebar from './FilesSidebar/FilesSidebar'; import { GeneralContextProvider } from './GeneralContext'; import Header from './Header/Header'; import MathSidebar from './MathSidebar/MathSidebar'; import Page from './Page/Page'; const Application = () => { return (
); }; export default Application; ================================================ FILE: src/renderer/components/CommandBar/CommandBar.scss ================================================ @import '../Theme.scss'; .command-bar { &-search { padding: 12px 16px; font-size: 16px; width: 100%; font-family: 'Rubik2', 'NotoSansArabic'; box-sizing: border-box; outline: none; border: none; background: transparent; color: hsla(var(--app-text-color), 1); } &-result { padding: 14px 10px; border-radius: 8px; font-size: 16px; user-select: none; background: transparent; &.active { background: hsla(var(--theme-hue), 50%, 50%, 0.1); } } } body:has(#main-app.rtl) { .command-bar-search, .command-bar-result { direction: rtl !important; } } ================================================ FILE: src/renderer/components/CommandBar/CommandBar.tsx ================================================ import React from 'react'; import './CommandBar.scss'; import { KBarPortal, KBarPositioner, KBarAnimator, KBarResults, KBarSearch, useMatches, useRegisterActions, } from 'kbar'; import { useTranslation } from 'react-i18next'; import { useGeneralContext } from '@components/GeneralContext'; export function CommandBar() { const { t } = useTranslation(); const { actions } = useGeneralContext(); function RenderResults() { // updating the actions using `useRegisterActions` *before* // getting the results from `useMatches` so the results // are always up to date. the actions are also up to date // cause they are now stateful useRegisterActions(actions); const { results } = useMatches(); return ( typeof item === 'string' ? (
{item}
) : (
{item.name}
) } /> ); } const animatorStyle = { maxWidth: '600px', width: '100%', zIndex: '10000', padding: '10px 8px', outline: '1px solid var(--page-border)', backdropFilter: 'blur(7px)', background: 'var(--command-bar-bg)', color: 'hsla(var(--app-text-color), 1)', borderRadius: '12px', boxShadow: '0 0 10px 1px rgba(0, 0, 0, .25)', overflow: 'hidden', }; return ( ); } ================================================ FILE: src/renderer/components/FilesSidebar/FileSystem.scss ================================================ @import '~react-complex-tree/lib/style-modern.css'; @import '../Theme.scss'; #main-app.rtl .rct-tree-item-title-container { margin-inline-end: 10px !important; margin-inline-start: unset !important; } .file-system { display: flex; flex-direction: column; width: 100%; height: 100%; min-width: 190px; max-width: 230px; .file-system-header { display: flex; flex-direction: row; direction: var(--app-direction); height: max-content; padding-bottom: 10px; padding-top: 3px; align-items: center; .file-system-header-title { cursor: alias; color: hsla(var(--app-text-color), 0.6); white-space: nowrap; font-size: 21px; font-weight: 700; width: 100%; &:hover::after { content: attr(data-tooltip); color: hsla(var(--app-text-color), 1); font-size: 14px; font-weight: 400; min-width: max-content; position: fixed; margin-top: 40px; margin-inline-start: -130px; z-index: 1000; padding: 6px 10px 6px 10px; background-color: var(--button-bgcolor); text-shadow: none; border: 1px solid var(--page-border); border-radius: 6px; opacity: 0%; animation: tooltip 0.1s ease-in-out 0.2s forwards; } } .file-system-header-buttons { border-radius: 10px; display: flex; flex-direction: row; direction: var(--app-direction); width: max-content; gap: 3px; button { background: none; padding: 5px !important; color: var(--tool-color); transition: 0.1s; &:hover { background: none; color: var(--button-color); &::after { content: attr(data-tooltip); color: hsla(var(--app-text-color), 1); min-width: max-content; position: fixed; margin-top: 80px; z-index: 1000; padding: 6px 10px 6px 10px; background-color: var(--button-bgcolor); text-shadow: none; border: 1px solid var(--page-border); border-radius: 6px; opacity: 0%; animation: tooltip 0.1s ease-in-out 0.2s forwards; } } &:active { transform: scale(0.92); } i { font-size: 18px; height: 18px; } } } } .files-tree-container { width: 100%; display: flex; flex-direction: column; justify-content: space-between; min-width: inherit; max-width: inherit; .rct-tree-root { height: 90%; [role='tree'] { height: inherit; & > .rct-tree-items-container { height: inherit; [role='treeitem'] { input { text-shadow: none; font-family: 'Rubik2', 'NotoSansArabic'; font-size: 16px; height: 40px; } .rct-tree-item-title-container { margin-inline-start: 10px; margin-inline-end: unset; padding-inline-end: 10px !important; padding-right: 0px !important; padding-left: 0px !important; .rct-tree-item-button { overflow: hidden; min-width: inherit; text-overflow: ellipsis; white-space: nowrap; height: 100%; direction: var(--app-direction); background-color: transparent; color: hsla(var(--app-text-color), 1); border-radius: 10px; text-shadow: none; font-family: 'Rubik2', 'NotoSansArabic'; font-size: 16px; height: 40px; flex-direction: row; justify-content: flex-start; padding-inline-start: 10px; // direction: ltr; // padding: 0 var(--rct-item-padding) 0 // calc( // var(--rct-item-padding) + var(--rct-arrow-container-size) + // var(--rct-arrow-padding) // ); // margin-right: calc(-1 * var(--rct-arrow-size)); // margin-left: calc(-1 * var(--rct-arrow-size)); // padding-right: 12px; cursor: var(--rct-cursor); transition: color 100ms ease-out, background-color 100ms ease-out, font-weight 100ms ease-in; &:hover { background-color: var(--file-hover-bgcolor); color: hsla(var(--app-text-color), 1); } &:active { background-color: var(--file-active-bgcolor); color: hsla(var(--app-text-color), 1); } &::before { // Override position: unset; top: unset; left: unset; height: unset; width: unset; background-color: unset; border-radius: unset; // Custom color: var(--tool-color); font-family: 'uicons-regular-rounded'; font-weight: 300; font-size: large; content: '\f35d'; padding-inline-end: 10px !important; // padding-left: 10px; } &.rct-tree-item-button-isFolder { &::after { display: none; } &.rct-tree-item-button-expanded { &::before { font-family: 'uicons-regular-rounded'; font-weight: 300; font-size: large; content: '\f1de'; } } &::before { font-family: 'uicons-regular-rounded'; font-weight: 300; font-size: large; content: '\f1e6'; } } } .rct-tree-item-button-focused { background-color: var(--selected-transparent); } .rct-tree-item-button-expanded { background-color: var(--selected-bgcolor); color: hsla(var(--app-text-color), 1); font-weight: 700; &:hover { background-color: var(--selected-hover-bgcolor); } &:active { background-color: var(--selected-active-bgcolor); } } .rct-tree-item-arrow { display: none; } } [role='group'] { height: fit-content; // padding-top: 10px; padding-inline-start: 20px; } } } } } .instruction-p { color: hsla(var(--app-text-color), 0.6); font-size: smaller; .button-text { background-color: var(--selected-bgcolor); padding: 2px 6px; border-radius: 15px; } } } } @keyframes tooltip { from { transform: scale(0.95); opacity: 0%; box-shadow: 0px 1px 3px #00000000; } to { transform: scale(1); opacity: 100%; box-shadow: 0px 1px 3px #0000002a; } } ================================================ FILE: src/renderer/components/FilesSidebar/FileSystem.tsx ================================================ /* eslint-disable import/named */ import ErrorModal from '@components/common/Modals/ErrorModal'; import { useGeneralContext } from '@components/GeneralContext'; import React, { SetStateAction, useEffect, useState } from 'react'; import { Tree, ControlledTreeEnvironment, DraggingPositionItem, TreeItem, DraggingPositionBetweenItems, TreeItemIndex, } from 'react-complex-tree'; import './FileSystem.scss'; import { draggedToTheSameParent, updateItemsPosition, changeItemPath, generateStateWithNewFolder, newFolderName, generateStateWithNewFile, newFileName, itemExistsInParent, getFileNameFromPath, deleteItemFromItsPreviousParent, getParent, } from './FileSystemHelpers'; import { MathTreeItem, TreeItemsObj } from './types'; import { useTranslation } from 'react-i18next'; type receivedProps = { filesPath: string; root: SetStateAction }; declare global { interface Window { api: any; } } function FileSystem() { const { t } = useTranslation(); const { setSelectedFile } = useGeneralContext(); const [items, setItems] = useState({ root: { index: 'root', data: '', path: '', }, }); const [errorModalContent, setErrorModalContent] = useState(''); const [errorModalOpen, setErrorModalOpen] = useState(false); const [expandedItems, setExpandedItems] = useState([]); const [selectedItems, setSelectedItems] = useState([]); const [selectedDirectory, setSelectedDirectory] = useState('root'); const [focusedItem, setFocusedItem] = useState(-1); useEffect(() => { window.api.getNotebooks(); }, [items]); useEffect(() => { window.api.receive('gotNotebooks', (data: receivedProps) => { setItems(data.root); }); }, []); const handleOnDrop = ( draggedItems: TreeItem[], target: DraggingPositionItem | DraggingPositionBetweenItems, ) => { setItems((prev) => { // Handle D&D intentionally only for one item const draggedItem = draggedItems[0]; if (draggedToTheSameParent(prev, draggedItem, target)) return prev; let dest: TreeItemIndex = ''; if ( 'targetItem' in target && ((target as DraggingPositionItem | DraggingPositionBetweenItems) .targetType !== 'item' || prev[target.targetItem].isFolder) ) { dest = target.targetItem; } else { dest = target.parentItem; } if (dest) { for (const item of items[dest].children) { if ( getFileNameFromPath(item as string) === draggedItem.data && items[item].isFolder === draggedItem.isFolder ) { setErrorModalContent(t('Modal 5')); setErrorModalOpen(true); return prev; } } } return updateItemsPosition(prev, draggedItem, target); }); }; const addFolder = () => { if (itemExistsInParent(newFolderName, selectedDirectory, items, true)) { setErrorModalContent(t('Modal 3')); setErrorModalOpen(true); return; } setItems((prev) => generateStateWithNewFolder(prev, selectedDirectory)); }; const addFile = () => { if (itemExistsInParent(newFileName, selectedDirectory, items, false)) { setErrorModalContent(t('Modal 2')); setErrorModalOpen(true); return; } setItems((prev) => generateStateWithNewFile(prev, selectedDirectory)); }; const handleRenameItem = (item: MathTreeItem, name: string): void => { if ( itemExistsInParent( name, getParent(items, item.index).index, items, item.isFolder, ) ) { setErrorModalContent(t('Modal 4')); setErrorModalOpen(true); } else { setItems((prev) => { let newPath: string; const oldPath = item.index; if (item.isFolder) { const split = item.path.split('\\'); split.pop(); split.push(name); newPath = split.join('\\'); } else { const index = item.path.length - (item.data + '.json').length; const dirName = item.path.slice(0, index); newPath = dirName + name + '.json'; } let newItems = { ...prev }; newItems = changeItemPath(newItems, item, newPath); for (const [, value] of Object.entries(newItems)) { const mathTreeItem = value as MathTreeItem; if (mathTreeItem.children.includes(oldPath)) { mathTreeItem.children = mathTreeItem.children.filter( (child) => child !== oldPath, ); mathTreeItem.children.push(newPath); } } return newItems; }); } }; const handleClickedOutsideItem = ( event: React.MouseEvent, ) => { const target = event.target as HTMLElement; if (target.classList.contains('rct-tree-items-container')) { setSelectedDirectory('root'); setFocusedItem(-1); setSelectedItems([]); setExpandedItems([]); } }; const handleDeleteItem = (event: React.KeyboardEvent) => { if (event.key === 'Delete' && focusedItem != -1) { window.api.delete(focusedItem, items[focusedItem].isFolder); setItems((prev) => { const newItems = { ...prev }; const item = newItems[focusedItem]; deleteItemFromItsPreviousParent(newItems, item); if (item.isFolder) { for (const [key] of Object.entries(newItems)) { if (key.startsWith(focusedItem as string)) { delete newItems[key]; } } } return newItems; }); setFocusedItem(-1); setSelectedDirectory('root'); } }; return (
window.api.openFiles()} > {t('My Notebooks')}
{/* */} item.data} canSearch={false} keyboardBindings={{ renameItem: ['shift+R'] }} viewState={{ ['fileSystem']: { focusedItem, expandedItems, selectedItems, }, }} onDrop={handleOnDrop} onFocusItem={(item) => { const mathTreeItem = item as MathTreeItem; setFocusedItem(mathTreeItem.index); item.isFolder ? setSelectedDirectory(mathTreeItem.index) : setSelectedFile(mathTreeItem.path); }} onExpandItem={(item) => setExpandedItems([...expandedItems, item.index]) } onCollapseItem={(item) => setExpandedItems( expandedItems.filter( (expandedItemIndex) => expandedItemIndex !== item.index, ), ) } onSelectItems={setSelectedItems} onRenameItem={handleRenameItem} >

{t('Press')} Delete{' '} {t('Delete Item')}

{t('Press')} Shift+R{' '} {t('Rename Item')}

setErrorModalOpen(false)} > {errorModalContent}
); } export default FileSystem; ================================================ FILE: src/renderer/components/FilesSidebar/FileSystemHelpers.ts ================================================ /* eslint-disable import/named */ import { TreeItem, DraggingPositionItem, DraggingPositionBetweenItems, TreeItemIndex, } from 'react-complex-tree'; import { TreeItemsObj, MathTreeItem } from './types'; export const draggedToTheSameParent = ( prev: TreeItemsObj, item: TreeItem, target: DraggingPositionItem | DraggingPositionBetweenItems, ): boolean => { let draggedToSameParent; if (target.targetType === "item" && prev[target.targetItem].isFolder) { draggedToSameParent = prev[target.targetItem].children.includes(item.index); } else { draggedToSameParent = prev[target.parentItem].children.includes(item.index); } return draggedToSameParent; }; export const changeItemPath = (prev: TreeItemsObj, item: MathTreeItem, newPath: string) => { const oldPath = item.path; window.api.move(oldPath, newPath) const { [oldPath]: _, ...rest } = prev; const newState = { ...rest, [newPath]: { ...item, index: newPath, path: newPath, }, } return newState; }; export const addItemToNewParent = ( target: DraggingPositionItem | DraggingPositionBetweenItems, prev: TreeItemsObj, item: MathTreeItem, ) => { if (target.targetType != 'item') { const newPath = prev[target.parentItem].path + '\\' + item.data + (item.isFolder ? '' : '.json'); prev = changeItemPath(prev, item, newPath); prev[target.parentItem].children.push(newPath); } else { if (prev[target.targetItem].isFolder) { const newPath = prev[target.targetItem].path + '\\' + item.data + (item.isFolder ? '' : '.json'); prev = changeItemPath(prev, item, newPath); prev[target.targetItem].children.push(newPath); } else { for (const [, value] of Object.entries(prev)) { const mathTreeItem = value as MathTreeItem; if (mathTreeItem.children.includes(target.targetItem)) { const newPath = mathTreeItem.path + '\\' + item.data + (item.isFolder ? '' : '.json'); prev = changeItemPath(prev, item, newPath); mathTreeItem.children.push(newPath); } } } } return prev; }; export const updateItemsPosition = ( prev: TreeItemsObj, item: TreeItem, target: DraggingPositionItem | DraggingPositionBetweenItems, ) => { deleteItemFromItsPreviousParent(prev, item); if ((target as DraggingPositionItem).targetItem == 'root') return prev; return addItemToNewParent(target, prev, item as MathTreeItem); }; export const deleteItemFromItsPreviousParent = ( prev: TreeItemsObj, item: TreeItem, ) => { for (const [, value] of Object.entries(prev)) { const mathItemTree = value as MathTreeItem; if (mathItemTree.children.includes(item.index)) { mathItemTree.children = mathItemTree.children.filter( (child) => child !== item.index, ); } } }; export const newFolderName = 'New Folder'; export const generateStateWithNewFolder = ( prev: TreeItemsObj, parentIndex: TreeItemIndex, ) => { let parentValue; let parentKey; if (parentIndex != 'root') { parentValue = prev[parentIndex]; parentKey = parentIndex; } else { parentValue = prev['root']; parentKey = 'root'; } // TODO: format \\ and / correctly const newFolderPath = parentValue.path + '\\' + newFolderName; parentValue.children.push(newFolderPath); const newState = { ...prev, [parentKey]: parentValue, [newFolderPath]: { index: newFolderPath, data: newFolderName, children: [] as TreeItemIndex[], path: newFolderPath, isFolder: true, }, }; window.api.newFolder(newFolderPath); return newState; }; export const newFileName = 'New File'; export const generateStateWithNewFile = ( prev: TreeItemsObj, parentIndex: TreeItemIndex, ) => { let parentValue; let parentKey; if (parentIndex != 'root') { parentValue = prev[parentIndex]; parentKey = parentIndex; } else { parentValue = prev['root']; parentKey = 'root'; } // TODO: format \\ and / correctly const newFilePath = parentValue.path + '\\' + newFileName + '.json'; parentValue.children.push(newFilePath); const newState = { ...prev, [parentKey]: parentValue, [newFilePath]: { index: newFilePath, data: newFileName, children: [] as Array, path: newFilePath, isFolder: false, }, }; window.api.newFile(newFilePath); return newState; }; export function itemExistsInParent( name: string, parent: TreeItemIndex, items: TreeItemsObj, folder: boolean, ): boolean { const parentItem = items[parent]; if (!parentItem || !parentItem.children) { return false; // the parent directory does not exist or is not a folder } for (const childIndex of parentItem.children) { const childItem = items[childIndex]; if (getFileNameFromPath(childItem.path) === name) { // if the name matches, check if the item is a folder or a file return folder ? childItem.isFolder === true : !childItem.isFolder; } } return false; // did not find a matching item } export const getFileNameFromPath = (path: string) => { // TODO: format \\ and / correctly return path.split('\\').pop().split('.')[0]; } export const getParent = (items: TreeItemsObj, childIndex: TreeItemIndex) => { for (const [, value] of Object.entries(items)) { const mathItemTree = value as MathTreeItem; if (mathItemTree.children.includes(childIndex)) { return mathItemTree; } } } ================================================ FILE: src/renderer/components/FilesSidebar/FilesSidebar.scss ================================================ .files-sidebar { transition: 0.2s; display: flex; flex-direction: row; width: 45px; min-width: 45px; max-width: 45px; gap: 10px; .basic { transition: 0.2s; height: 100%; display: flex; flex-direction: column; direction: var(--app-direction); justify-content: space-between; align-items: flex-start; section { display: flex; flex-direction: column; direction: var(--app-direction); align-items: flex-start; gap: 8px; transition: 0.2s; .sidebar-button { transition: 0.2s; background-color: transparent; width: 45px; height: 45px; border-radius: 15px; cursor: default; &.open { background-color: var(--page-border); .fi { color: var(--tool-color); } } &:hover::after { content: attr(data-tooltip); color: hsla(var(--app-text-color), 1); min-width: 95px; position: fixed; margin-inline-start: 170px; z-index: 1000; padding: 6px 10px 6px 10px; background-color: var(--button-bgcolor); text-shadow: none; border: 1px solid var(--page-border); border-radius: 6px; opacity: 0%; animation: tooltip 0.1s ease-in-out 0.3s forwards; } &:active::after { display: none; } &:hover { .fi { color: var(--tool-color); } } &:active { transform: scale(0.9); } .fi { transition: 0.2s color; margin: auto; font-size: 22px; color: var(--sidebar-button-color); text-shadow: none; padding-top: 3px; } } } section#bottom { padding-bottom: 10px; } } .extension { width: 45px; height: 100%; } &.open { width: 100%; animation: openSidebar 0s linear 0.3s; min-width: 230px; animation-fill-mode: forwards; max-width: 15vw; .extension { min-width: 210px; max-width: 15vw; width: 100%; } } } @keyframes openSidebar { from { min-width: 230px; } to { min-width: max-content; } } @keyframes tooltip { from { transform: scale(0.95); opacity: 0%; box-shadow: 0px 1px 3px #00000000; } to { transform: scale(1); opacity: 100%; box-shadow: 0px 1px 3px #0000002a; } } ================================================ FILE: src/renderer/components/FilesSidebar/FilesSidebar.tsx ================================================ import React from 'react'; import './FilesSidebar.scss'; import SidebarButton from './SidebarButton'; import FileSystem from './FileSystem'; import { useKBar } from 'kbar'; import { useGeneralContext } from '@components/GeneralContext'; import { useTranslation } from 'react-i18next'; const FilesSidebar = () => { const { t, i18n } = useTranslation(); const { query } = useKBar(); const { setIsMathSidebarOpen, setIsFilesSidebarOpen, isMathSidebarOpen, isFilesSidebarOpen, } = useGeneralContext(); const handleOnClickFiles = () => { setIsFilesSidebarOpen((isFilesSidebarOpen: boolean) => !isFilesSidebarOpen); }; const handleOnClickMathPanel = () => { setIsMathSidebarOpen((isMathSidebarOpen: boolean) => !isMathSidebarOpen); }; return (
query.toggle()} />
{isFilesSidebarOpen && (
)}
); }; export default FilesSidebar; ================================================ FILE: src/renderer/components/FilesSidebar/SidebarButton.tsx ================================================ import React from 'react'; import '../Application.scss'; import './FilesSidebar.scss'; type SidebarButtonProps = { title: string; state?: boolean; buttonType: string; icon?: string; onClick?: React.MouseEventHandler; }; const SidebarButton = ({ buttonType, state, title, icon, onClick, }: SidebarButtonProps) => { const iconName = `fi fi-rr-${icon}` return (
); }; export default SidebarButton; ================================================ FILE: src/renderer/components/FilesSidebar/types.ts ================================================ // eslint-disable-next-line import/named import { TreeItem } from 'react-complex-tree'; export type MathTreeItem = { path: string; } & TreeItem; export type TreeItemsObj = { [key: string]: MathTreeItem; }; ================================================ FILE: src/renderer/components/Fonts.css ================================================ @import "../../../node_modules/computer-modern/cmu-serif.css"; @font-face { font-family: 'Rubik2'; src: url('../../../assets/webfonts/Rubik-VariableFont_wght.woff2') format('woff2-variations'); font-weight: 100 900; } @font-face { font-family: 'NotoSansArabic'; src: url('../../../assets/webfonts/NotoSansArabic-VariableFont_wght.woff2') format('woff2-variations'); font-weight: 100 900; } ================================================ FILE: src/renderer/components/GeneralContext.tsx ================================================ import React, { createContext, PropsWithChildren, useContext, useEffect, useState, } from 'react'; import { newWidgetRequest } from '@renderer/common/types'; import i18n from '@common/i18n'; import { Action, KBarProvider } from 'kbar'; const GeneralContext = createContext(null); function GeneralContextProvider({ children }: PropsWithChildren) { // Needs to reordered properly const getDefaultLang = () => { return localStorage.getItem('language') ? localStorage.getItem('language') : 'en'; } const [saveRequest, setSaveRequest] = useState({ cmd: '' }); const [clearPageRequest, setClearPageRequest] = useState({ cmd: '' }); const [newWidgetRequest, setNewWidgetRequest] = useState(); const [selectedFile, setSelectedFile] = useState(); const [currentFileTags, setCurrentFileTags] = useState([]); const [isFilesSidebarOpen, setIsFilesSidebarOpen] = useState(false); const [isMathSidebarOpen, setIsMathSidebarOpen] = useState(false); const [isRtl, setIsRtl] = useState(true); const [language, setLanguage] = useState(getDefaultLang()); const [darkTheme, setDarkTheme] = useState(true); const [colorTheme, setColorTheme] = useState(''); const [currentOS, setCurrentOS] = useState(''); const setColor = (name: string, hue: number) => { localStorage.setItem('color', name); document.documentElement.style.setProperty('--theme-hue', hue.toString()); }; const setLang = (language: string) => { i18n.changeLanguage(language); setLanguage(language); localStorage.setItem('language', language); if (isRtlLang(language)) { document.querySelector('#main-app').classList.add('rtl'); } else { document.querySelector('#main-app').classList.remove('rtl'); } }; const isRtlLang = (language: string) => { const rtlLanguages = ['he', 'ar', 'ur']; return rtlLanguages.includes(language); }; const setTheme = (theme: number) => { localStorage.setItem('dark-mode', theme.toString()); switch (theme) { case 0: document.body.classList.remove('dark-mode'); break; case 1: document.body.classList.add('dark-mode'); break; } }; const actions: Action[] = [ { id: 'preferences', name: i18n.t('Preferences'), }, { id: 'language', name: i18n.t('Language'), parent: 'preferences', }, { id: 'theme', name: i18n.t('Theme'), parent: 'preferences', }, { id: 'color', name: i18n.t('Color'), parent: 'preferences', }, { id: 'arabic', name: i18n.t('Arabic'), perform: () => { setLang('ar'); }, parent: 'language', }, { id: 'english', name: i18n.t('English'), perform: () => { setLang('en'); }, parent: 'language', }, { id: 'hebrew', name: i18n.t('Hebrew'), perform: () => { setLang('he'); }, parent: 'language', }, { id: 'hindi', name: i18n.t('Hindi'), perform: () => { setLang('hi'); }, parent: 'language', }, { id: 'mandarin Chinese', name: i18n.t('Mandarin Chinese'), perform: () => { setLang('zh'); }, parent: 'language', }, { id: 'french', name: i18n.t('French'), perform: () => { setLang('fr'); }, parent: 'language', }, { id: 'russian', name: i18n.t('Russian'), perform: () => { setLang('ru'); }, parent: 'language', }, { id: 'spanish', name: i18n.t('Spanish'), perform: () => { setLang('es'); }, parent: 'language', }, { id: 'blue', name: i18n.t('Blue'), perform: () => { setColor('blue', 210); }, parent: 'color', }, { id: 'pink', name: i18n.t('Pink'), perform: () => { setColor('pink', 300); }, parent: 'color', }, { id: 'yellow', name: i18n.t('Yellow'), perform: () => { setColor('yellow', 35); }, parent: 'color', }, { id: 'purple', name: i18n.t('Purple'), perform: () => { setColor('purple', 250); }, parent: 'color', }, { id: 'red', name: i18n.t('Red'), perform: () => { setColor('red', 0); }, parent: 'color', }, { id: 'green', name: i18n.t('Green'), perform: () => { setColor('green', 140); }, parent: 'color', }, { id: 'light', name: i18n.t('Light'), perform: () => { setTheme(0); }, parent: 'theme', }, { id: 'dark', name: i18n.t('Dark'), perform: () => { setTheme(1); }, parent: 'theme', }, ]; useEffect(() => { const useDarkTheme = parseInt(localStorage.getItem('dark-mode')); if (isNaN(useDarkTheme)) { setDarkTheme(true); } else if (useDarkTheme == 1) { setDarkTheme(true); } else if (useDarkTheme == 0) { setDarkTheme(false); } if (!localStorage.getItem('all-tags')) localStorage.setItem('all-tags', JSON.stringify([])); if (!localStorage.getItem('color')) localStorage.setItem('color', 'blue'); setColorTheme(localStorage.getItem('color')); if (!localStorage.getItem('language')) localStorage.setItem('language', 'en'); setLanguage(localStorage.getItem('language')); }, []); useEffect(() => { if (darkTheme) { setTheme(1); } else { setTheme(0); } }, [darkTheme]); useEffect(() => { setLang(language); }, [language]); useEffect(() => { switch (colorTheme) { case 'red': setColor('red', 0); break; case 'yellow': setColor('yellow', 35); break; case 'green': setColor('green', 140); break; case 'blue': setColor('blue', 210); break; case 'purple': setColor('purple', 250); break; case 'pink': setColor('pink', 300); break; default: break; } }, [colorTheme]); useEffect(() => { window.api.getOS(); window.api.receive('gotOS', (data: string) => { setCurrentOS(data); }); }, []); return ( {children} ); } function useGeneralContext() { const context = useContext(GeneralContext); if (context === undefined) { throw new Error('useMyContext must be used within a MyContextProvider'); } return context; } export { GeneralContextProvider, useGeneralContext }; ================================================ FILE: src/renderer/components/Header/AddTag.tsx ================================================ import React, { useEffect, useState } from 'react'; import { TagProps } from './Tag'; import { useGeneralContext } from '@components/GeneralContext'; import { useTranslation } from 'react-i18next'; const AddTag = () => { const { t, i18n } = useTranslation(); const [clickState, setClickState] = useState(false); const [currentValue, setCurrentValue] = useState(''); const [savedValue, setSavedValue] = useState(''); const { setCurrentFileTags, currentFileTags } = useGeneralContext(); const addTagStyle = { backgroundColor: 'unset', height: '14px', border: 'none', color: 'unset', fontFamily: 'unset', fontSize: 'unset', minWidth: `79px`, width: `${currentValue.length * 9 + 10}px`, outline: 'none', }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { setSavedValue(currentValue); setClickState(false); } }; useEffect(() => { if (!localStorage.getItem('all-tags')) localStorage.setItem('all-tags', JSON.stringify([])); else { const allTags: TagProps[] = JSON.parse(localStorage.getItem('all-tags')); const createdTag: TagProps = { text: savedValue, color: Math.floor(Math.random() * 359).toString(), }; if ( !allTags.find((tag: TagProps) => tag.text == createdTag.text) && createdTag.text != '' ) { allTags.push(createdTag); } if ( !currentFileTags.find((tag: TagProps) => tag.text == createdTag.text) && createdTag.text != '' ) { setCurrentFileTags([...currentFileTags, createdTag.text].flat()) } localStorage.setItem('all-tags', JSON.stringify(allTags)); } }, [savedValue]); useEffect(() => { setCurrentValue(''); }, [clickState]); const content = clickState ? ( setCurrentValue(e.target.value)} onKeyDown={handleKeyDown} style={addTagStyle} /> ) : ( <> {t("Add Tag")} ); return (
setClickState(true)}> {content}
); }; export default AddTag; ================================================ FILE: src/renderer/components/Header/FilePath.tsx ================================================ import React from 'react'; import './Header.scss'; type FilePathProps = { filePath: string }; const FilePath = ({ filePath }: FilePathProps) => { const resolvedFilePath = filePath ? filePath.split(/[\/\\]/) : ['']; const folderPath = resolvedFilePath.slice(resolvedFilePath.length - 2)[0]; const fileName = resolvedFilePath.pop().replace('.json', ''); return (
{folderPath == 'files' || fileName == '' ? '' : `${folderPath} /`} {fileName}
); }; export default FilePath; ================================================ FILE: src/renderer/components/Header/Header.scss ================================================ @import '../Theme.scss'; .header { width: calc(100% - 20px); max-width: 100%; align-self: center; padding-top: 5px; height: calc(0% + 45px); display: flex; .main-heading { justify-content: space-between; display: flex; flex-direction: row; align-items: center; direction: var(--app-direction); height: 100%; width: 100%; .header-content { gap: 10px; display: flex; flex-direction: row; align-items: center; direction: var(--app-direction); height: 100%; max-width: calc(100% - 200px); white-space: nowrap; width: max-content; -webkit-app-region: no-drag; div { width: max-content; font-weight: 300; color: var(--erwt-heading-color); line-height: 1; } .tags { width: max-content; max-width: inherit; display: flex; flex-direction: row; direction: var(--app-direction); padding-inline-start: 5px; gap: 10px; } .tag-pill { padding: 3px 12px 3px 12px; display: flex; justify-content: space-between; background-color: var(--pill-bgcolor); border-radius: 5px; transition: 0.1s; height: max-content; .tag-action::before { content: "#"; } .tag-content { width: max-content; font-size: 16px; display: flex; gap: 5px; height: 16px; } .tag-action { opacity: 70%; width: 16px; font-size: 16px; &:hover { cursor: pointer; opacity: 100%; } } &:hover { filter: var(--tag-filter); .tag-action::before { content: "X"; font-size: 14px; } } &:active { filter: var(--tag-filter); } &.new { background-color: var(--pill-bgcolor); font-size: 16px; gap: 5px; &::before { opacity: 70%; content: "+"; } &:hover { cursor: pointer; filter: unset; background-color: var(--pill-hover-bgcolor); } &:active { cursor: pointer; filter: unset; background-color: var(--pill-active-bgcolor); transform: scale(0.98); } } } .filepath { max-width: 80%; font-size: 23px; display: flex; flex-direction: row; direction: var(--app-direction); gap: 6px; padding-bottom: 3px; .filepath-name { font-weight: 600; &:focus { outline: none; } } &-folder.hide { display: none; } } #logo { fill: hsla(var(--app-text-color), 1); display: block; width: 45px; height: 55%; filter: var(--logo-filter); } .logo { display: flex; align-items: center; justify-content: center; width: 45px; height: 100%; } } .header-draggable { height: 100%; width: 100%; -webkit-app-region: drag; } .header-controls { z-index: 1000; background-color: transparent; height: inherit; -webkit-app-region: no-drag; .window-titlebar-controls { height: inherit; position: unset; .control { border-radius: 10px; transition: 0.1s; color: var(--app-text-color); &:hover{ background-color: var(--file-hover-bgcolor); &.close { background: var(--button-danger); } } &:active { transform: scale(0.95); } } } } } } ================================================ FILE: src/renderer/components/Header/Header.tsx ================================================ import React, { useEffect, useState } from 'react'; import '../Application.scss'; import { icons } from '../Icons'; import FilePath from './FilePath'; import { Tag, TagProps } from './Tag'; import AddTag from './AddTag'; import WindowControls from '@misc/window/components/WindowControls'; import { useGeneralContext } from '@components/GeneralContext'; const Header = () => { const allTags = JSON.parse(localStorage.getItem('all-tags')); const { selectedFile, currentFileTags, setCurrentFileTags, currentOS } = useGeneralContext(); const [currentFilePath, setCurrentFilePath] = useState(''); useEffect(() => { setCurrentFilePath(selectedFile); setCurrentFileTags([]) }, [selectedFile]); return (
{currentFileTags ? currentFileTags.map((tag: string) => { const foundTag = allTags.find( (searchTag: TagProps) => searchTag.text == tag, ); return ( ); }) : null} {currentFilePath ? : <>}
); }; export default Header; ================================================ FILE: src/renderer/components/Header/Tag.tsx ================================================ import { useGeneralContext } from '@components/GeneralContext'; import React from 'react'; export type TagProps = { text: string; color: string; }; export const Tag = ({ text, color }: TagProps) => { const { setCurrentFileTags, currentFileTags } = useGeneralContext(); const handleRemoveTag = () => { setCurrentFileTags(currentFileTags.filter((tag: string) => tag != text)); //TODO: figure out how to check if tag is not in all of the files // localStorage.setItem( // 'all-tags', // JSON.stringify( // JSON.parse(localStorage.getItem('all-tags')).filter( // (tag: TagProps) => tag.text != text, // ), // ), // ); }; return (
{text}
); }; ================================================ FILE: src/renderer/components/Icons.tsx ================================================ const pngLogo = require('@assets/icons/pngLogo.png'); const logo = require('@assets/icons/logo.svg'); export const icons = { pngLogo, logo }; ================================================ FILE: src/renderer/components/MathSidebar/ActionsGroup.tsx ================================================ import React from 'react'; import '../Application.scss'; import './MathSidebar.scss' type ActionsGroupProps = { title: string; groupType: string }; const ActionsGroup = ({ groupType, title }: ActionsGroupProps) => { return (

{title}

); } export default ActionsGroup; ================================================ FILE: src/renderer/components/MathSidebar/MathSidebar.scss ================================================ @import '../Theme.scss'; .actions-group { display: none; } .math-sidebar.open{ min-width: max-content; .actions-group { display: flex; flex-direction: column; gap: 10px; .actions-group-title { width: 100%; margin: 0; color: hsla(var(--app-text-color), 1); font-size: 22px; opacity: 50%; } .actions-group-content{ width: 100%; min-height: 100px; } } } ================================================ FILE: src/renderer/components/MathSidebar/MathSidebar.tsx ================================================ import React, { useEffect, useState } from 'react'; import ActionsGroup from './ActionsGroup'; import { useGeneralContext } from '@components/GeneralContext'; import { useTranslation } from 'react-i18next'; const MathSidebar = () => { const { t, i18n } = useTranslation(); const { isMathSidebarOpen } = useGeneralContext() const [showClass, setShowClass] = useState(''); useEffect(() => { setShowClass(isMathSidebarOpen); }, [isMathSidebarOpen]) return
; }; export default MathSidebar; ================================================ FILE: src/renderer/components/Page/Grid/Blocks/Blocks.scss ================================================ @import '../../../Theme.scss'; @import '../../Page.scss'; @import '../Grid.scss'; @import "/node_modules/mafs/core.css"; .block-content{ transition: 0.2s; width: 100%; display: flex; outline: 1px solid transparent; border-radius: 5px; margin: 3px; direction: var(--app-direction); overflow-y: auto; overflow-x: hidden; // Math .math-field-element{ --hue: var(--theme-hue); --selection-background-color: var(--math-block-selection-bg); --selection-color: var(--math-block-selection); font-size: 25px; color: hsla(var(--app-text-color), 1); width: 100%; height: 100%; outline: none; display: flex; justify-content: center; background-color: var(--math-block-bgcolor); &::part(content){ margin-top: 5px; } } // Text .textbox{ padding: 10px; color: hsla(var(--app-text-color), 1); width: 100%; height: inherit; overflow-wrap: anywhere !important; font-size: larger; p, h1, h2, h3, h4, h5, h6 { &:first-child{ margin-top: 0 !important; } } [role="math"]{ --hue: var(--theme-hue); --selection-background-color: var(--math-block-selection-bg); --selection-color: var(--math-block-selection); font-size: 25px; color: hsla(var(--app-text-color), 1); width: 100%; height: 100%; outline: none; display: flex; justify-content: center; background-color: var(--math-block-bgcolor); &::part(content){ margin-top: 5px; } } } // Divider &:has(.pageDivider){ background: gray; opacity: 40%; direction: var(--app-direction); margin-right: 0px; max-width: 100%; width: 100%; height: 1px; border: none; transition: 0.2s; align-self: center; } // Graph .graph-block-wrapper { width: 100%; height: 100%; display: flex; flex-direction: column-reverse; justify-content: space-between; .math-field-element{ border-top: 1px solid var(--tool-hover-bgcolor); height: 50px; min-height: 50px; max-height: max-content; align-items: center; &::part(content){ margin-top: unset; } } .MafsView { font-family: 'CMU Serif'; direction: ltr; --mafs-line-color: var(--page-border); --mafs-bg: var(--page-bgcolor); --mafs-fg: hsla(var(--app-text-color), 1); --mafs-blue: var(--mafs-plot-color); } } // Draw .draw-applet { width: 100%; height: 100%; .t-ljGyqI{ border: 1px solid transparent !important; border-radius: 6px !important; width: calc(100% - 27px); height: calc(100% - 6px); .tl-positioned{ path:last-child{ fill: hsla(var(--app-text-color), 1); stroke: hsla(var(--app-text-color), 1); } } #tl{ --tl-background: var(--math-block-bgcolor); } #TD-Tools{ --colors-panelContrast: var(--tool-hover-bgcolor); --colors-panel: var(--panel-bgcolor); --colors-hover: var(--tool-hover-bgcolor); --colors-text: hsla(var(--app-text-color), 1); } .c-itYKbw{ display: none; } .c-dwQeTN{ padding: 0; } button { background-color: unset; &:hover{ background-color: unset; } } } } } ================================================ FILE: src/renderer/components/Page/Grid/Blocks/DrawBlock.tsx ================================================ import { ValueProps, canvasProps } from '@renderer/common/types'; import { Tldraw, TldrawApp } from '@tldraw/tldraw'; import React, { useEffect, useState } from 'react'; function DrawBlockContent({ content, blockStateFunction }: ValueProps) { const [canvasState, setCanvasState] = useState({ shapes: [] }); useEffect(() => { blockStateFunction(canvasState); }, [canvasState]); return (
{ // app.setSetting('showGrid', true); app.insertContent(content ? (content as canvasProps) : { shapes: [] }); app.zoomToContent(); }} onPersist={(app: TldrawApp) => setCanvasState(app.getContent())} />
); } export default DrawBlockContent; ================================================ FILE: src/renderer/components/Page/Grid/Blocks/GraphBlock.tsx ================================================ import { Mafs, Coordinates, Plot, Theme } from 'mafs'; import React, { useEffect, useRef, useState } from 'react'; // eslint-disable-next-line import/named import MathView, { MathViewRef } from 'react-math-view'; import { parseTex } from 'tex-math-parser'; import ML_SHORTCUTS from '@common/shortcuts'; import ML_KEYBINDINGS from '@common/keybindings'; import { ValueProps } from '@renderer/common/types'; function latex2function(latex: string, i: number) { if (latex != '') { try { const parsed = parseTex(String.raw`${latex}`); return parsed.compile().evaluate({ x: i }); } catch (error) { return; } } } function GraphBlockContent({ content, blockStateFunction }: ValueProps) { const [value, setValue] = useState(content ? (content as string[]) : ['']); useEffect(() => { blockStateFunction(value); }, [value]); function GraphBlockSetter() { const ref = useRef(null); return ( latex} onMathFieldBlur={() => { setValue([ref.current?.getValue('latex')]); }} plonkSound={null} keypressSound={null} /> ); } return (
{ // const fixedPoint = {x: point[0], y: point[1]} // setPoints([...points, ]) // } // } > latex2function(value[0], x)} color={Theme.blue} /> {/* {points} */}
); } export default GraphBlockContent; // TESTS // function CreatePolygon() { // const [pointsList, setPointsList] = React.useState([[1,1], [2,2], [3,-2]] as vec.Vector2[]); // const [movePoints, setMovePoints] = React.useState(false); // const handleMove = (point, newPoint) =>{ // setPointsList([...pointsList.filter((p) => p != point), newPoint]) // } // return ( // // movePoints // ? setPointsList([...pointsList, point]) // : null // } // > // // // {pointsList.map((point, i) => ( // handleMove(point, newPoint)} // /> // ))} // // ); // } ================================================ FILE: src/renderer/components/Page/Grid/Blocks/MathBlock.tsx ================================================ import React, { useEffect, useRef, useState } from 'react'; // eslint-disable-next-line import/named import MathView, { MathViewRef } from 'react-math-view'; import ML_SHORTCUTS from '@common/shortcuts'; import ML_KEYBINDINGS from '@common/keybindings'; import { ValueProps } from '@renderer/common/types'; function MathBlockContent({ content, blockStateFunction }: ValueProps) { const defaultValue = ''; const ref = useRef(null); const [value, setValue] = useState(defaultValue); useEffect(() => { blockStateFunction(value); }, [value]); return ( setValue(ref.current?.getValue())} onExport={(ref, latex) => latex} plonkSound={null} keypressSound={null} /> ); } export default MathBlockContent; ================================================ FILE: src/renderer/components/Page/Grid/Blocks/TextBlock.tsx ================================================ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { createEditor, // eslint-disable-next-line import/named Descendant, Editor, Element as SlateElement, Node as SlateNode, Point, Range, Transforms, // eslint-disable-next-line import/named BaseElement, } from 'slate'; import { withHistory } from 'slate-history'; import { Editable, ReactEditor, Slate, withReact } from 'slate-react'; import { ValueProps, canvasProps } from '@renderer/common/types'; import { useTranslation } from 'react-i18next'; // import MathView from 'react-math-view'; // import ML_KEYBINDINGS from '@common/keybindings'; // import ML_SHORTCUTS from '@common/shortcuts'; type BulletedListElement = { type: 'bulleted-list'; align?: string; children: Descendant[]; }; interface CustomElement extends BaseElement { type?: string; children: Descendant[]; } const SHORTCUTS = { '*': 'list-item', '-': 'list-item', '+': 'list-item', '>': 'block-quote', '#': 'heading-one', '##': 'heading-two', '###': 'heading-three', '####': 'heading-four', '#####': 'heading-five', '######': 'heading-six', // '$$': 'math-inline', // '---': 'divider', }; function isDescendant(content: string | string[] | Descendant[] | canvasProps): content is Descendant[] { return (content as Descendant[]) !== undefined; } const TextBlockContent = ({ content, blockStateFunction }: ValueProps) => { const { t, i18n } = useTranslation(); const valueToSet = isDescendant(content) ? content : [{ type: 'paragraph', children: [{ text: '' }] }]; const renderElement = useCallback( ( props: JSX.IntrinsicAttributes & { attributes: unknown; children: unknown; element: unknown; }, ) => , [], ); const editor = useMemo( () => withShortcuts(withReact(withHistory(createEditor()))), [], ); const [value, setValue] = useState(valueToSet); useEffect(() => { blockStateFunction(value); }, [value]); const handleDOMBeforeInput = useCallback(() => { queueMicrotask(() => { const pendingDiffs = ReactEditor.androidPendingDiffs(editor); const scheduleFlush = pendingDiffs?.some(({ diff, path }) => { if (!diff.text.endsWith(' ')) { return false; } const { text } = SlateNode.leaf(editor, path); const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1); if (!(beforeText in SHORTCUTS)) { return; } const blockEntry = Editor.above(editor, { at: path, match: (n) => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); if (!blockEntry) { return false; } const [, blockPath] = blockEntry; return Editor.isStart(editor, Editor.start(editor, path), blockPath); }); if (scheduleFlush) { ReactEditor.androidScheduleFlush(editor); } }); }, []); return ( setValue(currentValue)} > ); }; const withShortcuts = (editor: ReactEditor) => { const { deleteBackward, insertText } = editor; editor.insertText = (text) => { const { selection } = editor; if (text.endsWith(' ') && selection && Range.isCollapsed(selection)) { const { anchor } = selection; const block = Editor.above(editor, { match: (n) => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); const path = block ? block[1] : []; const start = Editor.start(editor, path); const range = { anchor, focus: start }; const beforeText = Editor.string(editor, range) + text.slice(0, -1); const type = (SHORTCUTS as any)[beforeText]; if (type) { Transforms.select(editor, range); if (!Range.isCollapsed(range)) { Transforms.delete(editor); } const newProperties: Partial = { type, }; Transforms.setNodes(editor, newProperties, { match: (n) => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); if (type === 'list-item') { const list: BulletedListElement = { type: 'bulleted-list', children: [], }; Transforms.wrapNodes(editor, list, { match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && (n as CustomElement).type === 'list-item', }); } return; } } insertText(text); }; editor.deleteBackward = (...args) => { const { selection } = editor; if (selection && Range.isCollapsed(selection)) { const match = Editor.above(editor, { match: (n) => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); if (match) { const [block, path] = match; const start = Editor.start(editor, path); if ( !Editor.isEditor(block) && SlateElement.isElement(block) && (block as CustomElement).type !== 'paragraph' && Point.equals(selection.anchor, start) ) { const newProperties: CustomElement = { type: 'paragraph', children: [] }; Transforms.setNodes(editor, newProperties); if ((block as CustomElement).type === 'list-item') { Transforms.unwrapNodes(editor, { match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && (n as CustomElement).type === 'bulleted-list', split: true, }); } return; } } deleteBackward(...args); } }; return editor; }; const Element = ({ attributes, children, element }: any) => { switch (element.type) { case 'block-quote': return
{children}
; case 'bulleted-list': return
    {children}
; case 'heading-one': return

{children}

; case 'heading-two': return

{children}

; case 'heading-three': return

{children}

; case 'heading-four': return

{children}

; case 'heading-five': return
{children}
; case 'heading-six': return
{children}
; case 'list-item': return
  • {children}
  • ; // case 'divider': // return
    //
    // {children} //
    ; // case 'math-inline': // return
    // latex} // plonkSound={null} // keypressSound={null} // /> // {children} //
    ; default: return

    {children}

    ; } }; export default TextBlockContent; ================================================ FILE: src/renderer/components/Page/Grid/Grid.scss ================================================ @import '../../Theme.scss'; @import '../Page.scss'; #main-app.rtl .block-remove-button { left: 1px !important; right: unset !important; } .layout { height: 100% !important; direction: ltr; width: 100%; max-height: 100%; min-width: 100%; .react-grid-placeholder{ transition: 0.2s; background-color: var(--tool-bgcolor); opacity: 20%; border-radius: 5px; } .block { transition: 0.2s; display: flex; flex-direction: row; width: 100%; height: 100%; gap: 5px; direction: var(--app-direction); .react-resizable-handle { z-index: 100; &::after { opacity: 20%; border-right: 2px solid hsla(var(--app-text-color), 1); border-bottom: 2px solid hsla(var(--app-text-color), 1); border-radius: 0 0 5px 0; } } .block-content { transition: inherit; } .block-handle { transition: 0.2s; padding-top: 8px; font-size: 16px; opacity: 0%; color: hsla(var(--app-text-color), 1); } .block-remove-button { display: none; position: absolute; z-index: 10; right: 1px; left: unset; top: 1px; background-color: transparent; height: 30px; width: 30px; border-radius: 5px; cursor: pointer; align-items: center; justify-content: center; .fi { pointer-events: none; color: hsla(var(--app-text-color), 1); font-size: 10px; } } .block-content{ outline: 1px solid transparent; transition: 0.2s; &:has(.MafsView){ outline: 1px solid var(--tool-hover-bgcolor); } } &:hover { .block-content{ outline: 1px solid var(--tool-hover-bgcolor); .block-remove-button{ display: flex; } } .block-handle { opacity: 70%; &:hover{ cursor: pointer; } } } } } ================================================ FILE: src/renderer/components/Page/Grid/Grid.tsx ================================================ import React, { useEffect, useState } from 'react'; import { useGeneralContext } from '@components/GeneralContext'; import { Notification } from '@components/common/Notification'; import ConfirmModal from '@components/common/Modals/ConfirmModal'; import { BlockElement, PageGridState } from '@renderer/common/types'; import '@components/Page/Page.scss'; import '@components/Page/Grid/Grid.scss'; import '@components/Page/Grid/Blocks/Blocks.scss'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import GridElement from './GridElement'; import { Responsive, WidthProvider } from 'react-grid-layout'; import { useAddBlock } from '@renderer/hooks/useAddBlock'; import { useDialog } from '@renderer/hooks/useDialog'; import { useFileSaveLoad } from '@renderer/hooks/useFileSaveLoad'; import { useTranslation } from 'react-i18next'; const ResponsiveGridLayout = WidthProvider(Responsive); const PageGrid = () => { const { t, i18n } = useTranslation(); const [state, setState] = useState({ items: [], breakpoint: 'lg', cols: 8, }); const [allBlockValues, setAllBlockValues] = useState([]); const [popupType, setPopupType] = useState(''); const [clearModalOpen, setClearModalOpen] = useState(false); const { isFilesSidebarOpen, isMathSidebarOpen } = useGeneralContext(); useEffect( function adjustGridWidth() { setTimeout(() => { dispatchEvent(new Event('resize')); }, 300); }, [isFilesSidebarOpen, isMathSidebarOpen], ); useFileSaveLoad( state, setState, allBlockValues, setAllBlockValues, setPopupType, ); useAddBlock(setState); const ModalChoice = useDialog(setState, setAllBlockValues, setClearModalOpen); const onLayoutChange = (layout: Array) => { layout.map((block) => { block.type = state.items.find((item) => item.i == block.i).type; block.metaData = state.items.find((item) => item.i == block.i).metaData; }); setState((prev) => ({ ...prev, items: layout })); }; const onBreakpointChange = (breakpoint: string, cols: number) => { setState((prev) => ({ ...prev, breakpoint: breakpoint, cols: cols })); }; const onRemoveItem = (e: React.MouseEvent) => { setState((prev) => ({ ...prev, items: prev.items.filter( (item) => item.i !== (e.target as HTMLTextAreaElement).name, ), })); setAllBlockValues((allValues) => allValues.filter( (value) => value.id !== (e.target as HTMLTextAreaElement).name, ), ); }; return (
    {state.items.map((element) => ( ))}
    ); }; export default PageGrid; ================================================ FILE: src/renderer/components/Page/Grid/GridElement.tsx ================================================ import React, { LegacyRef, useEffect, useState } from 'react'; import TextBlockContent from './Blocks/TextBlock'; import MathBlockContent from './Blocks/MathBlock'; import GraphBlockContent from './Blocks/GraphBlock'; import { BlockElement, ValueProps, WidgetType } from '@renderer/common/types'; import DrawBlockContent from './Blocks/DrawBlock'; import './Grid.scss'; type GridElementProps = { allValues: any[]; setValuesFunction: (...args: unknown[]) => unknown; blockElement: BlockElement; blockValue: ValueProps; onRemoveItem: (e: React.MouseEvent) => void; key?: string; className?: string; style?: React.CSSProperties; children?: React.ReactNode[]; onMouseDown?: (e: React.MouseEvent) => unknown; onMouseUp?: (e: React.MouseEvent) => unknown; onTouchEnd?: (e: React.TouchEvent) => unknown; }; function Switcher( widgetType: WidgetType, blockValue: ValueProps, blockStateFunction: (...args: unknown[]) => unknown, ) { try { switch (widgetType) { case WidgetType.Text: return ( ); case WidgetType.Math: return ( ); case WidgetType.Graph: return ( ); case WidgetType.Divider: return
    ; case WidgetType.Draw: return ( ); default: return null; } } catch (error) { console.log(error); } } const GridElement = React.forwardRef( ( { blockElement, setValuesFunction, allValues, blockValue, key, onRemoveItem, style, className, onMouseDown, onMouseUp, onTouchEnd, children, }: GridElementProps, ref, ) => { const [blockState, setBlockState] = useState(); useEffect(() => { const currentState = { id: blockElement.i, metaData: {content: blockState}, }; const newAllValues = allValues.map((state) => state.id == currentState.id ? { ...state, metaData: currentState.metaData } : state, ); if (!newAllValues.find((state) => state.id == currentState.id)) { newAllValues.push(currentState); } setValuesFunction(newAllValues); }, [blockState]); const ReactElement = (
    )} onMouseDown={onMouseDown} onMouseUp={onMouseUp} onTouchEnd={onTouchEnd} style={{ ...style }} key={key} id={blockElement.i} >
    {Switcher(blockElement.type, blockValue, setBlockState)}
    {children}
    ); return ReactElement; }, ); GridElement.displayName = 'GridElement'; export default GridElement; ================================================ FILE: src/renderer/components/Page/Page.scss ================================================ @import '../Theme.scss'; @import '~keyboard-css/dist/css/main.min.css'; ::-webkit-scrollbar { width: 0; } .page { transition: 0.3s; width: 100%; height: 100%; max-width: calc(100% - 110px); background: var(--page-bgcolor); border-width: 2px 2px 0px 2px; border-style: solid; box-sizing: border-box; border-color: var(--page-border); border-radius: 15px 15px 0px 0px; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; .page-grid { min-width: 60%; width: 70%; max-width: 80%; height: 100%; display: flex; flex-direction: column; overflow-y: auto; align-items: center; .grid-container { padding-top: 100px; margin-bottom: 80px; height: 100%; max-height: 100%; width: 100%; max-width: 100%; overflow-x: hidden; display: flex; flex-direction: column; align-self: center; justify-content: space-between; } } .page-placeholder { align-self: center; display: flex; flex-direction: column; justify-content: center; height: 100%; .placeholder-text { line-height: 1.5; opacity: 70%; font-weight: 100; text-align: center; padding-bottom: 80px; font-size: 20px; color: hsla(var(--app-text-color), 1); .fi{ height: 18px; display: inline-block; margin-bottom: 6px; font-size: 18px; } .kbc-button { font-family: 'Rubik2', 'NotoSansArabic' !important; font-weight: 100; color: hsla(var(--app-text-color), 1) !important; box-shadow: 0 0 var(--page-border), 0 0 var(--page-border), 0 1px var(--page-border), 0 2px var(--page-border), 0 3px var(--page-border), 0 4px var(--page-border), 0 5px var(--page-border), 2px 2.5px 4px var(--page-border) !important; background-color: var(--page-bgcolor) !important; border: 1px solid var(--page-border) !important; border-bottom: 1px solid var(--page-border) !important; &::after { border: 1px solid var(--page-border) !important; } } } } } ================================================ FILE: src/renderer/components/Page/Page.tsx ================================================ import React, { useEffect, useState } from 'react'; import '../Application.scss'; import './Page.scss'; import ToolsPanel from './ToolsPanel/ToolsPanel'; import './ToolsPanel/ToolsPanel.scss'; import { useGeneralContext } from '@components/GeneralContext'; import PagePlaceholder from './PagePlaceholder'; import PageGrid from './Grid/Grid'; const gridStyle = { width: '100%', height: '100%', backgroundImage: 'radial-gradient(circle at 50% 100%, var(--tool-hover-bgcolor) 1px, transparent 1px)', backgroundSize: '50px 50px', color: 'white' }; const Page = () => { const { selectedFile } = useGeneralContext(); const [currentFilePath, setCurrentFilePath] = useState("") useEffect(() => { setCurrentFilePath(selectedFile) }, [selectedFile]) return (
    { currentFilePath ?
    : }
    ); }; export default Page; ================================================ FILE: src/renderer/components/Page/PagePlaceholder.tsx ================================================ import React from 'react'; import Shortcut from '@components/common/Shortcut'; import './Page.scss'; import { useTranslation } from 'react-i18next'; const PagePlaceholder = () => { const { t, i18n } = useTranslation(); return (
    {t("Placeholder 1")},
    {t("Placeholder 2")}{' '} {' '} ({t("Placeholder Or")} ) {t("Placeholder 3")}!
    ); }; export default PagePlaceholder; ================================================ FILE: src/renderer/components/Page/ToolsPanel/Tool.tsx ================================================ import React from 'react'; type ToolProps = { title: string; buttonType: string; icon?: string; onClick?: React.MouseEventHandler; }; const Tool = ({ buttonType, title, icon, onClick }: ToolProps) => { const iconName = `fi fi-rr-${icon}`; return ( ); }; export default Tool; ================================================ FILE: src/renderer/components/Page/ToolsPanel/ToolsPanel.scss ================================================ @import '../../Theme.scss'; @import '../Page.scss'; .tools-panel-container { position: fixed; height: 80px; bottom: 0px; width: 45vw; z-index: 20; display: flex; justify-content: center; .tools-panel { direction: var(--app-direction); transition: 0.2s; position: fixed; padding: 8px 10px 8px 10px; height: 40px; bottom: -50px; width: max-content; display: flex; flex-direction: row; border-radius: 8px; gap: 8px; background: var(--panel-bgcolor); box-shadow: 0px 1px 12px rgba(0, 0, 0, 0.25); .tool { border-radius: 5px; width: 40px; height: 40px; background-color: var(--tool-bgcolor); &:hover { background-color: var(--tool-hover-bgcolor); &::after { content: attr(data-tooltip); color: hsla(var(--app-text-color), 1); min-width: max-content; position: fixed; margin-bottom: 100px; z-index: 1000; padding: 6px 10px 6px 10px; background-color: var(--button-bgcolor); text-shadow: none; border: 1px solid var(--page-border); border-radius: 6px; opacity: 0%; animation: tooltip 0.1s ease-in-out 0.2s forwards; } } &:active { transform: scale(0.9); background-color: var(--tool-active-bgcolor); } .fi { line-height: 1.6; margin: auto; font-size: 20px; color: var(--tool-color); text-shadow: none; } &#clearPage { background-color: var(--clear-button-bgcolor); &:hover { background-color: var(--clear-button-hover-bgcolor); } .fi { color: var(--clear-button-color); } } } .tool-divider { fill: var(--tool-color); opacity: 50%; width: 20px; height: 100%; } } &:hover { .tools-panel { bottom: 14px; } } } @keyframes tooltip { from { transform: scale(0.95); opacity: 0%; box-shadow: 0px 1px 3px #00000000; } to { transform: scale(1); opacity: 100%; box-shadow: 0px 1px 3px #0000002a; } } ================================================ FILE: src/renderer/components/Page/ToolsPanel/ToolsPanel.tsx ================================================ import React from 'react'; import '../Page.scss'; import Tool from './Tool'; import { useGeneralContext } from '@components/GeneralContext'; import { WidgetType } from '@renderer/common/types'; import { useTranslation } from 'react-i18next'; const ToolsPanel = () => { const { t, i18n } = useTranslation(); const { setNewWidgetRequest, setClearPageRequest, setSaveRequest } = useGeneralContext(); const handleOnClickTool = (widgetType: WidgetType) => { setNewWidgetRequest({ widgetType }); }; const clearPage = () => { setClearPageRequest({ cmd: 'clear' }); }; const savePage = () => { setSaveRequest({ cmd: 'save' }); }; return (
    handleOnClickTool(WidgetType.Text)} /> handleOnClickTool(WidgetType.Math)} /> handleOnClickTool(WidgetType.Graph)} /> {/* handleOnClickTool(WidgetType.Picture)} /> */} handleOnClickTool(WidgetType.Draw)} /> {/* */} handleOnClickTool(WidgetType.Divider)} /> savePage()} /> clearPage()} />
    ); }; export default ToolsPanel; ================================================ FILE: src/renderer/components/Theme.scss ================================================ #main-app.rtl { --app-direction: rtl; } :root { --app-direction: unset; // Logo --logo-filter: brightness(1); // Application --app-accent-color: hsl(var(--theme-hue), 53%, 40%); --app-font-size: 16px; --app-font-family: 'Rubik2', 'NotoSansArabic'; --app-bgcolor: hsl(var(--theme-hue), 11%, 7%); --app-border-color: hsl(var(--theme-hue), 7%, 25%); --app-text-color: 0, 0%, 85%; // Page --page-bgcolor: hsl(var(--theme-hue), 26%, 9%); --page-border: hsl(var(--theme-hue), 21%, 13%); // Blocks --math-block-bgcolor: hsl(var(--theme-hue), 21%, 13%); --math-block-selection: hsl(0, 0%, 100%); --math-block-selection-bg: hsl(var(--theme-hue), 21%, 25%); --mafs-plot-color: hsl(var(--theme-hue), 40%, 50%); --text-block-font-size: 20px; // Tools Panel --panel-bgcolor: hsl(var(--theme-hue), 26%, 18%); --tool-bgcolor: hsl(var(--theme-hue), 31%, 14%); --tool-hover-bgcolor: hsl(var(--theme-hue), 28%, 23%); --tool-active-bgcolor: hsl(var(--theme-hue), 27%, 18%); // Scrollbar --scroll-width: 8px; --scroll-track-bgcolor: hsla(0, 0%, 18%, 0.322); --scroll-thumb-bgcolor: hsla(0, 0%, 23%, 0.747); --scroll-thumb-hover-bgcolor: hsl(0, 0%, 33%); // Selection --selection-bgcolor: var(--app-accent-color); --selection-color: hsl(0, 0%, 100%); // Button --button-color: hsl(0, 0%, 100%); --button-border: 1px solid hsl(var(--theme-hue), 3%, 14%); --button-bgcolor: hsl(var(--theme-hue), 26%, 9%); --button-hover-bgcolor: hsl(var(--theme-hue), 25%, 12%); --button-active-bgcolor: hsl(var(--theme-hue), 27%, 18%); --button-shadow-color: hsla(0, 0%, 0%, 0.471); --pill-bgcolor: hsl(var(--theme-hue), 33%, 13%); --pill-hover-bgcolor: hsl(var(--theme-hue), 33%, 19%); --pill-active-bgcolor: hsl(var(--theme-hue), 33%, 15%); --tag-saturation: 19%; --tag-lightness: 23%; --tag-filter: brightness(110%); --button-danger: hsl(0, 50%, 50%); // --button-outline: 1px solid hsl(var(--theme-hue), 3%, 14%); --clear-button-bgcolor: hsl(0, 15%, 32%); --clear-button-hover-bgcolor: hsl(0, 16%, 39%); --clear-button-color: hsl(0, 12%, 66%); --sidebar-button-color: hsl(var(--theme-hue), 15%, 35%); --tool-color: hsl(var(--theme-hue), 16%, 67%); // File System --file-bgcolor: hsl(var(--theme-hue), 25%, 10%); --file-hover-bgcolor: hsl(var(--theme-hue), 25%, 12%); --file-active-bgcolor: hsl(var(--theme-hue), 25%, 14%); --selected-transparent: hsla(var(--theme-hue), 25%, 12%, 0.5); --selected-bgcolor: hsl(var(--theme-hue), 25%, 12%); --selected-hover-bgcolor: hsl(var(--theme-hue), 25%, 14%); --selected-active-bgcolor: hsl(var(--theme-hue), 25%, 16%); // Command Bar --command-bar-bg: linear-gradient(120deg, hsla(var(--theme-hue), 10%, 5%, 0.7), hsla(var(--theme-hue), 10%, 9%, 0.9), 70%, hsla(var(--theme-hue), 10%, 5%, 0.7)); // Titlebar --titlebar-bgcolor: var(--app-bgcolor); --titlebar-color: hsl(0, 0%, 85%); --titlebar-title-color: hsl(0, 0%, 85%); --titlebar-menu-border-color: hsl(0, 1%, 17%); --titlebar-menu-title-hover-bgcolor: hsl(0deg 0% 12%); --titlebar-menu-title-active-bgcolor: hsl(0deg 0% 14%); --titlebar-menu-title-active-border-color: var(--titlebar-menu-border-color); --titlebar-menu-separator-bgcolor: var(--titlebar-menu-border-color); --titlebar-popup-bgcolor: hsl(0deg 0% 14%); --titlebar-popup-border: 1px solid var(--titlebar-menu-border-color); --titlebar-popup-shadow: 4px 10px 10px rgba(0, 0, 0, 0.2); --titlebar-popup-item-name-color: hsl(0, 0%, 75%); --titlebar-popup-item-shortcut-color: hsla(0, 0%, 55%, 0.8); --titlebar-popup-item-hover-color: hsl(0, 0%, 85%); --titlebar-popup-item-hover-bgcolor: hsl(0deg 0% 18%); --titlebar-popup-item-hover-shortcut-color: var(--app-accent-color); --titlebar-popup-item-name-shadow-color: hsl(0, 0%, 8%); // ERWT --erwt-heading-color: hsl(0, 0%, 87%); } /* *========================================================================= * ERWT Light Theme *========================================================================= * * Light theme for ERWT application. */ body:not(.dark-mode) { // Logo --logo-filter: brightness(0.1); // Application --app-accent-color: hsl(var(--theme-hue), 62%, 49%); --app-bgcolor: hsl(var(--theme-hue), 78%, 98%); --app-text-color: 0, 0%, 10%; // Page --page-bgcolor: hsl(0, 0%, 100%); --page-border: hsl(var(--theme-hue), 51%, 86%); // Blocks --math-block-bgcolor: hsl(var(--theme-hue), 60%, 95%); --math-block-selection: hsl(0, 0%, 0%); --math-block-selection-bg: hsl(var(--theme-hue), 60%, 86%); --mafs-plot-color: hsl(var(--theme-hue), 60%, 60%); // Tools Panel --panel-bgcolor: hsl(0, 0%, 100%); --tool-bgcolor: hsl(var(--theme-hue), 51%, 86%); --tool-hover-bgcolor: hsl(var(--theme-hue), 40%, 78%); --tool-active-bgcolor: hsl(var(--theme-hue), 37%, 66%); // Selection --selection-bgcolor: var(--app-accent-color); --selection-color: hsl(0, 0%, 100%); // Scrollbar --scroll-track-bgcolor: hsla(0, 0%, 18%, 0.122); --scroll-thumb-bgcolor: hsla(0, 0%, 23%, 0.3); --scroll-thumb-hover-bgcolor: var(--app-accent-color); // Button --button-border: 1px solid hsl(0, 0%, 92%); --button-bgcolor: hsl(0, 0%, 100%); --button-color: hsl(0, 0%, 26%); --button-shadow-color: hsl(0, 0%, 100%); --button-hover-bgcolor: hsl(0, 0%, 98%); --button-active-bgcolor: hsl(0, 0%, 96%); --pill-bgcolor: hsl(var(--theme-hue), 29%, 86%); --pill-hover-bgcolor: hsl(var(--theme-hue), 29%, 75%); --pill-active-bgcolor: hsl(var(--theme-hue), 29%, 80%); --tag-saturation: 47%; --tag-lightness: 93%; --button-danger: hsl(0, 100%, 90%); --tag-filter: brightness(90%); --clear-button-bgcolor: hsl(0, 41%, 92%); --clear-button-color: hsl(0, 34%, 40%); --clear-button-hover-bgcolor: hsl(0, 44%, 85%); --tool-color: hsl(var(--theme-hue), 23%, 28%); --sidebar-button-color: hsl(var(--theme-hue), 15%, 35%); // File System --file-bgcolor: hsl(var(--theme-hue), 25%, 90%); --file-hover-bgcolor: hsl(var(--theme-hue), 25%, 88%); --file-active-bgcolor: hsl(var(--theme-hue), 25%, 86%); --selected-transparent: hsl(var(--theme-hue), 25%, 88%, 0.5); --selected-bgcolor: hsl(var(--theme-hue), 25%, 88%); --selected-hover-bgcolor: hsl(var(--theme-hue), 25%, 86%); --selected-active-bgcolor: hsl(var(--theme-hue), 25%, 84%); // Command Bar --command-bar-bg: linear-gradient(120deg, hsla(var(--theme-hue), 30%, 90%, 0.7), hsla(var(--theme-hue), 30%, 90%, 0.2), 70%, hsla(var(--theme-hue), 30%, 90%, 0.7)); // Titlebar --titlebar-bgcolor: var(--app-bgcolor); --titlebar-color: hsl(0, 0%, 12%); --titlebar-title-color: var(--titlebar-color); --titlebar-menu-title-hover-bgcolor: hsl(0, 0%, 90%); --titlebar-popup-item-hover-bgcolor: hsl(210deg 16% 92%); --titlebar-popup-bgcolor: hsl(0deg 0% 99%); --titlebar-menu-title-active-bgcolor: hsl(0deg 0% 99%); --titlebar-menu-title-active-border-color: hsl(204, 6%, 84%); --titlebar-popup-border: 1px solid hsl(204, 6%, 84%); --titlebar-menu-separator-bgcolor: hsl(204, 6%, 84%); --titlebar-popup-item-hover-bgcolor: hsl(210, 11%, 85%); --titlebar-popup-item-name-color: hsl(0, 0%, 5%); --titlebar-popup-item-name-shadow-color: hsla(0, 0%, 100%, 0.8); --titlebar-popup-item-hover-color: hsl(0, 0%, 0%); --titlebar-popup-item-hover-shortcut-color: var(--app-accent-color); --titlebar-popup-item-hover-bgcolor: hsl(0, 0%, 90%); // ERWT --erwt-heading-color: hsl(0, 0%, 16%); // Overrides .main-teaser { background: hsl(0, 0%, 100%); color: hsl(0, 0%, 45%); &:after { box-shadow: none; } } .versions .item { color: hsl(0, 0%, 40%); background: hsl(0, 0%, 100%); box-shadow: 0 2px 4px hsla(0, 0%, 0%, 0.02); &>span { color: hsl(0, 0%, 60%); } } } ================================================ FILE: src/renderer/components/common/Modals/ConfirmModal.scss ================================================ @import '../../Theme.scss'; body:has(#main-app.rtl){ .modal-text{ direction: rtl; } } #portal { display: flex; align-items: center; justify-content: center; .overlay { position: fixed; height: 100%; animation: blurPage 0.15s ease-in-out forwards; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.1); transition: 0.2s; z-index: 1000; } .modal { transform-origin: center; direction: var(--app-direction); font-size: larger; animation: popup 0.15s ease-in-out forwards; color: hsla(var(--app-text-color), 1); display: flex; flex-direction: column; position: absolute; box-shadow: 0px 1px 10px #0000005c; top: 40%; background-color: var(--page-bgcolor); padding: 2rem; z-index: 1000; border-radius: 10px; border: 1px solid var(--page-border); text-shadow: none; .actions-container { margin-top: 1rem; display: flex; flex-direction: row; justify-content: space-between; width: 100%; button { background-color: var(--tool-bgcolor); border-radius: 8px; width: 45%; font-size: medium; font-weight: 300; transition: 0.05s; text-shadow: none; border: 1px solid var(--panel-bgcolor); &:hover { transform: scale(1.04); font-weight: 700; background-color: var(--tool-hover-bgcolor); } &:active { transform: scale(0.98); font-weight: 700; background-color: var(--tool-active-bgcolor); } &.danger-button { background-color: var(--button-danger); border: 0px solid transparent; } } } } } @keyframes blurPage { to { -webkit-backdrop-filter: blur(3px); -moz-backdrop-filter: blur(3px); backdrop-filter: blur(3px); } } @keyframes popup { 0% { -webkit-filter: blur(3px); -moz-filter: blur(3px); filter: blur(3px); transform: scale(0.95); } 100% { -webkit-filter: blur(0px); -moz-filter: blur(0px); filter: blur(0px); transform: scale(1); } } ================================================ FILE: src/renderer/components/common/Modals/ConfirmModal.tsx ================================================ import React from 'react'; import ReactDom from 'react-dom'; import './ConfirmModal.scss'; import { useTranslation } from 'react-i18next'; interface ConfirmModalProps { open: boolean; onConfirm: () => void; onCancel: () => void; text: string; } export default function ConfirmModal({ open, onConfirm, onCancel, text }: ConfirmModalProps) { const { t, i18n } = useTranslation(); if (!open) return null; return ReactDom.createPortal( <>
    {text}
    , document.getElementById('portal'), ); } ================================================ FILE: src/renderer/components/common/Modals/ErrorModal.scss ================================================ @import '../../Theme.scss'; #portal { display: flex; align-items: center; justify-content: center; .overlay { position: fixed; height: 100%; animation: blurPage 0.15s ease-in-out forwards; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.1); transition: 0.2s; z-index: 1000; } .modal { transform-origin: center; direction: var(--app-direction); font-size: larger; animation: popup 0.15s ease-in-out forwards; color: hsla(var(--app-text-color), 1); display: flex; flex-direction: column; position: absolute; box-shadow: 0px 1px 10px #0000005c; top: 40%; background-color: var(--page-bgcolor); padding: 2rem; z-index: 1000; border-radius: 10px; border: 1px solid var(--page-border); text-shadow: none; .actions-container { margin-top: 1rem; display: flex; flex-direction: row; justify-content: space-between; width: 100%; button { background-color: var(--tool-bgcolor); border-radius: 8px; width: 45%; font-size: medium; font-weight: 300; transition: 0.05s; text-shadow: none; border: 1px solid var(--panel-bgcolor); &:hover { transform: scale(1.04); font-weight: 700; background-color: var(--tool-hover-bgcolor); } &:active { transform: scale(0.98); font-weight: 700; background-color: var(--tool-active-bgcolor); } &.danger-button { background-color: var(--button-danger); border: 0px solid transparent; } } } } } @keyframes blurPage { to { -webkit-backdrop-filter: blur(3px); -moz-backdrop-filter: blur(3px); backdrop-filter: blur(3px); } } @keyframes popup { 0% { -webkit-filter: blur(3px); -moz-filter: blur(3px); filter: blur(3px); transform: scale(0.95); } 100% { -webkit-filter: blur(0px); -moz-filter: blur(0px); filter: blur(0px); transform: scale(1); } } ================================================ FILE: src/renderer/components/common/Modals/ErrorModal.tsx ================================================ import React, { PropsWithChildren } from 'react'; import ReactDom from 'react-dom'; import './ErrorModal.scss'; import { useTranslation } from 'react-i18next'; interface ErrorModalProps extends PropsWithChildren { open: boolean; onClose: () => void; } export default function ErrorModal({ open, onClose, children, }: ErrorModalProps) { const { t, i18n } = useTranslation(); if (!open) return null; return ReactDom.createPortal( <>
    {children}
    , document.getElementById('portal'), ); } ================================================ FILE: src/renderer/components/common/Notification.scss ================================================ @import '../Theme.scss'; #notification { direction: var(--app-direction); visibility: hidden; z-index: 1; padding: 6px 10px 6px 10px; background-color: var(--button-bgcolor); text-shadow: none; border: 1px solid var(--page-border); border-radius: 6px; color: hsla(var(--app-text-color), 1); text-align: center; align-self: center; position: relative; width: max-content; box-shadow: 0px 1px 3px #0000002a; display: block; bottom: 1px; &.show { visibility: visible; -webkit-animation: fadein 0.2s, fadeout 0.3s 1s; animation: fadein 0.2s, fadeout 0.3s 1s; } } @-webkit-keyframes fadein { from { bottom: 0; opacity: 0; } to { bottom: 1px; opacity: 1; } } @keyframes fadein { from { bottom: 0; opacity: 0; } to { bottom: 1px; opacity: 1; } } @-webkit-keyframes fadeout { from { bottom: 1px; opacity: 1; } to { bottom: 0; opacity: 0; } } @keyframes fadeout { from { bottom: 1px; opacity: 1; } to { bottom: 0; opacity: 0; } } ================================================ FILE: src/renderer/components/common/Notification.tsx ================================================ import React from 'react'; import './Notification.scss'; import { useTranslation } from 'react-i18next'; type NotificationProps = { scene: string; }; export const Notification = ({ scene }: NotificationProps) => { const { t, i18n } = useTranslation(); let content = ''; switch (scene) { case '': content = ''; break; case 'save': content = t("Saved"); break; case 'error': content = t("Save Error"); break; default: break; } return (
    {content}
    ); }; export const triggerPopupAnimation = (type: string, setPopupType: (args: string) => unknown) => { setTimeout(() => { setPopupType(type); setTimeout(() => { setPopupType(''); }, 1200); }, 0); }; ================================================ FILE: src/renderer/components/common/Shortcut.tsx ================================================ import React from 'react'; type ShortcutProps = { shortcut: string[]; }; const Shortcut = ({ shortcut }: ShortcutProps) => { const keys = shortcut.map((k: string) => ( <>{k == shortcut[0] ? '' : '+'} {k} )); return ( {keys ? keys : <>} ); }; export default Shortcut; ================================================ FILE: src/renderer/hooks/useAddBlock.tsx ================================================ import { useEffect } from 'react'; import { BlockElement, WidgetType, newWidgetRequest, } from '@renderer/common/types'; import { useGeneralContext } from '@components/GeneralContext'; export function useAddBlock(setStateFunction: (...args: unknown[]) => unknown) { const { newWidgetRequest } = useGeneralContext(); useEffect(() => { if (newWidgetRequest) AddWidget(newWidgetRequest); }, [newWidgetRequest]); const AddWidget = (newWidgetRequest: newWidgetRequest) => { const handler = addWidgetHandlersMap.get(newWidgetRequest.widgetType); if (handler) handler(); }; function addBlockByType( blockType: number, height: number, width?: number, minHeight?: number, minWidth?: number, maxHeight?: number, maxWidth?: number, ) { setStateFunction((prev: { items: BlockElement[] }) => ({ ...prev, items: [ ...prev.items, { type: blockType, i: crypto.randomUUID(), metaData: {}, x: Infinity, y: Infinity, h: height, w: width ? width : 8, minH: minHeight ? minHeight : 1, minW: minWidth ? minWidth : 1, maxH: maxHeight ? maxHeight : 100, maxW: maxWidth ? maxWidth : 8, } as BlockElement, ], })); } const addWidgetHandlersMap = new Map void>([ [WidgetType.Divider, () => addBlockByType(WidgetType.Divider, 1, 8, 1, 1, 1)], [WidgetType.Graph, () => addBlockByType(WidgetType.Graph, 6, 8, 2, 2)], [WidgetType.Text, () => addBlockByType(WidgetType.Text, 2)], [WidgetType.Math, () => addBlockByType(WidgetType.Math, 2)], [WidgetType.Draw, () => addBlockByType(WidgetType.Draw, 6, 8, 4, 4)], [WidgetType.Picture, () => console.error('not implemented')], [WidgetType.Group, () => console.error('not implemented')], ]); } ================================================ FILE: src/renderer/hooks/useDialog.tsx ================================================ import { BlockElement } from '@renderer/common/types'; import { useEffect } from 'react'; import { useGeneralContext } from '@components/GeneralContext'; export function useDialog( setStateFunction: (...args: unknown[]) => unknown, setAllBlockValues: (...args: unknown[]) => unknown, setClearModalOpen: (args: boolean) => void, ) { const { clearPageRequest } = useGeneralContext(); const emptyArray: BlockElement[] = []; useEffect(() => { if (clearPageRequest.cmd === 'clear') setClearModalOpen(true); }, [clearPageRequest]); const handleDialogConfirm = () => { setClearModalOpen(false); setStateFunction((prev: { items: BlockElement[] }) => ({ ...prev, items: emptyArray, })); setAllBlockValues([]); }; const handleDialogCancel = () => setClearModalOpen(false); return { handleDialogCancel, handleDialogConfirm }; } ================================================ FILE: src/renderer/hooks/useFileSaveLoad.tsx ================================================ import { useGeneralContext } from '@components/GeneralContext'; import { useEffect } from 'react'; import { triggerPopupAnimation } from '@components/common/Notification'; import { BlockElement, FileStructure, PageGridState, BlockState, } from '@renderer/common/types'; export function useFileSaveLoad( state: PageGridState, setStateFunction: (...args: unknown[]) => unknown, allBlockValues: BlockState[], setAllBlockValues: (...args: unknown[]) => unknown, setPopupType: (args: string) => void, ) { const emptyArray: BlockElement[] = []; const { selectedFile, saveRequest, currentFileTags, setCurrentFileTags } = useGeneralContext(); const setEmptyPage = () => setStateFunction((prev: { items: BlockElement[] }) => ({ ...prev, items: emptyArray, })); useEffect( function loadFile() { if (selectedFile) window.api.loadX(selectedFile); window.api.receive('gotLoadedDataX', (data: string) => { if (!data) { setEmptyPage(); return; } const parsedData = JSON.parse(data); const blocksData: BlockElement[] = parsedData.blocks; parsedData.tags ? setCurrentFileTags(JSON.parse(data).tags) : setCurrentFileTags([]); if (!Array.isArray(blocksData)) { setEmptyPage(); return; } blocksData.map((block: BlockElement) => { if (block.y == null) { block.y = Infinity; } if (block.x == null) { block.x = Infinity; } }); setStateFunction((prev: { items: BlockElement[] }) => ({ ...prev, items: blocksData, })); let newData: object[] = blocksData; newData = newData.map((block: BlockElement) => { return { id: block.i, metaData: block.metaData }; }); setAllBlockValues(newData); }); }, [selectedFile], ); useEffect( function saveFile() { if (saveRequest?.cmd === 'save') { if (selectedFile) { try { saveGridDataToFile(); triggerPopupAnimation('save', setPopupType); } catch (error) { triggerPopupAnimation('error', setPopupType); console.error(error); } } else triggerPopupAnimation('firstSelect', setPopupType); } }, [saveRequest], ); const saveMetaDataPerBlock = (block: BlockElement) => { const found = allBlockValues.find( blockState => blockState.id == block.i, ).metaData; block.metaData = { content: found.content, blockStateFunction: () => null, }; }; const saveGridDataToFile = () => { const currentItems = state.items; currentItems.map(saveMetaDataPerBlock); const fileData: FileStructure = { blocks: currentItems, tags: currentFileTags, mathMemory: {}, }; window.api.saveX(JSON.stringify(fileData), selectedFile); }; } ================================================ FILE: src/renderer/hooks/useSettings.tsx ================================================ import { useState } from 'react'; function useSettings() { const [isRtl, setIsRtl] = useState(true); const [language, setLanguage] = useState('en'); const [darkTheme, setDarkTheme] = useState(true); const [colorTheme, setColorTheme] = useState(''); const [currentOS, setCurrentOS] = useState(''); const [isFilesSidebarOpen, setIsFilesSidebarOpen] = useState(false); const [isMathSidebarOpen, setIsMathSidebarOpen] = useState(false); return { isRtl, setIsRtl, language, setLanguage, darkTheme, setDarkTheme, colorTheme, setColorTheme, currentOS, setCurrentOS, isFilesSidebarOpen, setIsFilesSidebarOpen, isMathSidebarOpen, setIsMathSidebarOpen, }; } export default useSettings; ================================================ FILE: src/typings/index.d.ts ================================================ declare module '*.css'; declare module '*.png'; declare module '*.jpg'; declare module '*.jpeg'; declare module '*.svg'; import { DOMAttributes } from "react"; import { MathfieldElementAttributes } from 'mathlive' type CustomElement = Partial>; declare global { namespace JSX { interface IntrinsicElements { ["math-field"]: CustomElement; } } } ================================================ FILE: tools/forge/forge.config.js ================================================ // Forge Configuration const path = require('path'); const rootDir = process.cwd(); module.exports = { // Packager Config packagerConfig: { // Create asar archive for main, renderer process files asar: true, // Set executable name executableName: 'Mathberet', // Set application copyright appCopyright: 'Copyright (C) 2023 Mathberet, yonatanmgr', // Set application icon icon: path.resolve('assets/images/appIcon.ico'), }, // Forge Makers makers: [ { // Squirrel.Windows is a no-prompt, no-hassle, no-admin method of installing // Windows applications and is therefore the most user friendly you can get. name: '@electron-forge/maker-squirrel', config: { name: 'mathberet', }, }, { // The Zip target builds basic .zip files containing your packaged application. // There are no platform specific dependencies for using this maker and it will run on any platform. name: '@electron-forge/maker-zip', platforms: ['darwin'], }, { // The deb target builds .deb packages, which are the standard package format for Debian-based // Linux distributions such as Ubuntu. name: '@electron-forge/maker-deb', config: {}, }, { // The RPM target builds .rpm files, which is the standard package format for // RedHat-based Linux distributions such as Fedora. name: '@electron-forge/maker-rpm', config: {}, }, ], // Forge Plugins plugins: [ { name: '@electron-forge/plugin-webpack', config: { // Fix content-security-policy error when image or video src isn't same origin // Remove 'unsafe-eval' to get rid of console warning in development mode. devContentSecurityPolicy: `default-src 'self' 'unsafe-inline' data:; script-src 'self' 'unsafe-inline' data:`, // Ports port: 3000, // Webpack Dev Server port loggerPort: 9000, // Logger port // Main process webpack configuration mainConfig: path.join(rootDir, 'tools/webpack/webpack.main.js'), // Renderer process webpack configuration renderer: { // Configuration file path config: path.join(rootDir, 'tools/webpack/webpack.renderer.js'), // Entrypoints of the application entryPoints: [ { // Window process name name: 'app_window', // React Hot Module Replacement (HMR) rhmr: 'react-hot-loader/patch', // HTML index file template html: path.join(rootDir, 'src/renderer/app.html'), // Renderer js: path.join(rootDir, 'src/renderer/appRenderer.tsx'), // Main Window // Preload preload: { js: path.join(rootDir, 'src/renderer/appPreload.tsx'), }, }, ], }, devServer: { liveReload: false, }, }, }, ], }; ================================================ FILE: tools/webpack/webpack.aliases.js ================================================ const { createWebpackAliases } = require('./webpack.helpers'); // Export aliases module.exports = createWebpackAliases({ '@assets': 'assets', '@components': 'src/renderer/components', '@common': 'src/common', '@main': 'src/main', '@renderer': 'src/renderer', '@src': 'src', '@misc': 'misc', }); ================================================ FILE: tools/webpack/webpack.helpers.js ================================================ const path = require('path'); const cwd = process.cwd(); function inDev() { return process.env.NODE_ENV == 'development'; } function createWebpackAliases (aliases) { const result = {}; for (const name in aliases) { result[name] = path.join(cwd, aliases[name]); } return result; } module.exports = { inDev, createWebpackAliases, }; ================================================ FILE: tools/webpack/webpack.main.js ================================================ module.exports = { /** * This is the main entry point for your application, it's the first file * that runs in the main process. */ entry: ['./src/main/app.ts'], // Put your normal webpack config below here module: { rules: require('./webpack.rules'), }, resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], alias: require('./webpack.aliases'), }, stats: 'minimal', }; ================================================ FILE: tools/webpack/webpack.plugins.js ================================================ const webpack = require('webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); const { inDev } = require('./webpack.helpers'); module.exports = [ new ForkTsCheckerWebpackPlugin(), inDev() && new webpack.HotModuleReplacementPlugin(), inDev() && new ReactRefreshWebpackPlugin(), ].filter(Boolean); ================================================ FILE: tools/webpack/webpack.renderer.js ================================================ const rules = require('./webpack.rules'); const plugins = require('./webpack.plugins'); module.exports = { module: { rules, }, plugins: plugins, resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], alias: { // Custom Aliases ...require('./webpack.aliases'), }, }, stats: 'minimal', /** * Fix: Enable inline-source-map to fix following: * Dev tools: unable to load source maps over custom protocol */ devtool: 'inline-source-map', }; ================================================ FILE: tools/webpack/webpack.rules.js ================================================ module.exports = [ { // Add support for native node modules test: /native_modules\/.+\.node$/, use: 'node-loader', }, { test: /\.(m?js|node)$/, parser: { amd: false }, use: { loader: '@vercel/webpack-asset-relocator-loader', options: { outputAssetBase: 'native_modules', }, }, }, { // Typescript loader test: /\.tsx?$/, exclude: /(node_modules|\.webpack)/, use: { loader: 'ts-loader', options: { transpileOnly: true, }, }, }, { // CSS Loader test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], }, { // SCSS (SASS) Loader test: /\.s[ac]ss$/i, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader' }, ], }, { // Less loader test: /\.less$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'less-loader' }, ], }, { // Assets loader // More information here https://webpack.js.org/guides/asset-modules/ test: /\.(gif|jpe?g|tiff|png|webp|bmp|svg|eot|ttf|woff|woff2)$/i, type: 'asset', generator: { filename: 'assets/[hash][ext][query]', }, }, ]; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "forceConsistentCasingInFileNames": true, "jsx": "react", "allowJs": true, "target": "ES6", "module": "ESNext", "skipLibCheck": true, "esModuleInterop": true, "noImplicitAny": true, "sourceMap": true, "baseUrl": ".", "outDir": "dist", "moduleResolution": "node", "resolveJsonModule": true, "paths": { "*": ["node_modules/*"], "@assets/*": ["./assets/*"], "@components/*": ["./src/renderer/components/*"], "@common/*": ["./src/common/*"], "@main/*": ["./src/main/*"], "@renderer/*": ["./src/renderer/*"], "@src/*": ["./src/*"], "@misc/*": ["./misc/*"], } }, "include": ["src/**/*", "tools/**/*"] }