├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── release.yml ├── .gitignore ├── .hintrc ├── .prettierignore ├── HELP.md ├── LICENSE ├── README.md ├── assets ├── left-arrow.svg ├── nb.svg ├── notion-boost.svg ├── readme │ ├── bmc.png │ └── header.jpg └── twitter.svg ├── components ├── SettingsTable.tsx ├── features │ ├── codeLineNumbers.ts │ ├── openFullPage.ts │ ├── outline.ts │ ├── pageElements.ts │ ├── rollupUrlClickable.ts │ ├── scrollToTopBtn.ts │ └── spellCheckForCode.ts ├── settings.ts └── utils.ts ├── entrypoints ├── content.ts └── popup │ ├── About.tsx │ ├── App.tsx │ ├── index.html │ └── main.tsx ├── package.json ├── pnpm-lock.yaml ├── prettier.config.cjs ├── public └── icon │ ├── icon.png │ ├── icon128.png │ ├── icon16.png │ ├── icon19.png │ ├── icon24.png │ ├── icon32.png │ ├── icon38.png │ ├── icon48.png │ ├── icon64.png │ └── icon96.png ├── styles ├── button.scss ├── content.scss ├── popup.scss └── theme.scss ├── tsconfig.json ├── web-ext.config.ts └── wxt.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | /build_* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "webextensions": true 8 | }, 9 | "parser": "@babel/eslint-parser", 10 | "parserOptions": { 11 | "ecmaVersion": 11, 12 | "requireConfigFile": false, 13 | "ecmaFeatures": { 14 | "impliedStrict": true 15 | }, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["promise", "react"], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:react/recommended", 22 | "plugin:promise/recommended", 23 | "plugin:import/errors", 24 | "plugin:import/warnings", 25 | "plugin:react-hooks/recommended", 26 | "plugin:react/jsx-runtime" 27 | ], 28 | "rules": { 29 | "consistent-return": "off", 30 | "promise/always-return": "off", 31 | "semi": "off", 32 | "camelcase": "off", 33 | "lines-between-class-members": "off", 34 | "implicit-arrow-linebreak": "off", 35 | "object-curly-newline": "off", 36 | "prefer-destructuring": "off", 37 | "comma-spacing": "off", 38 | "react-hooks/exhaustive-deps": "error", 39 | "no-multiple-empty-lines": "off", 40 | "indent": "off", 41 | "no-lonely-if": "off", 42 | "no-loop-func": "off", 43 | "space-before-blocks": "off", 44 | "space-infix-ops": "off", 45 | "keyword-spacing": "off", 46 | "no-empty": "off", 47 | "no-trailing-spaces": "off", 48 | "padded-blocks": "off", 49 | "max-len": "off", 50 | "operator-linebreak": "off", 51 | "comma-dangle": "off", 52 | "linebreak-style": "off", 53 | "quotes": "off", 54 | "promise/no-nesting": "off", 55 | "no-unused-vars": "off", 56 | "no-console": "off", 57 | "no-nested-ternary": "off", 58 | "no-unused-expressions": "off", 59 | "import/prefer-default-export": "off", 60 | "promise/catch-or-return": ["error", { "allowFinally": true }], 61 | "no-use-before-define": "off", 62 | "no-debugger": "off", 63 | "no-plusplus": "off", 64 | "no-restricted-syntax": "off", 65 | "no-param-reassign": "off", 66 | "import/extensions": "off", 67 | "react/jsx-no-target-blank": "off", 68 | "import/no-unresolved": "off" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: gorvgoyl 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: gorvgoyl 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - ignore-for-release 7 | authors: 8 | - octocat 9 | categories: 10 | - title: Breaking Changes 🛠 11 | labels: 12 | - Semver-Major 13 | - breaking-change 14 | - title: Exciting New Features 🎉 15 | labels: 16 | - Semver-Minor 17 | - enhancement 18 | - title: Other Changes 19 | labels: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .output 12 | stats.html 13 | stats-*.json 14 | .wxt 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | build_chrome 25 | build_firefox 26 | build 27 | chrome-mv3.pem 28 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "compat-api/css": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | /build_* 4 | build 5 | -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Add new Feature 2 | 3 | - add feature info to `src\js\settings.js` 4 | - add code to relevant file in `src\js\feature\` like `pageElements.js` 5 | - add CSS if any to `src\css\content.scss` 6 | - Add feature info to [`CHANGELOG.md`](./CHANGELOG.md) 7 | 8 | --- 9 | 10 | # Doc HTML Structure 11 | 12 | - body.notion-body /dark 13 | - #notion-app /notion-boost-classes 14 | - notion-app-inner notion-light-theme /notion-dark-theme 15 | - notion-cursor-listener _resets when dragging some bullet point_ 16 | - notion-sidebar-container 17 | - notion-frame _stays on doc change_ 18 | - notion-scroller vertical horizontal _resets_ 19 | - notion-page-content _absent in case of full page table_ 20 | - blocks 21 | - notion-presence-container _resets_ _present for both full page table and full page content_ 22 | - notion-overlay-container notion-default-overlay-container (_stays_) 23 | - notion-peek-renderer 24 | - notion-overlay-container 25 | 26 | # Run in Firefox 27 | 28 | - run command from package.json `npm run start:ff` 29 | - open url in firefox 30 | - about:debugging#/runtime/this-firefox 31 | - pick manifest.json from folder `build_firefox` 32 | 33 | # Theme 34 | 35 | ## Dark 36 | 37 | - notion-body dark 38 | - notion-app 39 | - notion-app-inner notion-dark-theme 40 | 41 | ## Light 42 | 43 | - notion-body 44 | - notion-app 45 | - notion-app-inner notion-light-theme 46 | 47 | --- 48 | 49 | # Tables 50 | 51 | ## Full page table 52 | 53 | - notion-app-inner notion-light-theme 54 | - notion-cursor-listener 55 | - notion-sidebar-container 56 | - notion-frame 57 | - notion-scroller vertical horizontal 58 | - notion-scroller 59 | - notion-board-view 60 | - notion-selectable notion-collection_view_page-block 61 | 62 | ### new cell in full page table 63 | 64 | - notion-app-inner notion-light-theme 65 | - notion-overlay-container notion-default-overlay-container 66 | - notion-peek-renderer 67 | - notion-scroller vertical 68 | - `notion-page-content` 69 | - notion-selectable notion-page-block 70 | - 71 | 72 | ## Width - half 73 | 74 | - notion-scroller vertical horizontal 75 | - div 76 | - div (width: 900px) 77 | - div 78 | - div (width: 900px) 79 | - notion-page-content (width: 900px) 80 | - table 81 | 82 | ## Width - Full 83 | 84 | - notion-scroller vertical horizontal 85 | - div 86 | - div (width: 100%;) 87 | - div 88 | - div (width: 100%;) 89 | - notion-page-content (width: 100%) 90 | - table 91 | 92 | ## Font 93 | 94 | - notion-selectable notion-page-block (32px / 40px) 95 | - notion-page-content (14px / 16px) 96 | - 97 | 98 | ## Inline Tables 99 | 100 | ### Not Full width inline 101 | 102 | - notion-page-content (width: 900px;) -diff 103 | - notion-selectable notion-collection_view-block (width: 1263px;) (full page horz scrollbar) -dynamic 104 | - div (padding-left: 277.5px;padding-right: 277.5px;) -diff 105 | - notion-scroller horizontal 106 | - notion-table-view (padding-left: 277.5px;padding-right: 277.5px;) / notion-board-view -diff 107 | - notion-selectable notion-collection_view-block 108 | - div (min-width: 708px;) -diff 109 | 110 | ### Full width inline 111 | 112 | - notion-page-content (width: 100%;) -diff 113 | - notion-selectable notion-collection_view-block (width: 1263px;) (full page horz scrollbar) -dynamic 114 | - div (padding-left: 96px;padding-right: 96px;) -diff 115 | - notion-scroller horizontal 116 | - notion-table-view (padding-left: 96px;padding-right: 96px;) / notion-board-view -diff 117 | - notion-selectable notion-collection_view-block 118 | - div (min-width: calc(100% - 192px);) -diff 119 | 120 | # Docs 121 | 122 | ## CSS Selectors 123 | 124 | - CSS selectors based on inline styles: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors 125 | 126 | ```js 127 | // starts with 128 | $('div[style^="width:100;"]'); 129 | 130 | // contains 131 | $('div[style*="width:100;"]'); 132 | 133 | // ends with 134 | $('div[style$="width:100;"]'); 135 | 136 | // contains 2 styles (anywhere in inline styles) 137 | $('div[style*="width:100;"][style*="display:flex;"]'); 138 | 139 | // one of the classes should exactly be "notion-view" 140 | $('div[class~="notion-view"]'); 141 | 142 | // select first element of its type among a group of sibling 143 | $('div[style*="width:100;"]:first-of-type'); 144 | ``` 145 | 146 | ## simulate keys 147 | 148 | - https://stackoverflow.com/a/71195647/3073272 149 | - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent 150 | - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key 151 | 152 | ```js 153 | function simulatePointerdownEvent(element) { 154 | // const element = document.querySelector('div.sticky div[type="button"][id]'); 155 | 156 | // Calculate the center of the element 157 | const rect = element.getBoundingClientRect(); 158 | const centerX = rect.left + rect.width / 2; 159 | const centerY = rect.top + rect.height / 2; 160 | 161 | // Create the pointerdown event 162 | const event = new PointerEvent("pointerdown", { 163 | pointerId: 1, 164 | bubbles: true, 165 | cancelable: true, 166 | clientX: centerX, 167 | clientY: centerY, 168 | button: 0, 169 | // Additional properties can be added as needed 170 | }); 171 | 172 | // Dispatch the event on the target element 173 | element.dispatchEvent(event); 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Notion Boost](./assets/readme/header.jpg) 2 | 3 | # Notion Boost browser extension 4 | 5 | > Browser extension to add 20+ features to Notion.so like sticky outline (table of contents), small text & full width by default,scroll to top button, hide slash command menu, hide help button, bolder text and more. 6 | 7 |
8 | Chrome extension versionChrome usersChrome extension starsFirefox versionFirefox users 59 |
60 | 61 | ## 🏠 Homepage 62 | 63 | https://gourav.io/notion-boost 64 | 65 | ### ⬇ Downloads 66 | 67 | - Chrome/Edge/Brave extension - https://chrome.google.com/webstore/detail/notion-boost/eciepnnimnjaojlkcpdpcgbfkpcagahd 68 | - Firefox addon - https://addons.mozilla.org/en-US/firefox/addon/notion-boost/ 69 | 70 | ### ⭐ All Features 71 | 72 | https://gourav.io/notion-boost#-currently-added-features 73 | 74 | Missing some feature? [suggest it](https://github.com/GorvGoyl/Notion-Boost-browser-extension/issues) 75 | 76 | See what's new in latest update: https://gourav.io/notion-boost/whats-new 77 | 78 | --- 79 | 80 | #### 👍 Liked this extension? express your love by rating [★★★★★](https://chrome.google.com/webstore/detail/notion-boost/eciepnnimnjaojlkcpdpcgbfkpcagahd) on Chrome/Firefox store. 81 | 82 | 83 | 84 | #### 👨‍💻 See my other projects: https://gourav.io 85 | -------------------------------------------------------------------------------- /assets/left-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/nb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/notion-boost.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/readme/bmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/assets/readme/bmc.png -------------------------------------------------------------------------------- /assets/readme/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/assets/readme/header.jpg -------------------------------------------------------------------------------- /assets/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/SettingsTable.tsx: -------------------------------------------------------------------------------- 1 | import { settingDetails } from './settings'; 2 | import { getElement, getLatestSettings } from './utils'; 3 | import { Fragment } from 'react'; 4 | 5 | function isFuncEnabled(func: any) { 6 | const btnToDisable = getElement(`[data-func=${func}]`); 7 | 8 | if (btnToDisable && btnToDisable.classList.contains('enable')) { 9 | return true; 10 | } 11 | return false; 12 | } 13 | 14 | export function SettingsTable() { 15 | const [filterText, setFilterText] = useState(''); 16 | const [settings, setSettings] = useState(settingDetails); 17 | 18 | useEffect(() => { 19 | refreshSettings(); 20 | }, []); 21 | 22 | const refreshSettings = () => { 23 | console.log('refreshing settings'); 24 | const obj = [...settingDetails]; 25 | 26 | getLatestSettings() 27 | .then((set: any) => { 28 | console.log('LatestSettings: ', set); 29 | 30 | // add current status to settings object 31 | obj.forEach((e: any) => { 32 | e.status = set[e.func] ? 'enable' : 'disable'; 33 | }); 34 | 35 | setSettings([...obj]); 36 | 37 | return null; 38 | }) 39 | .catch((e) => console.log(e)); 40 | }; 41 | 42 | const handleFeature = useCallback((obj: any) => { 43 | console.log('obj', obj); 44 | 45 | console.log('clicked: '); 46 | 47 | const func = obj.func; 48 | const funcToDisable = obj.disable_func; 49 | 50 | let toEnable = false; 51 | 52 | if (obj.status === 'enable') { 53 | toEnable = false; 54 | } else { 55 | toEnable = true; 56 | } 57 | 58 | try { 59 | getLatestSettings() 60 | .then((set: any) => { 61 | console.log('getLatestSettings ->', set); 62 | set[func] = toEnable; 63 | set.call_func = { 64 | name: func, 65 | arg: toEnable, 66 | }; 67 | 68 | // disable other related func if both are enabled 69 | if (funcToDisable && isFuncEnabled(funcToDisable) && toEnable) { 70 | set[funcToDisable] = false; 71 | } 72 | 73 | browser.storage.sync.set({ nb_settings: set }).then(() => { 74 | refreshSettings(); 75 | }); 76 | return null; 77 | }) 78 | .catch((e) => console.log(e)); 79 | } catch (e) { 80 | console.log(e); 81 | } 82 | }, []); 83 | 84 | const filteredItems = settings.filter( 85 | (item) => 86 | item.name.toLocaleLowerCase().includes(filterText) || item.desc.toLocaleLowerCase().includes(filterText), 87 | ); 88 | console.log('filtered items', filteredItems); 89 | 90 | const itemsToDisplay = filterText ? filteredItems : settings; 91 | 92 | const handleOnchange = (e: any) => { 93 | console.log('value changed'); 94 | setFilterText(e.target.value.toLocaleLowerCase()); 95 | }; 96 | return ( 97 | <> 98 | 106 |
107 | {!filteredItems.length &&
No setting found
} 108 | {itemsToDisplay.map((obj, index) => ( 109 | 110 |
handleFeature(obj)}> 115 |
116 |
{obj.name}
117 | {obj.desc &&
{obj.desc}
} 118 |
119 |
124 |
125 |
126 |
127 |
128 |
129 | {/* {index === settingDetails.length - 1 ? ( 130 |
131 | ) : ( 132 |
133 |
134 |
135 | )} */} 136 | 137 | ))} 138 |
139 | 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /components/features/codeLineNumbers.ts: -------------------------------------------------------------------------------- 1 | import { getElement, onElementLoaded, toElement } from '../utils'; 2 | 3 | const notionAppInnerCls = '.notion-app-inner'; 4 | const notionPageContentCls = '.notion-page-content'; 5 | const notionCursorListenerCls = '.notion-cursor-listener'; 6 | const codeLineNumbersCls = 'codeLineNumbers'; 7 | 8 | const DEBUG = false; 9 | 10 | let docEditObserverObj = {}; 11 | 12 | export function codeLineNumbers(isEnabled: boolean) { 13 | try { 14 | console.log(`feature: codeLineNumbers: ${isEnabled}`); 15 | 16 | // triggers on page load 17 | // it waits for doc to be loaded 18 | onElementLoaded(notionPageContentCls) 19 | .then((isPresent) => { 20 | if (isPresent) { 21 | if (isEnabled) { 22 | addCodeLineNumbers(); 23 | docEditListener(); 24 | } else { 25 | removeCodeLineNumbers(); 26 | } 27 | } 28 | return true; 29 | }) 30 | .catch((e) => console.log(e)); 31 | } catch (e) { 32 | console.log(e); 33 | } 34 | } 35 | 36 | // Internal methods // 37 | 38 | function removeDocEditListener() { 39 | if (isObserverType(docEditObserverObj)) { 40 | DEBUG && console.log('disconnected docEditObserver'); 41 | (docEditObserverObj as MutationObserver).disconnect(); 42 | } 43 | } 44 | 45 | function isObserverType(obj: any) { 46 | return obj.disconnect !== undefined; 47 | } 48 | 49 | // add/update line if any code block change occurs 50 | // works for page change or window reload 51 | function docEditListener() { 52 | DEBUG && console.log('listening for doc edit changes...'); 53 | let isCodeChanged = false; 54 | 55 | let block = ''; 56 | docEditObserverObj = new MutationObserver((mutationList, obsrvr) => { 57 | DEBUG && console.log('found changes in doc content'); 58 | 59 | for (let i = 0; i < mutationList.length; i++) { 60 | const m = mutationList[i]; 61 | 62 | // case: check for text change 63 | if (m.type === 'childList') { 64 | if ( 65 | m.target && 66 | m.target.parentNode && 67 | // @ts-ignore 68 | m.target.parentNode.classList.contains('line-numbers') 69 | ) { 70 | isCodeChanged = true; 71 | // @ts-ignore 72 | block = m.target.parentNode; 73 | // briefly pause listener to avoid recursive triggers 74 | removeDocEditListener(); 75 | updateCodeline(block); 76 | docEditListener(); 77 | } 78 | } 79 | } 80 | 81 | if (isCodeChanged) { 82 | console.log('code changed'); 83 | isCodeChanged = false; 84 | } 85 | }); 86 | 87 | // now add listener for doc text change 88 | const pageContentEl = getElement(notionAppInnerCls); 89 | 90 | (docEditObserverObj as MutationObserver).observe(pageContentEl, { 91 | childList: true, 92 | subtree: true, 93 | }); 94 | } 95 | 96 | function addCodeLineNumbers() { 97 | const el1 = getElement(notionCursorListenerCls); 98 | 99 | el1.classList.add(codeLineNumbersCls); 100 | 101 | const el = document.querySelectorAll('div.notion-page-content .notion-code-block.line-numbers'); 102 | 103 | el.forEach((x) => { 104 | updateCodeline(x); 105 | return true; 106 | }); 107 | } 108 | 109 | function removeCodeLineNumbers() { 110 | console.log('removing removeCodeLineNumbers feature...'); 111 | 112 | removeDocEditListener(); 113 | 114 | const el = getElement(notionCursorListenerCls); 115 | 116 | el.classList.remove(codeLineNumbersCls); 117 | 118 | document.querySelectorAll('.code-line-numbers').forEach((x) => x.remove()); 119 | 120 | // clearOutline(); 121 | 122 | console.log('removed removeCodeLineNumbers feature'); 123 | } 124 | 125 | // credit @CloudHill 126 | function updateCodeline(block: any) { 127 | let lineNumbers = ''; 128 | 129 | let numbers = block.querySelector('.code-line-numbers'); 130 | if (!numbers) { 131 | numbers = toElement(''); 132 | 133 | const blockStyle = window.getComputedStyle(block.children[0]); 134 | numbers.style.top = blockStyle.paddingTop; 135 | numbers.style.bottom = blockStyle.paddingBottom; 136 | 137 | block.append(numbers); 138 | 139 | const temp = toElement('A'); 140 | block.firstChild.append(temp); 141 | block.lineHeight = temp.getBoundingClientRect().height; 142 | temp.remove(); 143 | } 144 | 145 | const lines = block.firstChild.innerText.split(/\r\n|\r|\n/); 146 | if (lines[lines.length - 1] === '') lines.pop(); 147 | let lineCounter = 0; 148 | const wordWrap = block.firstChild.style.wordBreak === 'break-all'; 149 | 150 | for (let i = 0; i < lines.length; i++) { 151 | lineCounter++; 152 | lineNumbers += `${lineCounter}\n`; 153 | 154 | if (wordWrap) { 155 | const temp = document.createElement('span'); 156 | temp.innerText = lines[i]; 157 | block.firstChild.append(temp); 158 | const lineHeight = temp.getBoundingClientRect().height; 159 | temp.remove(); 160 | 161 | for (let j = 1; j < lineHeight / block.lineHeight - 1; j++) { 162 | lineNumbers += '\n'; 163 | } 164 | } 165 | } 166 | 167 | if (lineNumbers.length > 2) { 168 | block.firstChild.classList.add('code-numbered'); 169 | numbers.innerText = lineNumbers; 170 | } else { 171 | block.firstChild.classList.remove('code-numbered'); 172 | numbers.innerText = ''; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /components/features/openFullPage.ts: -------------------------------------------------------------------------------- 1 | import { isObserverType, onElementLoaded } from '../utils'; 2 | 3 | const notionDefaultOverlayCls = '.notion-default-overlay-container'; 4 | 5 | const DEBUG = true; 6 | 7 | let docEditObserverObj = {}; 8 | 9 | export function openFullPage(isEnabled: boolean) { 10 | try { 11 | console.log(`feature: enableOpenFullPage: ${isEnabled}`); 12 | 13 | // triggers on page load 14 | // it waits for overlay to be loaded 15 | onElementLoaded(notionDefaultOverlayCls) 16 | .then((isPresent) => { 17 | if (isPresent) { 18 | if (isEnabled) { 19 | addOpenFullPage(); 20 | } else { 21 | removeOpenFullPage(); 22 | } 23 | } 24 | return true; 25 | }) 26 | .catch((e) => console.log(e)); 27 | } catch (e) { 28 | console.log(e); 29 | } 30 | } 31 | 32 | // Internal methods // 33 | 34 | function removeDocEditListener() { 35 | if (isObserverType(docEditObserverObj)) { 36 | DEBUG && console.log('disconnected docEditObserver'); 37 | (docEditObserverObj as MutationObserver).disconnect(); 38 | } 39 | } 40 | let lastPageID: any; 41 | let previousUrl = ''; 42 | /* 43 | Algo: 44 | 45 | case 1: open page and bypass preview 46 | id -> id?p / id&p -> p 47 | 48 | here lastPageID is id 49 | 50 | case 2: navigate back and bypass preview 51 | id <- id?p / id&p <- p 52 | 53 | here lastPageID is p 54 | */ 55 | function docEditListener() { 56 | console.log('listening for doc edit changes for openFullPage...'); 57 | 58 | docEditObserverObj = new MutationObserver((mutationList, obsrvr) => { 59 | DEBUG && console.log('found changes in doc content'); 60 | 61 | const currentUrl = window.location.href; 62 | if (window.location.href !== previousUrl) { 63 | DEBUG && console.log(`URL changed from ${previousUrl} to ${currentUrl}`); 64 | previousUrl = currentUrl; 65 | 66 | // save last page url 67 | const isPreviewPage = currentUrl.includes('&p=') || currentUrl.includes('?p='); 68 | if (!isPreviewPage) { 69 | // Credits: @dragonwocky 70 | lastPageID = (window.location.search 71 | .slice(1) 72 | .split('&') 73 | .map((opt) => opt.split('=')) 74 | .find((opt) => opt[0] === 'p') || ['', ...window.location.pathname.split(/(-|\/)/g).reverse()])[1]; 75 | } 76 | // case: check for div change 77 | const fullPageLink = document.querySelector(".notion-peek-renderer [style*='display: grid'] > a"); 78 | 79 | if (!fullPageLink) return; 80 | 81 | const href = fullPageLink.getAttribute('href'); 82 | const pathname = fullPageLink.getAttribute('pathname'); 83 | if (!href || !pathname) return; 84 | 85 | const previewPageID = (href 86 | .slice(1) 87 | .split('&') 88 | .map((opt: any) => opt.split('=')) 89 | .find((opt: any) => opt[0] === 'p') || ['', ...pathname.split(/(-|\/)/g).reverse()])[1]; 90 | 91 | if (isPreviewPage) { 92 | if (previewPageID === lastPageID) { 93 | DEBUG && console.log('going back', lastPageID); 94 | window.history.back(); 95 | } else { 96 | DEBUG && console.log('full page link found', fullPageLink.getAttribute('href')); 97 | (fullPageLink as HTMLElement).click(); 98 | } 99 | } 100 | } 101 | }); 102 | 103 | // now add listener for doc text change 104 | 105 | (docEditObserverObj as MutationObserver).observe(document, { 106 | childList: true, 107 | subtree: true, 108 | // attributes: true, 109 | }); 110 | } 111 | function addOpenFullPage() { 112 | docEditListener(); 113 | } 114 | 115 | function removeOpenFullPage() { 116 | console.log('removing removeOpenFullPage feature...'); 117 | 118 | removeDocEditListener(); 119 | 120 | console.log('openFullPage feature removed'); 121 | } 122 | -------------------------------------------------------------------------------- /components/features/outline.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getElement, 3 | getElements, 4 | isObserverType, 5 | onElementLoaded, 6 | pageChangeListener, 7 | removeChildren, 8 | removePageChangeListener, 9 | toElement, 10 | } from "../utils"; 11 | 12 | 13 | let pageChangeObserverObj = {}; 14 | let docEditObserverObj = {}; 15 | 16 | // ENABLE/DISABLE Console Logs 17 | const DEBUG = false; 18 | 19 | // keep classes in hierarchy of DOM 20 | 21 | // stays on doc change 22 | const notionFrameCls = ".notion-frame"; 23 | const outlineFrameCls = ".nb-outline"; 24 | 25 | // these gets removed on doc change 26 | const notionScrollerCls = ".notion-frame .notion-scroller.vertical"; 27 | const notionPageContentCls = ".notion-page-content"; 28 | 29 | // starting point 30 | export function displayOutline(isShow: boolean) { 31 | console.log(`feature: displayOutline: ${isShow}`); 32 | 33 | if (isShow) { 34 | console.log("setting up outline feature"); 35 | 36 | // triggers on page load 37 | // it waits for doc to be loaded 38 | onElementLoaded(notionPageContentCls) 39 | .then((isPresent) => { 40 | if (isPresent) { 41 | // addOutlineFrame(); 42 | 43 | addOutlineToggleBtn(); 44 | addOutline(); 45 | docEditListener(); 46 | // add listener for page change or window reload 47 | // it detaches old listeners and adds new docEditListener and outline 48 | pageChangeObserverObj = pageChangeListener( 49 | [removeDocEditListener, hideOutline, removeOutlineToggleBtn], 50 | [addOutline, addOutlineToggleBtn, docEditListener] 51 | ); 52 | } 53 | return null; 54 | }) 55 | .catch((e) => console.log(e)); 56 | } else { 57 | removeOutline(); 58 | } 59 | } 60 | 61 | function addOutlineToggleBtn() { 62 | try { 63 | const outlineToggleBtn = "outlineToggleBtn"; 64 | 65 | const addOutlineToNotion = () => { 66 | const siblingCls = ".notion-topbar-share-menu"; 67 | console.log("add outline btn"); 68 | onElementLoaded(siblingCls) 69 | .then((isPresent) => { 70 | const pageContent = getElement(notionPageContentCls); 71 | if (!pageContent) { 72 | console.log("no page content class"); 73 | return; 74 | } 75 | 76 | // eslint-disable-next-line promise/always-return 77 | if (isPresent) { 78 | if (document.querySelector(`.${outlineToggleBtn}`)) { 79 | // btn already exist somehow 80 | return; 81 | } 82 | const sibling = document.querySelector(siblingCls); 83 | const btnEl = toElement( 84 | `
Outline
` 85 | ); 86 | 87 | btnEl.addEventListener("click", () => { 88 | console.log("yup"); 89 | const el = document.querySelector(".nb-outline"); 90 | if (el) { 91 | el.classList.toggle("disableForPage"); 92 | el.classList.toggle("show"); 93 | } 94 | }); 95 | 96 | sibling?.parentNode?.insertBefore(btnEl, sibling); 97 | } 98 | }) 99 | .catch((e) => console.log(e)); 100 | }; 101 | 102 | const addOutlineToNotionSubdomain = () => { 103 | const siblingCls = ".notion-topbar > div > div.notion-focusable"; 104 | console.log("add outline btn"); 105 | onElementLoaded(siblingCls) 106 | .then((isPresent) => { 107 | const pageContent = getElement(notionPageContentCls); 108 | if (!pageContent) { 109 | console.log("no page content class"); 110 | return; 111 | } 112 | 113 | // eslint-disable-next-line promise/always-return 114 | if (isPresent) { 115 | if (document.querySelector(`.${outlineToggleBtn}`)) { 116 | // btn already exist somehow 117 | return; 118 | } 119 | const sibling = document.querySelectorAll(siblingCls)[2]; 120 | const btnEl = toElement( 121 | `
Outline
` 122 | ); 123 | 124 | btnEl.addEventListener("click", () => { 125 | const el = document.querySelector(".nb-outline"); 126 | if (el) { 127 | el.classList.toggle("disableForPage"); 128 | el.classList.toggle("show"); 129 | } 130 | }); 131 | 132 | sibling?.parentNode?.insertBefore(btnEl, sibling); 133 | } 134 | }) 135 | .catch((e) => console.log(e)); 136 | }; 137 | 138 | addOutlineToNotion(); 139 | addOutlineToNotionSubdomain(); 140 | } catch (e:any) { 141 | console.log("Error: ", e.message); 142 | } 143 | } 144 | 145 | function removeDocEditListener() { 146 | if (isObserverType(docEditObserverObj)) { 147 | console.log("disconnected docEditObserver"); 148 | (docEditObserverObj as MutationObserver).disconnect(); 149 | } 150 | } 151 | 152 | function removeOutline() { 153 | console.log("removing outline feature..."); 154 | 155 | removeDocEditListener(); 156 | 157 | removePageChangeListener(pageChangeObserverObj); 158 | 159 | clearOutline(); 160 | 161 | removeOutlineToggleBtn(); 162 | 163 | console.log("removed outline feature"); 164 | } 165 | 166 | function removeOutlineToggleBtn() { 167 | const btn = document.querySelector(".outlineToggleBtn"); 168 | 169 | if (btn) { 170 | btn.remove(); 171 | } 172 | } 173 | function hideOutline() { 174 | const outline = getElement(outlineFrameCls); 175 | 176 | if (!outline) return; 177 | outline.classList.remove("show"); 178 | } 179 | 180 | function clearOutline() { 181 | hideOutline(); 182 | 183 | const outline = getElement(outlineFrameCls); 184 | if (outline) { 185 | console.log("removed outline div"); 186 | outline.remove(); 187 | } 188 | } 189 | 190 | function addOutline() { 191 | DEBUG && console.log("adding/updating OUTLINE"); 192 | 193 | try { 194 | const pageContent = getElement(notionPageContentCls); 195 | if (!pageContent) { 196 | console.log("no page content class"); 197 | return; 198 | } 199 | 200 | const fullPageTable = getElement(".notion-peek-renderer"); 201 | if (fullPageTable && fullPageTable.querySelector(notionPageContentCls)) { 202 | console.log("don't show outline for full page tables"); 203 | return; 204 | } 205 | 206 | const notionScrollerEl = getElement(notionScrollerCls); 207 | 208 | // check if it outline exist already 209 | let outlineEl = getElement(outlineFrameCls); 210 | 211 | if (!outlineEl || outlineEl.length === 0) { 212 | // do not add any space between closing and ending of ` 213 | outlineEl = toElement(` 214 |
215 |
216 |
217 |

Outline

218 |
219 |
220 |
221 |
222 |
`); 223 | 224 | // add toc container 225 | notionScrollerEl.parentNode.insertBefore(outlineEl, notionScrollerEl); 226 | 227 | document.getElementById("nbScrollToTop")?.addEventListener("click", () => { 228 | const doc = document.querySelector(".notion-frame > .notion-scroller"); 229 | if (doc) { 230 | doc.scroll({ top: 0, left: 0 }); 231 | } 232 | }); 233 | } 234 | 235 | const blockWrapperEl = outlineEl.querySelector(".block-wrapper"); 236 | 237 | // empty any previous headings 238 | removeChildren(blockWrapperEl); 239 | 240 | const tocBlockHTML = ``; 253 | 254 | let block = ""; 255 | 256 | let isHeadingsFound = false; 257 | 258 | // select all divs containing headings 259 | const pageHeadings = getElements( 260 | `${notionPageContentCls} [class$="header-block"]` 261 | ); 262 | 263 | isHeadingsFound = pageHeadings.length > 0; 264 | 265 | const contains = { 266 | h1: false, 267 | h2: false, 268 | h3: false, 269 | }; 270 | // add headings to outline view 271 | for (let i = 0; i < pageHeadings.length; i++) { 272 | let headingCls = ""; 273 | const pageHeading = pageHeadings[i]; 274 | if (pageHeading.classList.contains("notion-header-block")) { 275 | headingCls = "nb-h1"; 276 | contains.h1 = true; 277 | } else if (pageHeading.classList.contains("notion-sub_header-block")) { 278 | headingCls = "nb-h2"; 279 | contains.h2 = true; 280 | } else if ( 281 | pageHeading.classList.contains("notion-sub_sub_header-block") 282 | ) { 283 | headingCls = "nb-h3"; 284 | contains.h3 = true; 285 | } else { 286 | headingCls = ""; 287 | } 288 | // @ts-ignore 289 | block = toElement(tocBlockHTML); 290 | // add text 291 | let text = ""; 292 | pageHeading.querySelector("div").childNodes.forEach((hxEl: { nodeName: string; childNodes: any[]; textContent: string; alt: string; }) => { 293 | // heading is inside span 294 | if (["SPAN", "DIV"].includes(hxEl.nodeName)) { 295 | hxEl.childNodes.forEach((el) => { 296 | if ( 297 | ["#text", "H1", "H2", "H3", "H4", "H5", "H6"].includes( 298 | el.nodeName 299 | ) 300 | ) { 301 | // it's regualar text 302 | text += el.textContent; 303 | } else if (hxEl.nodeName === "A") { 304 | // it's link inside heading 305 | text += hxEl.textContent; 306 | } else if (el.nodeName === "IMG") { 307 | // emojis inside heading 308 | text += el.alt; 309 | } 310 | // handle toggle heading type 311 | else if (el.nodeName === "DIV") { 312 | const heading = el.querySelector("H1,H2,H3,H4,H5,H6"); 313 | if (heading) { 314 | text += heading.textContent; 315 | } 316 | } 317 | }); 318 | } else if ( 319 | ["#text", "H1", "H2", "H3", "H4", "H5", "H6"].includes(hxEl.nodeName) 320 | ) { 321 | // it's regualar text 322 | text += hxEl.textContent; 323 | } else if (hxEl.nodeName === "A") { 324 | // it's link inside heading 325 | text += hxEl.textContent; 326 | } else if (hxEl.nodeName === "IMG") { 327 | // emojis inside heading 328 | text += hxEl.alt; 329 | } 330 | }); 331 | // @ts-ignore 332 | block.querySelector(".align").classList.add(headingCls); 333 | // @ts-ignore 334 | block.querySelector(".text").textContent = text; 335 | // if (text.length > 20) { 336 | // block.querySelector(".btn").title = text; 337 | // } 338 | 339 | // add href 340 | const blockId = pageHeading 341 | .getAttribute("data-block-id") 342 | .replace(/-/g, ""); 343 | // @ts-ignore 344 | block.setAttribute("hash", blockId); 345 | // evaluate href at runtime cuz notion url is not consistent 346 | // @ts-ignore 347 | block.addEventListener("click", (e) => { 348 | e.currentTarget.querySelector("a").href = `${ 349 | window.location.pathname 350 | }#${e.currentTarget.getAttribute("hash")}`; 351 | }); 352 | 353 | blockWrapperEl.appendChild(block); 354 | } 355 | 356 | // hide outline if there is no heading 357 | if (!isHeadingsFound) { 358 | console.log("no heading found so removing outline frame"); 359 | hideOutline(); 360 | } else { 361 | // if there's no h1 then reduce space from left 362 | if (!contains.h1) { 363 | if (contains.h2 && contains.h3) { 364 | // convert h2->h1 and h3->h2 365 | blockWrapperEl.querySelectorAll(".nb-h2").forEach((x:any) => { 366 | x.classList.add("nb-h1"); 367 | x.classList.remove("nb-h2"); 368 | }); 369 | blockWrapperEl.querySelectorAll(".nb-h3").forEach((x:any) => { 370 | x.classList.add("nb-h2"); 371 | x.classList.remove("nb-h3"); 372 | }); 373 | } else { 374 | // all are of same type i.e. h2 or h3 so convert to h1 375 | blockWrapperEl.querySelectorAll(".nb-h2").forEach((x:any) => { 376 | x.classList.add("nb-h1"); 377 | x.classList.remove("nb-h2"); 378 | }); 379 | blockWrapperEl.querySelectorAll(".nb-h3").forEach((x:any) => { 380 | x.classList.add("nb-h1"); 381 | x.classList.remove("nb-h3"); 382 | }); 383 | } 384 | } 385 | 386 | if (!outlineEl.classList.contains("disableForPage")) { 387 | outlineEl.classList.add("show"); 388 | } 389 | } 390 | } catch (e:any) { 391 | console.error("Error: ", e.message); 392 | } 393 | } 394 | 395 | // UTILITY FUNCTIONS 396 | 397 | // add/update outline if any heading change occurs 398 | function docEditListener() { 399 | DEBUG && console.log("listening for doc edit changes..."); 400 | 401 | docEditObserverObj = new MutationObserver((mutationList, obsrvr) => { 402 | DEBUG && console.log("found changes in doc content"); 403 | 404 | let isDocHeadingChanged = false; 405 | 406 | let placeholder = ""; 407 | for (let i = 0; i < mutationList.length; i++) { 408 | const m = mutationList[i]; 409 | 410 | // case: check for text change in headings 411 | if (!isHeading(placeholder) && m.type === "characterData") { 412 | DEBUG && console.log(`changed text: ${m.target.textContent}`); 413 | 414 | if (!isHeading(placeholder) && m.target.parentNode) { 415 | // @ts-ignore 416 | placeholder = m.target.parentNode.getAttribute("placeholder"); 417 | } 418 | 419 | // case: when styling (b/i) is added to heading 420 | if ( 421 | !isHeading(placeholder) && 422 | m.target.parentNode && 423 | m.target.parentNode.parentNode 424 | ) { 425 | placeholder = 426 | // @ts-ignore 427 | m.target.parentNode.parentNode.getAttribute("placeholder"); 428 | } 429 | } 430 | if (!isHeading(placeholder) && m.type === "childList") { 431 | // console.log("childList changed"); 432 | 433 | // case: hitting backspace in headings 434 | // @ts-ignore 435 | placeholder = m.target.getAttribute("placeholder"); 436 | 437 | // case: when empty heading is being removed 438 | if ( 439 | !isHeading(placeholder) && 440 | m.removedNodes.length > 0 && 441 | // @ts-ignore 442 | m.removedNodes[0].firstElementChild 443 | ) { 444 | placeholder = 445 | // @ts-ignore 446 | m.removedNodes[0].firstElementChild.getAttribute("placeholder"); 447 | 448 | if (placeholder) { 449 | // console.log("empty block got removed"); 450 | } 451 | 452 | // case: when select and delete multiple headings 453 | if ( 454 | !isHeading(placeholder) && 455 | m.removedNodes.length > 0 && 456 | // @ts-ignore 457 | m.removedNodes[0].firstElementChild.firstElementChild 458 | ) { 459 | placeholder = 460 | // @ts-ignore 461 | m.removedNodes[0].firstElementChild.firstElementChild.getAttribute( 462 | "placeholder" 463 | ); 464 | 465 | // console.log("empty blocks got removed"); 466 | } 467 | } 468 | 469 | // case: when empty heading is being added 470 | if ( 471 | !isHeading(placeholder) && 472 | m.addedNodes.length > 0 && 473 | // @ts-ignore 474 | m.addedNodes[0].firstElementChild 475 | ) { 476 | placeholder = 477 | // @ts-ignore 478 | m.addedNodes[0].firstElementChild.getAttribute("placeholder"); 479 | // console.log("empty block got added"); 480 | } 481 | } 482 | 483 | // check if the change was related to headings 484 | if (isHeading(placeholder)) { 485 | DEBUG && console.log("heading changed"); 486 | 487 | isDocHeadingChanged = true; 488 | break; 489 | } 490 | } 491 | 492 | if (isDocHeadingChanged) { 493 | addOutline(); 494 | } 495 | }); 496 | 497 | // now add listener for doc text change 498 | const pageContentEl = getElement(notionPageContentCls); 499 | 500 | if (pageContentEl && docEditObserverObj) { 501 | (docEditObserverObj as MutationObserver).observe(pageContentEl, { 502 | childList: true, 503 | characterData: true, 504 | subtree: true, 505 | }); 506 | } 507 | } 508 | 509 | function isHeading(placeholder:string) { 510 | // check if the change was related to headings 511 | 512 | if (!placeholder || !placeholder.trim()) { 513 | return true; 514 | } 515 | const headings = ["Heading", "제목"]; 516 | headings.forEach((x) => { 517 | if (placeholder.includes(x)) { 518 | return true; 519 | } 520 | }); 521 | 522 | return false; 523 | } 524 | -------------------------------------------------------------------------------- /components/features/pageElements.ts: -------------------------------------------------------------------------------- 1 | import { getElement, isObserverType, onElementLoaded, simulateKey } from '../utils'; 2 | 3 | const notionAiBtnCls = '[role=button].notion-ai-button'; 4 | const notionAppId = '#notion-app'; 5 | 6 | // To add theme based color: check indentationLines sass class 7 | 8 | const notionCursorListenerCls = '.notion-cursor-listener'; 9 | let titleObserver = {}; 10 | export function hideComments(isEnabled: boolean) { 11 | try { 12 | console.log(`feature: hideComments: ${isEnabled}`); 13 | 14 | onElementLoaded(notionAppId) 15 | .then((isPresent) => { 16 | if (isPresent) { 17 | const el = getElement(notionAppId); 18 | if (isEnabled) { 19 | el.classList.add('hideComments-nb'); 20 | } else { 21 | el.classList.remove('hideComments-nb'); 22 | } 23 | } 24 | return null; 25 | }) 26 | .catch((e) => console.error(e)); 27 | } catch (e) { 28 | console.error(e); 29 | } 30 | } 31 | 32 | export function hideBacklinks(isEnabled: boolean) { 33 | try { 34 | console.log(`feature: hideBacklinks: ${isEnabled}`); 35 | 36 | onElementLoaded(notionAppId) 37 | .then((isPresent) => { 38 | if (isPresent) { 39 | const el = getElement(notionAppId); 40 | if (isEnabled) { 41 | el.classList.add('hideBacklinks'); 42 | } else { 43 | el.classList.remove('hideBacklinks'); 44 | } 45 | } 46 | return null; 47 | }) 48 | .catch((e) => console.error(e)); 49 | } catch (e) { 50 | console.error(e); 51 | } 52 | } 53 | 54 | export function disableSlashCommandPlaceholder(isEnabled: boolean) { 55 | try { 56 | console.log(`feature: disableSlashCommandPlaceholder: ${isEnabled}`); 57 | 58 | onElementLoaded(notionAppId) 59 | .then((isPresent) => { 60 | if (isPresent) { 61 | const el = getElement(notionAppId); 62 | if (isEnabled) { 63 | el.classList.add('disableSlashCommandPlaceholder'); 64 | } else { 65 | el.classList.remove('disableSlashCommandPlaceholder'); 66 | } 67 | } 68 | return null; 69 | }) 70 | .catch((e) => console.error(e)); 71 | } catch (e) { 72 | console.error(e); 73 | } 74 | } 75 | 76 | export function smallText(isEnabled: boolean) { 77 | try { 78 | console.log(`feature: smallText: ${isEnabled}`); 79 | 80 | onElementLoaded(notionAppId) 81 | .then((isPresent) => { 82 | if (isPresent) { 83 | const el = getElement(notionAppId); 84 | if (isEnabled) { 85 | el.classList.add('smallText'); 86 | } else { 87 | el.classList.remove('smallText'); 88 | } 89 | } 90 | return null; 91 | }) 92 | .catch((e) => console.error(e)); 93 | } catch (e) { 94 | console.error(e); 95 | } 96 | } 97 | 98 | export function fullWidth(isEnabled: boolean) { 99 | try { 100 | console.log(`feature: fullWidth: ${isEnabled}`); 101 | 102 | onElementLoaded(notionAppId) 103 | .then((isPresent) => { 104 | if (isPresent) { 105 | const el = getElement(notionAppId); 106 | if (isEnabled) { 107 | el.classList.add('fullWidth'); 108 | } else { 109 | el.classList.remove('fullWidth'); 110 | } 111 | } 112 | return null; 113 | }) 114 | .catch((e) => console.log(e)); 115 | } catch (e) { 116 | console.log(e); 117 | } 118 | } 119 | 120 | export function borderOnImages(isEnabled: boolean) { 121 | try { 122 | console.log(`feature: borderOnImages: ${isEnabled}`); 123 | 124 | onElementLoaded(notionAppId) 125 | .then((isPresent) => { 126 | if (isPresent) { 127 | const el = getElement(notionAppId); 128 | if (isEnabled) { 129 | el.classList.add('borderOnImages'); 130 | } else { 131 | el.classList.remove('borderOnImages'); 132 | } 133 | } 134 | }) 135 | .catch((e) => console.log(e)); 136 | } catch (e) { 137 | console.log(e); 138 | } 139 | } 140 | 141 | export function bolderTextInDark(isEnabled: boolean) { 142 | try { 143 | console.log(`feature: bolderTextInDark: ${isEnabled}`); 144 | 145 | onElementLoaded(notionAppId) 146 | .then((isPresent) => { 147 | if (isPresent) { 148 | const el = getElement(notionAppId); 149 | if (isEnabled) { 150 | el.classList.add('bolder'); 151 | } else { 152 | el.classList.remove('bolder'); 153 | } 154 | // console.log(`${notionAppInner} style is ${el.style.display}`); 155 | } 156 | return null; 157 | }) 158 | .catch((e) => console.log(e)); 159 | } catch (e) { 160 | console.log(e); 161 | } 162 | } 163 | 164 | export function hideAiBtn(isHidden: boolean) { 165 | try { 166 | console.debug(`feature: hideAiBtn: ${isHidden}`); 167 | 168 | onElementLoaded(notionAiBtnCls) 169 | .then((isPresent) => { 170 | if (isPresent) { 171 | const el = getElement(notionAiBtnCls); 172 | if (isHidden) { 173 | el.style.display = 'none'; 174 | } else { 175 | el.style.display = 'flex'; 176 | } 177 | console.log(`${notionAiBtnCls} style is ${el.style.display}`); 178 | } 179 | return null; 180 | }) 181 | .catch((e) => console.log(e)); 182 | } catch (e) { 183 | console.log(e); 184 | } 185 | } 186 | 187 | export function hideSlashMenuAfterSpace(isEnabled: boolean) { 188 | try { 189 | console.log(`feature: hideSlashMenuAfterSpace: ${isEnabled}`); 190 | 191 | onElementLoaded(notionAppId) 192 | .then((isPresent) => { 193 | if (isPresent) { 194 | if (isEnabled) { 195 | getElement(notionAppId).addEventListener('keydown', hideSlashMenuAfterSpaceEvent); 196 | } else { 197 | getElement(notionAppId).removeEventListener('keydown', hideSlashMenuAfterSpaceEvent); 198 | } 199 | } 200 | return null; 201 | }) 202 | .catch((e) => console.log(e)); 203 | } catch (e) { 204 | console.log(e); 205 | } 206 | } 207 | 208 | export function disableSlashMenu(isEnabled: boolean) { 209 | try { 210 | console.log(`feature: disableSlashMenu: ${isEnabled}`); 211 | 212 | onElementLoaded(notionAppId) 213 | .then((isPresent) => { 214 | if (isPresent) { 215 | if (isEnabled) { 216 | // this preceeds 'hideSlashMenuAfterSpaceEvent' so remove that first 217 | getElement(notionAppId).removeEventListener('keydown', hideSlashMenuAfterSpaceEvent); 218 | 219 | window.addEventListener('keyup', disableSlashMenuEvent, {}); 220 | } else { 221 | window.removeEventListener('keyup', disableSlashMenuEvent, {}); 222 | } 223 | } 224 | return null; 225 | }) 226 | .catch((e) => console.log(e)); 227 | } catch (e) { 228 | console.log(e); 229 | } 230 | } 231 | 232 | export function disableAiAfterSpaceKey(isEnabled: boolean) { 233 | try { 234 | console.log(`feature: disableAiAfterSpaceKey: ${isEnabled}`); 235 | 236 | onElementLoaded(notionAppId) 237 | .then((isPresent) => { 238 | if (isPresent) { 239 | if (isEnabled) { 240 | // simulate esc key to prevent menu from appearing 241 | window.addEventListener('keydown', disableAiAfterSpaceKeyHandler); 242 | } else { 243 | window.removeEventListener('keydown', disableAiAfterSpaceKeyHandler); 244 | } 245 | } 246 | return null; 247 | }) 248 | .catch((e) => console.log(e)); 249 | } catch (e) { 250 | console.log(e); 251 | } 252 | } 253 | 254 | export function hideNotification(isEnabled: boolean) { 255 | try { 256 | console.log(`feature: hideNotification: ${isEnabled}`); 257 | 258 | const removeBadgeFromTitle = () => { 259 | if (document.title.indexOf(')') > -1) { 260 | document.title = document.title.substring(document.title.indexOf(')') + 2); 261 | } 262 | }; 263 | onElementLoaded(notionAppId) 264 | .then((isPresent) => { 265 | if (isPresent) { 266 | const el = getElement(notionAppId); 267 | if (isEnabled) { 268 | el.classList.add('hideNotification'); 269 | removeBadgeFromTitle(); 270 | // select the target node 271 | const target = document.querySelector('title') as Node; 272 | 273 | // create an observer instance 274 | titleObserver = new MutationObserver((mutations) => { 275 | removeBadgeFromTitle(); 276 | }); 277 | 278 | // configuration of the observer: 279 | const config = { 280 | subtree: true, 281 | characterData: true, 282 | childList: true, 283 | }; 284 | 285 | // pass in the target node, as well as the observer options 286 | (titleObserver as MutationObserver).observe(target, config); 287 | } else { 288 | el.classList.remove('hideNotification'); 289 | if (isObserverType(titleObserver)) { 290 | console.log('disconnected docEditObserver'); 291 | (titleObserver as MutationObserver).disconnect(); 292 | } 293 | } 294 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 295 | } 296 | return null; 297 | }) 298 | .catch((e) => console.log(e)); 299 | } catch (e) { 300 | console.log(e); 301 | } 302 | } 303 | 304 | export function leftAlignMedia(isEnabled: boolean) { 305 | try { 306 | console.log(`feature: leftAlignMedia: ${isEnabled}`); 307 | 308 | onElementLoaded(notionAppId) 309 | .then((isPresent) => { 310 | if (isPresent) { 311 | const el = getElement(notionAppId); 312 | if (isEnabled) { 313 | el.classList.add('leftAlignMedia'); 314 | } else { 315 | el.classList.remove('leftAlignMedia'); 316 | } 317 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 318 | } 319 | return null; 320 | }) 321 | .catch((e) => console.log(e)); 322 | } catch (e) { 323 | console.log(e); 324 | } 325 | } 326 | 327 | export function addMoreHeightToPage(isEnabled: boolean) { 328 | try { 329 | console.log(`feature: addMoreHeightToPage: ${isEnabled}`); 330 | 331 | onElementLoaded(notionAppId) 332 | .then((isPresent) => { 333 | if (isPresent) { 334 | const el = getElement(notionAppId); 335 | if (isEnabled) { 336 | el.classList.add('addMoreHeightToPage'); 337 | } else { 338 | el.classList.remove('addMoreHeightToPage'); 339 | } 340 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 341 | } 342 | return null; 343 | }) 344 | .catch((e) => console.log(e)); 345 | } catch (e) { 346 | console.log(e); 347 | } 348 | } 349 | 350 | export function narrowListItems(isEnabled: boolean) { 351 | try { 352 | console.log(`feature: narrowListItems: ${isEnabled}`); 353 | 354 | onElementLoaded(notionAppId) 355 | .then((isPresent) => { 356 | if (isPresent) { 357 | const el = getElement(notionAppId); 358 | if (isEnabled) { 359 | el.classList.add('narrowListItems'); 360 | } else { 361 | el.classList.remove('narrowListItems'); 362 | } 363 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 364 | } 365 | return null; 366 | }) 367 | .catch((e) => console.log(e)); 368 | } catch (e) { 369 | console.log(e); 370 | } 371 | } 372 | 373 | // export function enableSpellcheckForCode(isEnabled) { 374 | // try { 375 | // console.log(`feature: enableSpellcheckForCode: ${isEnabled}`); 376 | 377 | // onElementLoaded(notionAppInnerCls) 378 | // .then((isPresent) => { 379 | // if (isPresent) { 380 | // const codeDivs = document.querySelectorAll( 381 | // "div.notion-page-content > div.notion-selectable.notion-code-block div.notion-code-block > div" 382 | // ); 383 | 384 | // if (isEnabled) { 385 | // codeDivs.forEach((x) => { 386 | // x.setAttribute("spellcheck", "true"); 387 | // }); 388 | // } else { 389 | // codeDivs.forEach((x) => { 390 | // x.setAttribute("spellcheck", "false"); 391 | // }); 392 | // } 393 | // } 394 | // return null; 395 | // }) 396 | // .catch((e) => console.log(e)); 397 | // } catch (e) { 398 | // console.log(e); 399 | // } 400 | // } 401 | 402 | export function showHoverText(isEnabled: boolean) { 403 | try { 404 | console.log(`feature: showHoverText: ${isEnabled}`); 405 | 406 | // TODO: replace notionCursorListener with notionAppInner after fixing theme bug 407 | onElementLoaded(notionCursorListenerCls) 408 | .then((isPresent) => { 409 | if (isPresent) { 410 | const el = getElement(notionCursorListenerCls); 411 | if (isEnabled) { 412 | el.classList.add('showHoverText'); 413 | } else { 414 | el.classList.remove('showHoverText'); 415 | } 416 | // console.log(`${notionCursorListener} style is ${el.style.display}`); 417 | } 418 | return null; 419 | }) 420 | .catch((e) => console.log(e)); 421 | } catch (e) { 422 | console.log(e); 423 | } 424 | } 425 | 426 | export function hideHiddenColumns(isHidden: boolean) { 427 | try { 428 | console.log(`feature: hideHiddenColumns: ${isHidden}`); 429 | 430 | onElementLoaded(notionAppId) 431 | .then((isPresent) => { 432 | if (isPresent) { 433 | const el = getElement(notionAppId); 434 | if (isHidden) { 435 | el.classList.add('hideHiddenColumns'); 436 | } else { 437 | el.classList.remove('hideHiddenColumns'); 438 | } 439 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 440 | } 441 | return null; 442 | }) 443 | .catch((e) => console.log(e)); 444 | } catch (e) { 445 | console.log(e); 446 | } 447 | } 448 | 449 | export function disablePopupOnURLPaste(isEnabled: boolean) { 450 | try { 451 | console.log(`feature: disablePopupOnURLPaste: ${isEnabled}`); 452 | 453 | onElementLoaded(notionAppId) 454 | .then((isPresent) => { 455 | if (isPresent) { 456 | if (isEnabled) { 457 | getElement(notionAppId).addEventListener('paste', disablePopupOnURLPasteEvent); 458 | } else { 459 | getElement(notionAppId).removeEventListener('paste', disablePopupOnURLPasteEvent); 460 | } 461 | } 462 | return null; 463 | }) 464 | .catch((e) => console.log(e)); 465 | } catch (e) { 466 | console.log(e); 467 | } 468 | } 469 | 470 | export function indentationLines(isHidden: boolean) { 471 | try { 472 | console.log(`feature: indentationLines: ${isHidden}`); 473 | 474 | onElementLoaded(notionAppId) 475 | .then((isPresent) => { 476 | if (isPresent) { 477 | const el = getElement(notionAppId); 478 | if (isHidden) { 479 | el.classList.add('indentationLines'); 480 | } else { 481 | el.classList.remove('indentationLines'); 482 | } 483 | // console.log(`${notionBodyCls} style is ${el.style.display}`); 484 | } 485 | return null; 486 | }) 487 | .catch((e) => console.log(e)); 488 | } catch (e) { 489 | console.log(e); 490 | } 491 | } 492 | 493 | // #region ## ----------------- internal methods ----------------- ## 494 | 495 | function disablePopupOnURLPasteEvent(e: any) { 496 | const content = e.clipboardData.getData('text/plain'); 497 | 498 | // hide popup for external urls matching "xx.yy " 499 | if ( 500 | (!content.includes(' ') || content.slice(-1) === ' ') && 501 | !content.includes('notion.so') && 502 | content.includes('.') 503 | ) { 504 | console.log('inside disablePopupOnURLPasteEvent'); 505 | const dismissBtn = 506 | '#notion-app .notion-overlay-container.notion-default-overlay-container .notion-embed-menu .notion-scroller.vertical > div > div > div:nth-child(1)'; 507 | onElementLoaded(dismissBtn) 508 | .then((ex) => { 509 | simulateKey('esc'); 510 | }) 511 | .catch((ex) => { 512 | console.log(ex); 513 | }); 514 | } 515 | } 516 | 517 | function isSlashMenuVisible() { 518 | // this selector covers both scenario of slash menu when it appears in main doc or inside popup doc 519 | const slashMenuCls = 520 | '#notion-app > div > div.notion-overlay-container.notion-default-overlay-container > div > div > div > div:nth-child(2) > div > div > div > div > div.notion-scroller.vertical'; 521 | const isVisible = getElement(slashMenuCls) !== null; 522 | return isVisible; 523 | } 524 | 525 | function hideSlashMenuAfterSpaceEvent(e: any) { 526 | try { 527 | const spaceKey = ' '; 528 | // console.log(e); 529 | if (e.key === spaceKey) { 530 | if (e.target.textContent.includes('/')) { 531 | if (isSlashMenuVisible()) { 532 | // hide slash menu by simulating ESC key 533 | simulateKey('esc'); 534 | console.info('slash menu hid'); 535 | } 536 | } 537 | } 538 | } catch (x) { 539 | console.error(e); 540 | } 541 | } 542 | 543 | function disableAiAfterSpaceKeyHandler(e: any) { 544 | if (e.code === 'Space') { 545 | e.preventDefault(); 546 | document.execCommand('insertText', false, ' '); 547 | } 548 | } 549 | 550 | function disableSlashMenuEvent(e: any) { 551 | const slashKey = '/'; 552 | 553 | const insideTable = e?.path?.some((x: any) => { 554 | if ( 555 | e?.target?.classList?.contains('notranslate') && // select only cells and not preview window 556 | x?.classList?.contains('notion-default-overlay-container') && 557 | x?.classList?.contains('notion-overlay-container') 558 | ) { 559 | return true; 560 | } 561 | return false; 562 | }); 563 | 564 | // don't simulate esc when using slash key inside table cell becuz it'll exit the table 565 | // If the slash key is pressed, without the ctrl/cmd key (would be intent to modify selected block) 566 | // https://notion.notion.site/Learn-the-shortcuts-66e28cec810548c3a4061513126766b0#5c679ece35ee4e81b1217333a4cf35b3 567 | if (e.code === 'Slash' && !insideTable && !(e.ctrlKey || e.metaKey)) { 568 | // hide popup menu as soon as it's added to DOM 569 | onElementLoaded( 570 | 'div.notion-scroller.vertical', 571 | // @ts-ignore 572 | 'div.notion-overlay-container.notion-default-overlay-container', 573 | ) 574 | .then(() => { 575 | console.log('popup found'); 576 | simulateKey('esc'); 577 | console.log('hid menu'); 578 | }) 579 | .catch((e2) => { 580 | console.error(e2); 581 | }); 582 | } 583 | } 584 | 585 | export function hideTableOfContents(isEnabled: boolean) { 586 | try { 587 | console.debug(`feature: hideTableOfContents: ${isEnabled}`); 588 | 589 | onElementLoaded(notionAppId) 590 | .then((isPresent) => { 591 | if (isPresent) { 592 | const el = getElement(notionAppId); 593 | if (isEnabled) { 594 | el.classList.add('hideTableOfContents'); 595 | } else { 596 | el.classList.remove('hideTableOfContents'); 597 | } 598 | } 599 | return null; 600 | }) 601 | .catch((e) => console.error(e)); 602 | } catch (e) { 603 | console.error(e); 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /components/features/rollupUrlClickable.ts: -------------------------------------------------------------------------------- 1 | import { isObserverType, onElementLoaded, pageChangeListener, removePageChangeListener, toElement } from '../utils'; 2 | 3 | const notionAppInnerCls = '.notion-app-inner'; 4 | 5 | // this works for both full page table and full page content 6 | const notionPresenceContainerCls = '.notion-presence-container'; 7 | 8 | const DEBUG = true; 9 | 10 | const docEditObserverObj = {}; 11 | let pageChangeObserverObj = {}; 12 | 13 | export function rollupUrlClickable(isEnabled: boolean) { 14 | try { 15 | console.log(`feature: enablerollupUrlClickable: ${isEnabled}`); 16 | 17 | // triggers on page load 18 | // it waits for doc to be loaded 19 | onElementLoaded(notionPresenceContainerCls) 20 | .then((isPresent) => { 21 | if (isPresent) { 22 | if (isEnabled) { 23 | console.log('calling addrollupUrlClickable'); 24 | bindRollupClickableEvent(); 25 | // docEditListener(); 26 | } else { 27 | removerollupUrlClickable(); 28 | } 29 | } 30 | return true; 31 | }) 32 | .catch((e) => console.error(e)); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | } 37 | 38 | // Internal methods // 39 | 40 | function removeDocEditListener() { 41 | if (isObserverType(docEditObserverObj)) { 42 | DEBUG && console.log('disconnected docEditObserver'); 43 | (docEditObserverObj as MutationObserver).disconnect(); 44 | } 45 | } 46 | 47 | function bindRollupClickableEvent() { 48 | // add event 49 | addrollupUrlClickable(); 50 | 51 | // re-add event on page change 52 | pageChangeObserverObj = pageChangeListener([], [addrollupUrlClickable]); 53 | } 54 | 55 | function getUrl(textParam: string) { 56 | if (!textParam) { 57 | return null; 58 | } 59 | let text = textParam.trim(); 60 | 61 | if (text.indexOf(' ') === -1 && text.indexOf('.') > 0 && text.length > 3) { 62 | // href needs http(s) in url 63 | if (!/^https?:\/\//i.test(text)) { 64 | text = `http://${text}`; 65 | } 66 | return text; 67 | } 68 | return null; 69 | } 70 | 71 | let linkComponentEl: any = null; 72 | let rollupCellEl: any = null; 73 | 74 | function createLinkComponent(url: string) { 75 | return toElement( 76 | `
77 | 99 |
100 | `, 101 | ); 102 | } 103 | 104 | function handleTableHover(e: any) { 105 | // console.log("hover over table", e.target); 106 | let nestedLevel = 0; 107 | const path = e.composedPath(); 108 | for (let i = 0; i < path.length; i++) { 109 | const x = path[i]; 110 | // console.log(x); 111 | nestedLevel++; 112 | // return if hovering over link component 113 | if (x.className === 'linkComponent') { 114 | return; 115 | } 116 | if (nestedLevel > 5) { 117 | // linkComponent not found 118 | break; 119 | } 120 | } 121 | if (linkComponentEl) { 122 | linkComponentEl.remove(); 123 | } 124 | if (rollupCellEl) { 125 | rollupCellEl.style.position = null; 126 | rollupCellEl = null; 127 | } 128 | // onhover: background: rgb(239, 239, 238) 129 | 130 | let urlSpan = null; 131 | const rollupCellStyle = 'display: flex;'; 132 | 133 | for (const x of path) { 134 | if (x.getAttribute('style') === rollupCellStyle) { 135 | // console.log(x.getAttribute("style")); 136 | urlSpan = x.querySelector( 137 | 'div[style*="display: block;"] div[style*="display: flex; flex-wrap: nowrap;"] span', 138 | ); 139 | if (urlSpan) { 140 | // console.log("it's rollup cell"); 141 | rollupCellEl = x; 142 | break; 143 | } 144 | } 145 | 146 | // break if it reaches original eventListener element 147 | if (x === e.currentTarget) { 148 | break; 149 | } 150 | } 151 | 152 | if (rollupCellEl) { 153 | // debugger; 154 | // console.log("inside rollupcell"); 155 | let text = ''; 156 | // see if it contains actual link 157 | const anchor = urlSpan.querySelector('a'); 158 | if (anchor) { 159 | text = anchor.href; 160 | } else { 161 | // nope it's just a span 162 | text = urlSpan.textContent; 163 | } 164 | const url = getUrl(text); 165 | if (url) { 166 | rollupCellEl.style.position = 'relative'; 167 | 168 | linkComponentEl = createLinkComponent(url); 169 | 170 | rollupCellEl.appendChild(linkComponentEl); 171 | 172 | // console.log(url); 173 | } else { 174 | // console.log("empty text"); 175 | } 176 | } 177 | } 178 | 179 | function handletableRowAsPageHover(e: any) { 180 | // console.log("relatedTarget", e.relatedTarget); 181 | 182 | let nestedLevel = 0; 183 | const path = e.composedPath(); 184 | 185 | for (const x of path) { 186 | nestedLevel++; 187 | // return if hovering over link component 188 | if (x.className === 'linkComponent') { 189 | // console.log("ignoring link hover"); 190 | return; 191 | } 192 | if (nestedLevel > 5) { 193 | // linkComponent not found 194 | break; 195 | } 196 | } 197 | if (linkComponentEl) { 198 | linkComponentEl.remove(); 199 | } 200 | if (rollupCellEl) { 201 | rollupCellEl.style.position = null; 202 | rollupCellEl = null; 203 | } 204 | // onhover: background: rgb(239, 239, 238) 205 | 206 | let urlSpan = null; 207 | const rollupCellStyle = 'display: flex; flex: 1 1 0%; min-width: 0px;'; 208 | 209 | for (let i = 0; i < path.length; i++) { 210 | const x = path[i]; 211 | if (x.getAttribute('style') === rollupCellStyle) { 212 | urlSpan = x.querySelector( 213 | 'div.notion-focusable div[style*="display: flex; flex-wrap:"] > span[style*="word-break: break-word;"]', 214 | ); 215 | console.log(urlSpan); 216 | if (urlSpan) { 217 | rollupCellEl = x; 218 | break; 219 | } 220 | } 221 | 222 | // break if it reaches original eventListener element 223 | if (x === e.currentTarget) { 224 | break; 225 | } 226 | } 227 | 228 | if (rollupCellEl) { 229 | // debugger; 230 | let text = ''; 231 | // see if it contains actual link 232 | const anchor = urlSpan.querySelector('a'); 233 | if (anchor) { 234 | text = anchor.href; 235 | } else { 236 | // nope it's just a span 237 | text = urlSpan.textContent; 238 | } 239 | const url = getUrl(text); 240 | if (url) { 241 | rollupCellEl.style.position = 'relative'; 242 | 243 | linkComponentEl = createLinkComponent(url); 244 | 245 | rollupCellEl.appendChild(linkComponentEl); 246 | } 247 | } 248 | } 249 | 250 | function addrollupUrlClickable() { 251 | console.log('adding addrollupUrlClickable feature...'); 252 | 253 | // console.log( 254 | // "notion-table-view count", 255 | // document.querySelectorAll(".notion-table-view").length 256 | // ); 257 | 258 | const tables = document.querySelectorAll('.notion-table-view'); 259 | if (tables) { 260 | tables.forEach((x) => { 261 | x.addEventListener('mouseover', handleTableHover); 262 | }); 263 | } 264 | 265 | const tableRowAsPage = document.querySelector( 266 | '.notion-scroller.vertical > div:nth-of-type(2)[style*="display: flex;"] div[style="margin: 0px;"]', 267 | ); 268 | if (tableRowAsPage) { 269 | tableRowAsPage.addEventListener('mouseover', handletableRowAsPageHover); 270 | } 271 | } 272 | 273 | function removerollupUrlClickable() { 274 | console.log('removing removerollupUrlClickable feature...'); 275 | 276 | removePageChangeListener(pageChangeObserverObj); 277 | 278 | // removeDocEditListener(); 279 | const tables = document.querySelectorAll('.notion-table-view'); 280 | if (tables) { 281 | tables.forEach((x) => { 282 | x.removeEventListener('mouseover', handleTableHover); 283 | }); 284 | } 285 | 286 | const tableRowAsPage = document.querySelector( 287 | '.notion-scroller.vertical > div:nth-of-type(2)[style*="width: 100%; display: flex; flex-direction: column;"] div[style="margin: 0px;"]', 288 | ); 289 | if (tableRowAsPage) { 290 | tableRowAsPage.removeEventListener('mouseover', handletableRowAsPageHover); 291 | } 292 | 293 | console.log('removerollupUrlClickable feature done'); 294 | } 295 | -------------------------------------------------------------------------------- /components/features/scrollToTopBtn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getElement, 3 | onElementLoaded, 4 | toElement, 5 | pageChangeListener, 6 | removePageChangeListener, 7 | } from '../utils'; 8 | 9 | const notionFrameCls = '.notion-frame'; 10 | let pageChangeObserverObj = {}; 11 | let ticking = false; 12 | let isBtnVisible = false; 13 | let scrollBtn: HTMLElement; 14 | let docElement: HTMLElement; 15 | 16 | export function scrollTopBtn(isEnabled: boolean) { 17 | try { 18 | console.log(`feature: scrollTopBtn: ${isEnabled}`); 19 | 20 | onElementLoaded(notionFrameCls) 21 | .then((isPresent) => { 22 | if (isPresent) { 23 | if (isEnabled) { 24 | addScrollTopBtn(); 25 | } else { 26 | removeScrollTopBtn(); 27 | } 28 | } 29 | return null; 30 | }) 31 | 32 | .catch((e) => console.log(e)); 33 | } catch (e) { 34 | console.log(e); 35 | } 36 | } 37 | 38 | // Internal methods // 39 | 40 | function bindScrollEventToDoc() { 41 | docElement = getElement('.notion-frame > .notion-scroller'); 42 | docElement.addEventListener('scroll', handleScrollEvent); 43 | adjustBtnVisibilty(); 44 | console.log('added bindScrollEventToDoc'); 45 | } 46 | function scrollToTop() { 47 | docElement.scroll({ 48 | top: 0, 49 | left: 0, 50 | // behavior: "auto", 51 | }); 52 | } 53 | function addScrollTopBtn() { 54 | // add btn div to HTML 55 | const btnHTML = `
`; 56 | getElement(notionFrameCls).after(toElement(btnHTML)); 57 | console.log('inserted scroll btn div'); 58 | scrollBtn = getElement('.scroll-top-btn'); 59 | // scrollTopBtn.classList.add(btnCls); 60 | // add onclick event 61 | scrollBtn.addEventListener('click', scrollToTop); 62 | adjustBtnVisibilty(); 63 | 64 | // add scroll event to doc 65 | bindScrollEventToDoc(); 66 | 67 | // re-add scroll event on page change 68 | pageChangeObserverObj = pageChangeListener([bindScrollEventToDoc], []); 69 | } 70 | 71 | function handleScrollEvent() { 72 | // console.log("scroll event fired"); 73 | 74 | if (!ticking) { 75 | // optimizing scroll event 76 | window.requestAnimationFrame(() => { 77 | // console.log("requestAnimationFrame fired"); 78 | adjustBtnVisibilty(); 79 | }); 80 | 81 | ticking = true; 82 | } 83 | } 84 | 85 | function adjustBtnVisibilty() { 86 | // show scroll btn when user scroll down 100% of window height 87 | const didUserScrollDown = docElement.scrollTop >= docElement.clientHeight; 88 | 89 | if (didUserScrollDown) { 90 | // Show button if it's not shown 91 | if (!isBtnVisible) { 92 | scrollBtn.classList.add('show'); 93 | isBtnVisible = true; 94 | console.log('btn shown'); 95 | } 96 | } else if (isBtnVisible) { 97 | // Hide button if it's not hidden 98 | scrollBtn.classList.remove('show'); 99 | isBtnVisible = false; 100 | console.log('btn hidden'); 101 | } 102 | ticking = false; 103 | } 104 | function removeScrollTopBtn() { 105 | removePageChangeListener(pageChangeObserverObj); 106 | 107 | isBtnVisible = false; 108 | if (scrollBtn) { 109 | scrollBtn.remove(); 110 | console.log('scrollTopBtn removed'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /components/features/spellCheckForCode.ts: -------------------------------------------------------------------------------- 1 | import { getElement, onElementLoaded, isObserverType } from '../utils'; 2 | 3 | const notionAppInnerCls = '.notion-app-inner'; 4 | const notionPageContentCls = '.notion-page-content'; 5 | 6 | const DEBUG = false; 7 | 8 | let docEditObserverObj = {}; 9 | 10 | export function spellcheckForCode(isEnabled: boolean) { 11 | try { 12 | console.log(`feature: enableSpellcheckForCode: ${isEnabled}`); 13 | 14 | // triggers on page load 15 | // it waits for doc to be loaded 16 | onElementLoaded(notionPageContentCls) 17 | .then((isPresent) => { 18 | if (isPresent) { 19 | if (isEnabled) { 20 | addSpellCheckForCode(); 21 | docEditListener(); 22 | } else { 23 | removeSpellCheckForCode(); 24 | } 25 | } 26 | return true; 27 | }) 28 | .catch((e) => console.log(e)); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | } 33 | 34 | // Internal methods // 35 | 36 | function removeDocEditListener() { 37 | if (isObserverType(docEditObserverObj)) { 38 | DEBUG && console.log('disconnected docEditObserver'); 39 | (docEditObserverObj as MutationObserver).disconnect(); 40 | } 41 | } 42 | 43 | // works for page change or window reload 44 | function docEditListener() { 45 | console.log('listening for doc edit changes: enableSpellcheckForCode...'); 46 | let isSpellcheckEnabled = false; 47 | 48 | // @ts-ignore 49 | docEditObserverObj = new MutationObserver((mutationList, obsrvr) => { 50 | DEBUG && console.log('found changes in doc content'); 51 | 52 | for (let i = 0; i < mutationList.length; i++) { 53 | const m = mutationList[i]; 54 | 55 | // case: check for div change 56 | if (m.type === 'childList') { 57 | if ( 58 | m.target && 59 | m.addedNodes.length > 0 && 60 | // @ts-ignore 61 | m.target.classList.contains('notion-code-block') && 62 | // @ts-ignore 63 | m.target.querySelector('div[spellcheck]') 64 | ) { 65 | console.log('codeblok'); 66 | isSpellcheckEnabled = true; 67 | // briefly pause listener to avoid recursive triggers 68 | // removeDocEditListener(); 69 | 70 | m.target 71 | // @ts-ignore 72 | .querySelector('div[spellcheck]') 73 | .setAttribute('spellcheck', 'true'); 74 | // updateCodeline(block); 75 | // docEditListener(); 76 | } 77 | } 78 | } 79 | 80 | if (isSpellcheckEnabled) { 81 | console.log('spellcheck enabled'); 82 | isSpellcheckEnabled = false; 83 | } 84 | }); 85 | 86 | // now add listener for doc text change 87 | const pageContentEl = getElement(notionAppInnerCls); 88 | 89 | (docEditObserverObj as MutationObserver).observe(pageContentEl, { 90 | childList: true, 91 | subtree: true, 92 | }); 93 | } 94 | function addSpellCheckForCode() { 95 | const codeDivs = document.querySelectorAll( 96 | 'div.notion-page-content > div.notion-selectable.notion-code-block div.notion-code-block > div' 97 | ); 98 | codeDivs.forEach((x) => { 99 | x.setAttribute('spellcheck', 'true'); 100 | }); 101 | } 102 | 103 | function removeSpellCheckForCode() { 104 | console.log('removing removeSpellCheckForCode feature...'); 105 | 106 | removeDocEditListener(); 107 | 108 | const codeDivs = document.querySelectorAll( 109 | 'div.notion-page-content > div.notion-selectable.notion-code-block div.notion-code-block > div' 110 | ); 111 | 112 | codeDivs.forEach((x) => { 113 | x.setAttribute('spellcheck', 'false'); 114 | }); 115 | 116 | console.log('removeCodeLineNumbers feature done'); 117 | } 118 | -------------------------------------------------------------------------------- /components/settings.ts: -------------------------------------------------------------------------------- 1 | // settings and their default value 2 | export const defaultSettings = { 3 | displayOutline: true, 4 | hideAiBtn: false, 5 | hideTableOfContents: false, 6 | bolderTextInDark: false, 7 | smallText: false, 8 | fullWidth: false, 9 | hideComments: false, 10 | hideBacklinks: false, 11 | scrollTopBtn: false, 12 | hideSlashMenuAfterSpace: false, 13 | disableSlashMenu: false, 14 | leftAlignMedia: false, 15 | hideNotification: false, 16 | showHoverText: false, 17 | hideHiddenColumns: false, 18 | disablePopupOnURLPaste: false, 19 | addMoreHeightToPage: false, 20 | spellcheckForCode: false, 21 | codeLineNumbers: false, 22 | // step 1 of 2: add function name 23 | openFullPage: false, 24 | narrowListItems: false, 25 | indentationLines: false, 26 | rollupUrlClickable: false, 27 | borderOnImages: false, 28 | disableSlashCommandPlaceholder: false, 29 | disableAiAfterSpaceKey: false, 30 | }; 31 | 32 | export const settingDetails = [ 33 | { 34 | func: 'displayOutline', 35 | name: 'Show Outline', 36 | desc: 'Show sticky outline (table of contents) for pages that have headings', 37 | }, 38 | { 39 | func: 'hideTableOfContents', 40 | name: 'Hide Table of contents', 41 | desc: "Hide Notion's default 'Table of contents' feature", 42 | }, 43 | { 44 | func: 'hideAiBtn', 45 | name: 'Hide AI button from pages', 46 | desc: 'Hide floating AI button (bottom-right corner) from pages', 47 | }, 48 | { 49 | func: 'disableAiAfterSpaceKey', 50 | name: 'Disable AI menu when pressing space', 51 | desc: "Don't show AI command menu when pressing space key", 52 | disable_func: 'disableAiAfterSpaceKey', 53 | }, 54 | { 55 | func: 'fullWidth', 56 | name: 'Full width for all pages', 57 | desc: 'Set full width for all pages by default', 58 | }, 59 | { 60 | func: 'smallText', 61 | name: 'Small text for all pages', 62 | desc: 'Set small text for all pages by default', 63 | }, 64 | 65 | { 66 | func: 'leftAlignMedia', 67 | name: 'Left align media', 68 | desc: 'Align document images and videos to the left instead of center', 69 | }, 70 | 71 | { 72 | func: 'addMoreHeightToPage', 73 | name: 'Add more height to page', 74 | desc: 'Add more height to page by hiding top padding, image cover, & icon', 75 | pf: true, 76 | }, 77 | { 78 | func: 'openFullPage', 79 | name: 'Open full page instead of preview', 80 | desc: 'Bypass preview and open full page of table, board, etc', 81 | }, 82 | { 83 | func: 'rollupUrlClickable', 84 | name: 'Make Rollup URLs clickable', 85 | desc: 'Make URLs in Rollup property clickable', 86 | }, 87 | { 88 | func: 'scrollTopBtn', 89 | name: "'Scroll to top' button", 90 | desc: 'Add button at bottom-right corner for scrolling back to top', 91 | }, 92 | { 93 | func: 'hideSlashMenuAfterSpace', 94 | name: 'Close slash command menu after space', 95 | desc: "Close slash command popup menu '/' by pressing space key", 96 | disable_func: 'disableSlashMenu', 97 | }, 98 | { 99 | func: 'disableSlashMenu', 100 | name: "Don't show slash command menu when pressing '/'", 101 | desc: "Don't show slash command popup menu when pressing '/'", 102 | disable_func: 'hideSlashMenuAfterSpace', 103 | }, 104 | { 105 | func: 'disableSlashCommandPlaceholder', 106 | name: 'Hide slash command placeholder', 107 | desc: "Hide placeholder: Press '/' for commands…", 108 | }, 109 | { 110 | func: 'showHoverText', 111 | name: 'Show full text on hover', 112 | desc: 'Show full text in table cells on mouse hover', 113 | }, 114 | { 115 | func: 'codeLineNumbers', 116 | name: 'Show code line numbers', 117 | desc: 'Show line numbers for code blocks', 118 | }, 119 | { 120 | func: 'spellcheckForCode', 121 | name: 'Enable spellcheck inside code blocks', 122 | desc: 'Show squiggly red lines for any spelling mistakes inside code blocks', 123 | }, 124 | { 125 | func: 'disablePopupOnURLPaste', 126 | name: "Don't show popup menu when pasting external links", 127 | desc: "Don't show popup menu (i.e. dismiss, create bookmark, create embed) when pasting external URLs", 128 | }, 129 | 130 | { 131 | func: 'hideNotification', 132 | name: 'Hide notification icon', 133 | desc: "Hide red notification icon from sidebar when it's in closed state and hide notification number from tab title", 134 | }, 135 | { 136 | func: 'narrowListItems', 137 | name: 'Narrow spacing between items', 138 | desc: 'Fit more content on screen by reducing space between items i.e. headings, lists, etc.', 139 | }, 140 | { 141 | func: 'indentationLines', 142 | name: 'Add indentation lines to lists', 143 | desc: 'Add vertical indentation lines to bullet and to-do lists', 144 | }, 145 | { 146 | func: 'bolderTextInDark', 147 | name: 'Bolder text in dark mode', 148 | desc: 'Fix poorly recognizable bold text in dark mode', 149 | }, 150 | { 151 | func: 'hideHiddenColumns', 152 | name: "Hide 'Hidden columns' in board view", 153 | desc: "Truly hide 'Hidden columns' in Kanban board view", 154 | }, 155 | { 156 | func: 'hideComments', 157 | name: 'Hide comments section from all pages', 158 | desc: '', 159 | }, 160 | { 161 | func: 'hideBacklinks', 162 | name: 'Hide backlinks section from all pages', 163 | desc: '', 164 | }, 165 | { 166 | func: 'borderOnImages', 167 | name: 'Add frame to images', 168 | desc: 'Add frame around images to make them easily noticeable on page', 169 | }, 170 | 171 | // step 2 of 2: add function name and description 172 | ]; 173 | -------------------------------------------------------------------------------- /components/utils.ts: -------------------------------------------------------------------------------- 1 | import { defaultSettings } from './settings'; 2 | 3 | export const twitterShareTxt = 4 | 'https://twitter.com/intent/tweet?url=&text=%40NotionBoost%20by%20%40GorvGoyl%20adds%20tons%20of%20capabilities%20to%20%40NotionHQ%20like%20outline%2C%20scroll%20top%20button%2C%20hide%20slash%20command%20menu%20and%20many%20more!'; 5 | // stays on doc change 6 | const notionFrameCls = '.notion-frame'; 7 | const DEBUG = false; 8 | // these gets removed on doc change 9 | const notionScrollerCls = '.notion-frame .notion-scroller.vertical'; 10 | const notionPageContentCls = '.notion-page-content'; 11 | const notionPresenceContainerCls = '.notion-presence-container'; 12 | 13 | /** 14 | * add listener for page change or window reload event 15 | * 16 | * accepts array of functions 17 | * @param {*} callbacksAfterDocReady call any function after notion doc is ready 18 | * @param {*} callbacksAfterContentReady call any function after notion doc content is loaded (async) 19 | * @return {*} observer object which can be used to disconnect pageChangeListener 20 | */ 21 | export function pageChangeListener(callbacksAfterDocReady: any, callbacksAfterContentReady: any) { 22 | console.log(`listening to page change events`); 23 | 24 | const pageChangeObserverObj = new MutationObserver((mutationList, obsrvr) => { 25 | console.log('new page opened'); 26 | 27 | // check if scroller class is loaded 28 | if (getElement(notionScrollerCls)) { 29 | for (let i = 0; i < callbacksAfterDocReady.length; i++) { 30 | callbacksAfterDocReady[i](); 31 | } 32 | // now wait for notionPresenceContainerCls to be loaded 33 | onElementLoaded(notionPresenceContainerCls, notionScrollerCls as any) 34 | .then((isPresent) => { 35 | if (isPresent) { 36 | for (let i = 0; i < callbacksAfterContentReady.length; i++) { 37 | callbacksAfterContentReady[i](); 38 | } 39 | } 40 | return null; 41 | }) 42 | .catch((e) => console.log(e)); 43 | } 44 | }); 45 | 46 | const docFrameEl = getElement(notionFrameCls); 47 | 48 | pageChangeObserverObj.observe(docFrameEl, { 49 | childList: true, 50 | }); 51 | return pageChangeObserverObj; 52 | } 53 | 54 | export function removePageChangeListener(pageChangeObserverObj: any) { 55 | if (isObserverType(pageChangeObserverObj)) { 56 | console.log('disconnected pageChangeObserver'); 57 | pageChangeObserverObj.disconnect(); 58 | } 59 | } 60 | 61 | export function isObserverType(obj: any) { 62 | return obj.disconnect !== undefined; 63 | } 64 | 65 | /** 66 | * 67 | * Wait for an HTML element to be loaded like `div`, `span`, `img`, etc. 68 | * ex: `onElementLoaded("div.some_class").then(()=>{}).catch(()=>{})` 69 | * @param {*} elementToObserve wait for this element to load 70 | * @param {*} parentStaticElement (optional) if parent element is not passed then `document` is used 71 | * @return {*} Promise - return promise when `elementToObserve` is loaded 72 | */ 73 | export function onElementLoaded(elementToObserve: any, parentStaticElement = undefined) { 74 | DEBUG && console.log(`waiting for element: ${elementToObserve}`); 75 | const promise = new Promise((resolve, reject) => { 76 | try { 77 | if (getElement(elementToObserve)) { 78 | DEBUG && console.log(`element already present: ${elementToObserve}`); 79 | resolve(true); 80 | return; 81 | } 82 | const parentElement = parentStaticElement ? getElement(parentStaticElement) : document; 83 | 84 | const observer = new MutationObserver((mutationList, obsrvr) => { 85 | const divToCheck = getElement(elementToObserve); 86 | // console.log("checking for div..."); 87 | 88 | if (divToCheck) { 89 | console.log(`element loaded: ${elementToObserve}`); 90 | obsrvr.disconnect(); // stop observing 91 | resolve(true); 92 | } 93 | }); 94 | 95 | // start observing for dynamic div 96 | observer.observe(parentElement, { 97 | childList: true, 98 | subtree: true, 99 | }); 100 | } catch (e) { 101 | console.log(e); 102 | reject(Error('some issue... promise rejected')); 103 | } 104 | }); 105 | return promise; 106 | } 107 | 108 | // detect if css is changed for a div (once) 109 | export function onElementCSSChanged(divClassToObserve: any, timeOut: any) { 110 | DEBUG && console.log(`waiting for element: ${divClassToObserve}`); 111 | const promise = new Promise((resolve, reject) => { 112 | try { 113 | const observer = new MutationObserver((mutationList, obsrvr) => { 114 | mutationList.forEach((mutation) => { 115 | if (mutation.attributeName === 'style') { 116 | console.log('style change'); 117 | obsrvr.disconnect(); // stop observing 118 | resolve(true); 119 | } 120 | }); 121 | }); 122 | 123 | // start observing for dynamic div 124 | observer.observe(divClassToObserve, { 125 | attributes: true, 126 | attributeFilter: ['style'], 127 | }); 128 | 129 | if (timeOut) { 130 | setTimeout(() => { 131 | observer.disconnect(); 132 | console.log(`observer disconnected after ${timeOut}`); 133 | }, timeOut); 134 | } 135 | } catch (e) { 136 | console.log(e); 137 | reject(Error('some issue... promise rejected')); 138 | } 139 | }); 140 | return promise; 141 | } 142 | 143 | // create html node from string like $('div') 144 | export function toElement(s: any) { 145 | let e = document.createElement('div'); 146 | const r = document.createRange(); 147 | r.selectNodeContents(e); 148 | const f = r.createContextualFragment(s); 149 | e.appendChild(f); 150 | // @ts-ignore 151 | e = e.firstElementChild; 152 | return e; 153 | } 154 | // export function toElement( 155 | // s = "", 156 | // c, 157 | // t = document.createElement("template"), 158 | // l = "length" 159 | // ) { 160 | // t.innerHTML = s.trim(); 161 | // c = [...t.content.childNodes]; 162 | // return c[l] > 1 ? c : c[0] || ""; 163 | // } 164 | 165 | // run func every x millisec and stop after y millisec 166 | export function runMethod(func: any, runEvery: any, stopAfter: any) { 167 | const process = setInterval(func, runEvery); 168 | 169 | setTimeout(() => { 170 | console.log('stopped'); 171 | clearInterval(process); 172 | }, stopAfter); 173 | } 174 | 175 | export function removeChildren(el: any) { 176 | while (el.firstChild) el.removeChild(el.firstChild); 177 | } 178 | export function getElement(selector: any) { 179 | return document.querySelector(selector); 180 | } 181 | export function getElements(selector: any) { 182 | return document.querySelectorAll(selector); 183 | } 184 | // ugly method to check for empty 185 | export function isEmpty(obj: any) { 186 | // boolean like false = non-empty 187 | if (obj === false || obj === true) return false; 188 | 189 | // empty string, null is empty 190 | if (!obj) return true; 191 | 192 | // empty object = empty 193 | if (Object.keys(obj).length === 0) return true; 194 | return false; 195 | } 196 | 197 | // 1. get from default settings 2. update it with saved settings 3. return 198 | export function getLatestSettings() { 199 | const promise = new Promise((resolve, reject) => { 200 | try { 201 | const latestSet = { ...defaultSettings }; 202 | 203 | browser.storage.sync.get(['nb_settings']).then((result) => { 204 | const savedSet = result.nb_settings; 205 | if (!isEmpty(savedSet)) { 206 | for (const k of Object.keys(defaultSettings)) { 207 | if (!isEmpty(savedSet[k])) { 208 | (latestSet as any)[k] = savedSet[k]; 209 | } 210 | } 211 | } 212 | resolve(latestSet); 213 | }); 214 | } catch (e) { 215 | console.log(e); 216 | reject(Error('some issue... promise rejected')); 217 | } 218 | }); 219 | return promise; 220 | } 221 | 222 | export function simulateKey(key: any) { 223 | switch (key) { 224 | case 'esc': { 225 | window.dispatchEvent( 226 | new KeyboardEvent('keydown', { 227 | altKey: false, 228 | code: 'Escape', 229 | ctrlKey: false, 230 | isComposing: false, 231 | key: 'Escape', 232 | location: 0, 233 | metaKey: false, 234 | repeat: false, 235 | shiftKey: false, 236 | which: 27, 237 | charCode: 0, 238 | keyCode: 27, 239 | }) 240 | ); 241 | break; 242 | } 243 | default: 244 | console.error('key not implemented', key); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /entrypoints/content.ts: -------------------------------------------------------------------------------- 1 | import '../styles/content.scss'; 2 | import { codeLineNumbers } from '../components/features/codeLineNumbers'; 3 | import { openFullPage } from '../components/features/openFullPage'; 4 | import { displayOutline } from '../components/features/outline'; 5 | import * as features from '../components/features/pageElements'; 6 | import { rollupUrlClickable } from '../components/features/rollupUrlClickable'; 7 | import { scrollTopBtn } from '../components/features/scrollToTopBtn'; 8 | // step 1 of 2: import feature 9 | import { spellcheckForCode } from '../components/features/spellCheckForCode'; 10 | import { defaultSettings } from '../components/settings'; 11 | import { getLatestSettings, isEmpty } from '../components/utils'; 12 | import { hideAiBtn, hideTableOfContents } from '../components/features/pageElements'; 13 | 14 | let featureList: any = {}; 15 | 16 | featureList = { ...features }; 17 | 18 | // step 2 of 2: add that feature to featureList object 19 | featureList.displayOutline = displayOutline; 20 | featureList.scrollTopBtn = scrollTopBtn; 21 | featureList.codeLineNumbers = codeLineNumbers; 22 | featureList.spellcheckForCode = spellcheckForCode; 23 | featureList.openFullPage = openFullPage; 24 | featureList.rollupUrlClickable = rollupUrlClickable; 25 | featureList.hideAiBtn = hideAiBtn; 26 | featureList.hideTableOfContents = hideTableOfContents; 27 | 28 | export default defineContentScript({ 29 | matches: ['*://*.notion.so/*', '*://*.notion.site/*'], 30 | main() { 31 | init(); 32 | 33 | browser.storage.onChanged.addListener((changes, namespace) => { 34 | console.debug(changes); 35 | console.debug(namespace); 36 | const func = changes.nb_settings.newValue.call_func; 37 | featureList[func.name](func.arg); 38 | }); 39 | 40 | if (document.readyState !== 'loading') { 41 | console.debug('document is already ready'); 42 | } else { 43 | document.addEventListener('DOMContentLoaded', () => { 44 | console.debug('document was not ready'); 45 | }); 46 | } 47 | 48 | window.onload = () => { 49 | // same as window.addEventListener('load', (event) => { 50 | console.debug('window is ready'); 51 | }; 52 | }, 53 | }); 54 | 55 | function init() { 56 | let syncSet: any = {}; 57 | const updatedSet = { ...defaultSettings }; 58 | 59 | getLatestSettings() 60 | .then((set: any) => { 61 | console.debug('LatestSettings: ', set); 62 | 63 | console.debug('enabling features...'); 64 | // on page load, execute only enabled features 65 | for (const func of Object.keys(set)) { 66 | const isEnabled = set[func]; 67 | if (isEnabled) { 68 | featureList[func](isEnabled); 69 | } 70 | } 71 | return null; 72 | }) 73 | .catch((e) => console.debug(e)); 74 | 75 | browser.storage.sync.get(['nb_settings']).then((result) => { 76 | syncSet = result; 77 | 78 | // merge synced settings with default settings and then apply updated settings 79 | if (!isEmpty(syncSet)) { 80 | for (const k of Object.keys(defaultSettings)) { 81 | if ( 82 | k in syncSet && 83 | !isEmpty(syncSet[k as keyof typeof syncSet]) && 84 | syncSet[k as keyof typeof syncSet] !== defaultSettings[k as keyof typeof defaultSettings] 85 | ) { 86 | updatedSet[k as keyof typeof updatedSet] = syncSet[k as keyof typeof syncSet]; 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /entrypoints/popup/About.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from 'react'; 2 | import { Page } from './main'; 3 | 4 | export function About({ setPageToShow }: { setPageToShow: Dispatch> }) { 5 | return ( 6 |
7 |
{ 11 | setPageToShow('main'); 12 | }} 13 | tabIndex={0}> 14 | 15 |
16 |
19 | 20 | Notion Boost 21 |
22 |
Make Notion more productive and less distractive
23 | 24 | 30 | 31 | 37 | 38 | {/* */} 44 | 45 | {/* what's new} 48 | url="https://gourav.io/notion-boost/whats-new" 49 | txtE=" in this update" 50 | /> */} 51 | {/* 55 | @NotionBoost 56 | 57 | } 58 | url="https://twitter.com/notionboost" 59 | txtE=" for unique tips, tricks, and free goodies." 60 | /> */} 61 | 62 | {/* hey@gourav.io} 65 | // url="hey@gourav.io" 66 | /> */} 67 | Gourav Goyal} 71 | url="https://gourav.io" 72 | /> 73 | 79 |
80 | ); 81 | } 82 | 83 | function Bullet({ txtS, url, urlTxt, txtE }: { txtS: any; url: any; urlTxt: any; txtE: any }) { 84 | return ( 85 |
92 |
101 |
111 |
112 |
113 |
120 |
121 |
134 | {txtS} 135 | {url && ( 136 | 142 | {urlTxt} 143 | 144 | )} 145 | {txtE}{' '} 146 |
147 |
148 |
149 |
150 |
151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /entrypoints/popup/App.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from 'react'; 2 | 3 | import { Page } from './main'; 4 | import { SettingsTable } from '@/components/SettingsTable'; 5 | 6 | function Home({ setPageToShow }: { setPageToShow: Dispatch> }) { 7 | return ( 8 |
9 |
10 |
11 |
{ 16 | window.open('https://gourav.io/notion-boost', '_blank'); 17 | }}> 18 | Notion Boost 19 |
20 |
21 | 22 | 23 |
24 | 29 |
33 | Features info 34 |
35 |
36 | {/* 37 |
38 | Share 39 | 40 |
41 |
*/} 42 |
setPageToShow('about')}> 45 |
49 | About 50 | {/* */} 51 |
52 |
53 |
54 |
60 | 66 | ChatGPT Writer 67 | 68 | - Let AI write emails and messages for you 69 |
70 |
71 |
72 | ); 73 | } 74 | 75 | export default Home; 76 | 77 | // #endregion 78 | 79 | // #region ## ----------------- ICONS ----------------- ## 80 | 81 | function NewTabIcon() { 82 | return ( 83 | 90 | 91 | 92 | 93 | ); 94 | } 95 | 96 | // #endregion 97 | 98 | export const styles = { 99 | link: { 100 | textDecoration: 'underline', 101 | color: '#37352f80', 102 | lineHeight: '1.2', 103 | fontSize: '13px', 104 | cursor: 'pointer', 105 | }, 106 | smallGreyText: { 107 | color: '#37352f80', 108 | lineHeight: '1.2', 109 | fontSize: '12px', 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /entrypoints/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | notion-boost-browser-extension 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /entrypoints/popup/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import '@/styles/popup.scss'; 5 | import Home from './App'; 6 | import { About } from './About'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | , 12 | ); 13 | 14 | function App() { 15 | const [pageToShow, setPageToShow] = useState('main'); 16 | 17 | return ( 18 | <> 19 | {pageToShow === 'main' && } 20 | {pageToShow === 'about' && } 21 | 22 | ); 23 | } 24 | 25 | export type Page = 'main' | 'about'; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-boost-browser-extension", 3 | "description": "Notion Boost - add outline and more to notion.so", 4 | "private": false, 5 | "version": "1.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "wxt", 9 | "dev:firefox": "wxt -b firefox --mv2", 10 | "build": "wxt build", 11 | "compile": "tsc --noEmit", 12 | "postinstall": "wxt prepare", 13 | "check-updates": "ncu -i --format repo,group", 14 | "lint": "eslint .", 15 | "package-chrome": "wxt zip", 16 | "package-firefox": "wxt zip -b firefox", 17 | "package": "pnpm run package-chrome && pnpm run package-firefox" 18 | }, 19 | "dependencies": { 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.3.11", 25 | "@types/react-dom": "^18.3.1", 26 | "@typescript-eslint/eslint-plugin": "^8.9.0", 27 | "@typescript-eslint/parser": "^8.9.0", 28 | "@wxt-dev/module-react": "^1.1.1", 29 | "eslint": "^9.9.0", 30 | "eslint-config-prettier": "^9.1.0", 31 | "eslint-plugin-import": "^2.31.0", 32 | "eslint-plugin-prettier": "^5.2.1", 33 | "eslint-plugin-promise": "^7.1.0", 34 | "eslint-plugin-react": "^7.37.1", 35 | "eslint-plugin-react-hooks": "^5.0.0", 36 | "globals": "^15.11.0", 37 | "prettier": "^3.3.3", 38 | "prettier-plugin-tailwindcss": "^0.6.8", 39 | "sass": "1.70.0", 40 | "sass-loader": "^16.0.2", 41 | "style-loader": "^4.0.0", 42 | "svg-inline-loader": "^0.8.2", 43 | "typescript": "^5.6.3", 44 | "typescript-eslint": "^8.9.0", 45 | "wxt": "^0.19.11" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | // prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs 2 | 3 | /** @type {import("prettier").Config} */ 4 | const config = { 5 | printWidth: 120, 6 | trailingComma: 'all', 7 | tabWidth: 4, 8 | semi: true, 9 | singleQuote: true, 10 | useTabs: false, 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | arrowParens: 'always', 14 | endOfLine: 'lf', 15 | singleAttributePerLine: true, 16 | overrides: [ 17 | { 18 | files: ['*.md'], 19 | options: { 20 | tabWidth: 2, 21 | }, 22 | }, 23 | { 24 | files: ['*.yml'], 25 | options: { 26 | tabWidth: 2, 27 | }, 28 | }, 29 | { 30 | files: ['.hintrc'], 31 | options: { 32 | trailingComma: 'none', 33 | }, 34 | }, 35 | ], 36 | 37 | plugins: [ 38 | 'prettier-plugin-tailwindcss', //must be last 39 | ], 40 | }; 41 | 42 | module.exports = config; 43 | -------------------------------------------------------------------------------- /public/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon.png -------------------------------------------------------------------------------- /public/icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon128.png -------------------------------------------------------------------------------- /public/icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon16.png -------------------------------------------------------------------------------- /public/icon/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon19.png -------------------------------------------------------------------------------- /public/icon/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon24.png -------------------------------------------------------------------------------- /public/icon/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon32.png -------------------------------------------------------------------------------- /public/icon/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon38.png -------------------------------------------------------------------------------- /public/icon/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon48.png -------------------------------------------------------------------------------- /public/icon/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon64.png -------------------------------------------------------------------------------- /public/icon/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GorvGoyl/Notion-Boost-browser-extension/82691faf557fae2fc918ca4dd87d89605f99ac57/public/icon/icon96.png -------------------------------------------------------------------------------- /styles/button.scss: -------------------------------------------------------------------------------- 1 | $toggle: ( 2 | enable: ( 3 | background: rgb(46, 170, 220), 4 | transform: translateX(12px) translateY(0px), 5 | ), 6 | disable: ( 7 | background: rgba(135, 131, 120, 0.3), 8 | transform: translateX(0px) translateY(0px), 9 | ), 10 | ); 11 | 12 | // states of toggle button in popup 13 | @mixin button() { 14 | @each $theme, $map in $toggle { 15 | .row.#{$theme} & { 16 | $theme-map: () !global; 17 | @each $key, $submap in $map { 18 | $value: map-get(map-get($toggle, $theme), "#{$key}"); 19 | $theme-map: map-merge( 20 | $theme-map, 21 | ( 22 | $key: $value, 23 | ) 24 | ) !global; 25 | } 26 | @content; 27 | $theme-map: null !global; 28 | } 29 | } 30 | } 31 | 32 | @function themed($key) { 33 | @return map-get($theme-map, $key); 34 | } 35 | -------------------------------------------------------------------------------- /styles/content.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | $caret-color-light-theme: rgb(55, 53, 47); 4 | $caret-color-dark-theme: rgb(255, 255, 255); 5 | 6 | :root { 7 | --sidebarWidth: 240px; 8 | --outlineWidth: 260px; 9 | } 10 | 11 | .notion-frame { 12 | position: relative; 13 | } 14 | // utility classes 15 | 16 | .nb-h1 { 17 | margin-left: 0px; 18 | } 19 | 20 | .nb-h2 { 21 | margin-left: 16px; 22 | } 23 | 24 | .nb-h3 { 25 | margin-left: 32px; 26 | } 27 | 28 | // bolder text 29 | .bolder { 30 | .notion-dark-theme { 31 | [style^='font-weight:600'] { 32 | font-weight: 700 !important; 33 | } 34 | } 35 | } 36 | 37 | // div[style*="width: 100%"].notion-page-content { 38 | // font-weight: 700 !important; 39 | // } 40 | 41 | // set outline toggle btn 42 | 43 | .outlineToggleBtn { 44 | user-select: none; 45 | transition: background 20ms ease-in 0s; 46 | cursor: pointer; 47 | display: inline-flex; 48 | align-items: center; 49 | flex-shrink: 0; 50 | white-space: nowrap; 51 | height: 28px; 52 | border-radius: 3px; 53 | font-size: 14px; 54 | line-height: 1.2; 55 | min-width: 0px; 56 | padding-left: 8px; 57 | padding-right: 8px; 58 | } 59 | 60 | .notion-dark-theme .outlineToggleBtn { 61 | color: rgba(255, 255, 255, 0.81); 62 | &:hover { 63 | background: rgba(255, 255, 255, 0.055); 64 | } 65 | } 66 | .notion-light-theme .outlineToggleBtn { 67 | color: rgb(55, 53, 47); 68 | &:hover { 69 | background: rgba(55, 53, 47, 0.08); 70 | } 71 | } 72 | 73 | // when outline shown: set inline db width 74 | .nb-outline.show + .notion-scroller.vertical div[style*='width: 100%;'] .notion-page-content > { 75 | .notion-collection_view-block { 76 | width: calc(100% + calc(2 * calc(96px + env(safe-area-inset-left)))) !important; 77 | } 78 | } 79 | 80 | // when outline shown: set simple table width for notion site subdomain 81 | 82 | .notion-cursor-listener 83 | > div:first-child[style*='display: flex; flex-direction: column;'] 84 | .nb-outline.show 85 | + .notion-scroller.vertical 86 | .notion-page-content 87 | > { 88 | .notion-table-block { 89 | width: calc(100vw - var(--outlineWidth) - 10px) !important; 90 | } 91 | } 92 | 93 | // when outline shown: set simple table width when sidebar is hidden 94 | .notion-sidebar-container[style*='width: 0'] 95 | ~ div[style*='display: flex; flex-direction: column;'] 96 | .nb-outline.show 97 | + .notion-scroller.vertical 98 | .notion-page-content 99 | > { 100 | .notion-table-block { 101 | width: calc(100vw - var(--outlineWidth) - 10px) !important; 102 | } 103 | } 104 | 105 | // when outline shown: set simple table width when sidebar is shown 106 | .notion-sidebar-container[style*='width: 2'] 107 | ~ div[style*='display: flex; flex-direction: column;'] 108 | .nb-outline.show 109 | + .notion-scroller.vertical 110 | .notion-page-content 111 | > { 112 | .notion-table-block { 113 | width: calc(100vw - var(--outlineWidth) - 10px - var(--sidebarWidth)) !important; 114 | } 115 | } 116 | 117 | // when outline shown: set page content width 118 | .nb-outline.show + .notion-scroller.vertical { 119 | width: calc(100% - var(--outlineWidth)) !important; 120 | > .whenContentEditable { 121 | width: 100% !important; 122 | } 123 | } 124 | 125 | .nb-outline { 126 | &.show { 127 | display: block !important; 128 | } 129 | display: none; 130 | width: var(--outlineWidth); 131 | position: absolute; 132 | right: 0px; 133 | top: 0px; 134 | .table_of_contents { 135 | width: 100%; 136 | max-width: 1071px; 137 | margin-top: 2px; 138 | margin-bottom: 4px; 139 | padding-left: 15px; 140 | .title { 141 | p { 142 | font-size: 15px; 143 | cursor: pointer; 144 | line-height: 1.5; 145 | padding: 0 0 5px 0; 146 | word-break: break-word; 147 | white-space: pre-wrap; 148 | margin: 0 0 6px 0; 149 | text-align: center; 150 | 151 | &:hover, 152 | &:focus, 153 | &:active { 154 | @include themify() { 155 | background: themed('background'); 156 | } 157 | } 158 | 159 | @include themify() { 160 | color: themed('color'); 161 | fill: themed('fill'); 162 | background-image: themed('background-image'); 163 | } 164 | 165 | font-weight: 500; 166 | text-transform: uppercase; 167 | background-repeat: repeat-x; 168 | background-position: 0px 100%; 169 | background-size: 100% 1px; 170 | } 171 | } 172 | .block-wrapper { 173 | position: relative; 174 | height: calc(100vh - 80px); 175 | overflow: auto; 176 | .block { 177 | @include themify() { 178 | color: themed('color'); 179 | fill: themed('fill'); 180 | } 181 | a { 182 | display: block; 183 | color: inherit; 184 | text-decoration: none; 185 | 186 | .btn { 187 | &:hover, 188 | &:focus, 189 | &:active { 190 | @include themify() { 191 | background: themed('background'); 192 | } 193 | } 194 | transition: background 20ms ease-in 0s; 195 | cursor: pointer; 196 | width: 100%; 197 | .align { 198 | padding: 6px 2px; 199 | font-size: 13px; 200 | line-height: 1.2; 201 | display: flex; 202 | align-items: center; 203 | // margin-left: 0px; add heading class here 204 | .text { 205 | display: inline-block; 206 | // white-space: nowrap; 207 | // overflow: hidden; 208 | // text-overflow: ellipsis; 209 | } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | // fullWidth 219 | .fullWidth .notion-frame { 220 | div.notion-scroller.vertical div.layout { 221 | --margin-width: 96px; 222 | --content-width: 1fr; 223 | } 224 | // applies to both: page header & page content 225 | div.notion-scroller.vertical div[style^='max-width: 100%'][style*='min-width: 0px;']:first-of-type { 226 | width: 100% !important; 227 | } 228 | 229 | // page content 230 | div.notion-scroller.vertical > div > div[style^='display: flex;'][style*='width: 100%'] { 231 | justify-content: space-between !important; 232 | } 233 | 234 | // inline db - set db tabs (view) width 235 | .notion-scroller.vertical .notion-page-content > .notion-collection_view-block > div[style*='padding-left'] { 236 | padding-left: 96px !important; 237 | padding-right: 96px !important; 238 | } 239 | 240 | // inline db - move subgroup to left 241 | .notion-scroller.vertical 242 | .notion-page-content 243 | > .notion-collection_view-block 244 | div[style*='left'][style*='position: sticky'] { 245 | left: 96px !important; 246 | } 247 | 248 | // inline db - set title width 249 | .notion-scroller.vertical 250 | .notion-page-content 251 | > .notion-collection_view-block 252 | > div[style*='display: flex;'] 253 | > div[style*='position: relative;'] 254 | > div[style*='padding-left'][style*='display: flex'] { 255 | padding-left: 96px !important; 256 | padding-right: 96px !important; 257 | } 258 | 259 | // inline db - set width 260 | .notion-scroller.vertical .notion-page-content > .notion-collection_view-block .notion-scroller { 261 | div.notion-table-view, 262 | div.notion-board-view, 263 | div.notion-gallery-view, 264 | div.notion-list-view, 265 | div.notion-calendar-view { 266 | padding-left: 96px !important; 267 | padding-right: 96px !important; 268 | } 269 | } 270 | 271 | // inline db - timeline view - set width 272 | .notion-scroller.vertical 273 | .notion-page-content 274 | > .notion-table-block 275 | .notion-scroller.horizontal 276 | > div[style*='padding-left'] 277 | .notion-scroller.horizontal.notion-collection-view-body[style*='margin-left'] { 278 | margin-right: 96px !important; 279 | margin-left: 96px !important; 280 | } 281 | 282 | // simple table - set width 283 | .notion-scroller.vertical 284 | .notion-page-content 285 | > .notion-table-block 286 | .notion-scroller.horizontal 287 | > div[style*='padding-left'] { 288 | padding-left: 96px !important; 289 | padding-right: 96px !important; 290 | } 291 | 292 | // inline table - set footer width 293 | .notion-scroller.vertical 294 | .notion-page-content 295 | > .notion-collection_view-block 296 | .notion-scroller 297 | > .notion-table-view 298 | > div:first-child 299 | > div:nth-child(5) { 300 | min-width: calc(100% - 192px) !important; 301 | } 302 | } 303 | 304 | // borderOnImages 305 | .borderOnImages { 306 | .notion-light-theme { 307 | .notion-frame { 308 | .notion-page-content .notion-image-block { 309 | box-shadow: 0px 0px 3px 0px $caret-color-light-theme !important; 310 | } 311 | } 312 | } 313 | .notion-dark-theme { 314 | .notion-frame { 315 | .notion-page-content .notion-image-block { 316 | box-shadow: 0px 0px 3px 0px $caret-color-dark-theme !important; 317 | } 318 | } 319 | } 320 | } 321 | 322 | // smallText 323 | .smallText { 324 | // for full page and kanban popup window 325 | .notion-page-content { 326 | font-size: 14px !important; 327 | } 328 | 329 | .notion-frame { 330 | // page title 331 | .notion-scroller.vertical { 332 | div div[style^='width: 100%;'][style*='display: flex;'] div[style*='font-weight: 700;'] { 333 | font-size: 32px !important; 334 | } 335 | } 336 | 337 | // set fonts 338 | .notion-scroller.vertical { 339 | > div:nth-child(2) 340 | > div 341 | > div:nth-child(1) 342 | > div 343 | > div:nth-child(2) 344 | > div.notion-selectable.notion-page-block { 345 | font-size: 32px !important; 346 | } 347 | } 348 | } 349 | } 350 | 351 | // # hideComments 352 | .hideComments-nb { 353 | .notion-scroller.vertical { 354 | .notion-page-view-discussion { 355 | display: none !important; 356 | 357 | & + div { 358 | display: none; 359 | } 360 | } 361 | } 362 | } 363 | 364 | // # hideBacklinks 365 | .hideBacklinks { 366 | .notion-scroller.vertical { 367 | div[contenteditable] > div[style='width: 100%; max-width: 100%; margin: 0px auto 6px;'] { 368 | display: none !important; 369 | & + div { 370 | display: none !important; 371 | } 372 | } 373 | // > div:nth-child(2) > div > div[style*="width: 100%; max-width: 100%;"] { 374 | // display: none !important; 375 | // & + div { 376 | // display: none; 377 | // } 378 | // } 379 | } 380 | } 381 | 382 | // # Scroll Top btn 383 | .scroll-top-btn { 384 | &.show { 385 | display: flex !important; 386 | } 387 | &:hover { 388 | @include themify() { 389 | background: themed('floating-btn-hover-bg'); 390 | } 391 | } 392 | @include themify() { 393 | background: themed('floating-btn-bg'); 394 | box-shadow: themed('floating-btn-shadow'); 395 | } 396 | display: none; 397 | user-select: none; 398 | transition: 399 | opacity 700ms ease 0s, 400 | color 700ms ease 0s; 401 | cursor: pointer; 402 | opacity: 1; 403 | position: absolute; 404 | align-items: center; 405 | justify-content: center; 406 | bottom: 88px; 407 | right: 33px; 408 | width: 36px; 409 | height: 36px; 410 | border-radius: 100%; 411 | font-size: 20px; 412 | z-index: 101; 413 | } 414 | 415 | // 416 | .codeLineNumbers { 417 | .notion-code-block.line-numbers > div { 418 | position: relative; 419 | } 420 | 421 | .code-numbered { 422 | padding-left: 48px !important; 423 | } 424 | .code-line-numbers { 425 | font-size: 85%; 426 | // font-family: var(--theme--font_code) !important; 427 | // color: var(--theme--text_ui_info); 428 | @include themify() { 429 | color: themed('color2'); 430 | } 431 | text-align: right; 432 | position: absolute; 433 | right: calc(100% - 30px); 434 | overflow: hidden; 435 | pointer-events: none; 436 | } 437 | } 438 | 439 | // leftAlignMedia 440 | .leftAlignMedia { 441 | .notion-selectable.notion-image-block, 442 | .notion-selectable.notion-video-block { 443 | align-self: flex-start !important; 444 | } 445 | } 446 | 447 | // hideNotification 448 | .hideNotification .notion-topbar svg.hamburgerMenu + div[style*='background'] { 449 | display: none !important; 450 | } 451 | // addMoreHeightToPage 452 | .addMoreHeightToPage .notion-frame { 453 | div.notion-scroller.vertical > div { 454 | // hide cover 455 | div.layout-full div.pseudoSelection > div[style*='height'] { 456 | display: none !important; 457 | } 458 | 459 | // page icon 460 | div.notion-record-icon.notranslate[style*='margin-top'] { 461 | // margin-top: 0px !important; 462 | display: none !important; 463 | } 464 | 465 | div.notion-page-controls { 466 | margin-top: 0px !important; 467 | } 468 | } 469 | } 470 | 471 | //showHoverText 472 | .showHoverText { 473 | @keyframes hover-delay { 474 | 0% { 475 | position: absolute; 476 | z-index: 1; 477 | } 478 | 100% { 479 | position: absolute; 480 | z-index: 1; 481 | } 482 | } 483 | 484 | .notion-timeline-view { 485 | .notion-selectable.notion-collection_view-block { 486 | clip-path: none !important; 487 | } 488 | } 489 | .notion-table-view, 490 | .notion-timeline-view { 491 | // Headers of table 492 | div.notion-table-view-header-cell > div:hover { 493 | //// make text fully visible 494 | animation-name: hover-delay; 495 | animation-delay: 500ms; 496 | animation-duration: 2s; 497 | // retain the style values that is set by the last keyframe 498 | animation-fill-mode: forwards; 499 | //// remove ellipses, add bg color 500 | div > div:nth-child(2) { 501 | @include themify() { 502 | background: themed('table-header'); 503 | } 504 | overflow: inherit !important; 505 | padding-right: 10px; 506 | height: 100%; 507 | display: flex; 508 | align-items: center; 509 | } 510 | } 511 | 512 | // each row 513 | .notion-selectable.notion-page-block.notion-collection-item { 514 | // for title (ID) cell 515 | > div[style*='display: flex;'] > div[style*='white-space: nowrap;']:hover > span { 516 | animation-name: hover-delay; 517 | animation-delay: 500ms; 518 | animation-duration: 1.5s; 519 | // retain the style values that is set by the last keyframe 520 | animation-fill-mode: forwards; 521 | @include themify() { 522 | background: themed('table-cell'); 523 | } 524 | padding-right: 10px; 525 | } 526 | 527 | // for url 528 | > div[style*='display: flex;'] > div.notion-focusable[style*='white-space: nowrap;']:hover span { 529 | animation-name: hover-delay; 530 | animation-delay: 500ms; 531 | animation-duration: 1.5s; 532 | // retain the style values that is set by the last keyframe 533 | animation-fill-mode: forwards; 534 | @include themify() { 535 | background: themed('table-cell'); 536 | } 537 | padding-right: 10px; 538 | } 539 | 540 | // for text 541 | > div[style*='white-space: nowrap;']:hover > span { 542 | // position: absolute !important; 543 | animation-name: hover-delay; 544 | animation-delay: 500ms; 545 | animation-duration: 1.5s; 546 | // retain the style values that is set by the last keyframe 547 | animation-fill-mode: forwards; 548 | @include themify() { 549 | background: themed('table-cell'); 550 | } 551 | z-index: 1; 552 | padding-right: 10px; 553 | } 554 | 555 | // select & mutli-select 556 | > div[style*='display: block;']:hover > div[style*='display: flex; flex-wrap: nowrap;'] { 557 | animation-name: hover-delay; 558 | animation-delay: 500ms; 559 | animation-duration: 1.5s; 560 | // retain the style values that is set by the last keyframe 561 | animation-fill-mode: forwards; 562 | @include themify() { 563 | background: themed('table-cell'); 564 | } 565 | } 566 | 567 | // for date 568 | > div[style*='display: block;']:hover > div[style*='white-space: nowrap;'] { 569 | animation-name: hover-delay; 570 | animation-delay: 500ms; 571 | animation-duration: 1.5s; 572 | // retain the style values that is set by the last keyframe 573 | animation-fill-mode: forwards; 574 | @include themify() { 575 | background: themed('table-cell'); 576 | } 577 | padding-right: 10px; 578 | } 579 | 580 | // // files & media 581 | // .notion-selectable.notion-page-block:hover { 582 | // div > div { 583 | // position: absolute !important; 584 | // background-color: white; 585 | // div { 586 | // max-width: fit-content; 587 | // span { 588 | // overflow: inherit !important; 589 | // } 590 | // } 591 | // } 592 | // } 593 | } 594 | } 595 | } 596 | 597 | // hideHiddenColumns 598 | .hideHiddenColumns { 599 | // #notion-app > div > div.notion-cursor-listener.showHoverText > div:nth-child(2) > div.notion-frame > div.notion-scroller.vertical > div:nth-child(3) > div > div > div > div:nth-child(1) > div:nth-child(4) 600 | .notion-board-view > .notion-selectable.notion-collection_view-block { 601 | > div:nth-child(1) > div[style*='flex-shrink: 0; display: flex; align-items: center;']:last-child { 602 | display: none !important; 603 | } 604 | 605 | // old: 606 | // > div:nth-child(1) 607 | // > div[style*="flex-shrink: 0; display: flex; align-items: center;"]:nth-last-child(2) { 608 | // display: none !important; 609 | // } 610 | 611 | > div[style*='flex-shrink: 0;']:last-child { 612 | display: none !important; 613 | } 614 | } 615 | } 616 | 617 | // narrowListItems 618 | .narrowListItems { 619 | // smaller line height of para text 620 | .notion-page-content { 621 | line-height: 1.3 !important; 622 | } 623 | // paragraphs 624 | 625 | .notion-selectable.notion-text-block { 626 | margin-top: 0 !important; 627 | margin-bottom: 0 !important; 628 | } 629 | 630 | // notion internal links 631 | .notion-selectable.notion-page-block { 632 | margin-top: 0 !important; 633 | margin-bottom: 0 !important; 634 | } 635 | 636 | // headings 637 | .notion-selectable.notion-header-block, 638 | .notion-selectable.notion-sub_header-block, 639 | .notion-selectable.notion-sub_sub_header-block { 640 | margin-top: 0 !important; 641 | margin-bottom: 0 !important; 642 | } 643 | 644 | // toc 645 | .notion-selectable.notion-table_of_contents-block { 646 | a { 647 | div[style*='display: flex; align-items: center;'] { 648 | padding-top: 3px !important; 649 | padding-bottom: 3px !important; 650 | } 651 | } 652 | } 653 | 654 | // bullet points 655 | .notion-selectable.notion-to_do-block, 656 | .notion-selectable.notion-toggle-block, 657 | .notion-selectable.notion-bulleted_list-block, 658 | .notion-selectable.notion-numbered_list-block { 659 | margin-top: 0 !important; 660 | margin-bottom: 0 !important; 661 | 662 | > div { 663 | // align-items: center !important; 664 | 665 | // select the list icon 666 | 667 | // numbered list 668 | div.pseudoSelection[data-text-edit-side='start'] { 669 | min-height: auto !important; 670 | } 671 | 672 | div[style*='margin-right: 2px; width: 24px; display: flex; align-items: center; justify-content: center;'] { 673 | min-height: auto !important; 674 | } 675 | 676 | // select the text 677 | div[placeholder*='할 일'], 678 | div[placeholder*='토글'], 679 | div[placeholder*='리스트'], 680 | div[placeholder*='To-do'], 681 | div[placeholder*='Toggle'], 682 | div[placeholder*='List'] { 683 | padding-top: 0 !important; 684 | padding-bottom: 0 !important; 685 | } 686 | } 687 | } 688 | 689 | // align center checkbox icon 690 | .notion-selectable.notion-to_do-block { 691 | > div > div.pseudoSelection { 692 | margin-top: 3px !important; 693 | } 694 | } 695 | } 696 | 697 | .indentationLines { 698 | .notion-bulleted_list-block, 699 | .notion-to_do-block { 700 | > div > div:last-child { 701 | position: relative; 702 | } 703 | } 704 | 705 | .notion-bulleted_list-block, 706 | .notion-to_do-block { 707 | > div > div:last-child::before { 708 | content: ''; 709 | position: absolute; 710 | height: calc(100% - 2em); 711 | top: 2em; 712 | left: -14.48px; 713 | width: 1px; 714 | } 715 | } 716 | 717 | // add color based on theme 718 | .notion-light-theme { 719 | .notion-bulleted_list-block, 720 | .notion-to_do-block { 721 | > div > div:last-child::before { 722 | background: rgba($caret-color-light-theme, 0.2); 723 | } 724 | } 725 | } 726 | .notion-dark-theme { 727 | .notion-bulleted_list-block, 728 | .notion-to_do-block { 729 | > div > div:last-child::before { 730 | background: rgba($caret-color-dark-theme, 0.2); 731 | } 732 | } 733 | } 734 | } 735 | 736 | // clickable urls on rollup 737 | .linkComponent { 738 | display: flex; 739 | position: absolute; 740 | right: 6px; 741 | top: 4px; 742 | > div > a { 743 | &:hover { 744 | @include themify() { 745 | background: themed('table-cell-hover'); 746 | } 747 | cursor: pointer !important; 748 | } 749 | user-select: none; 750 | transition: background 20ms ease-in 0s; 751 | cursor: pointer !important; 752 | display: inline-flex; 753 | align-items: center; 754 | justify-content: center; 755 | flex-shrink: 0; 756 | border-radius: 3px; 757 | height: 24px; 758 | width: 24px; 759 | padding: 0px; 760 | @include themify() { 761 | background: themed('table-cell'); 762 | } 763 | @include themify() { 764 | box-shadow: themed('floating-btn-shadow'); 765 | } 766 | svg { 767 | width: 14px; 768 | height: 14px; 769 | display: block; 770 | // fill: rgba(55, 53, 47, 0.4); 771 | @include themify() { 772 | fill: themed('color2'); 773 | } 774 | flex-shrink: 0; 775 | backface-visibility: hidden; 776 | } 777 | } 778 | } 779 | 780 | .disableSlashCommandPlaceholder { 781 | /* prettier-ignore */ 782 | .notranslate[placeholder*=" for commands"]::after { 783 | content: "" !important; 784 | } 785 | } 786 | .hideTableOfContents { 787 | .hide-scrollbar.ignore-scrolling-container[style*='absolute'] { 788 | display: none !important; 789 | } 790 | } 791 | -------------------------------------------------------------------------------- /styles/popup.scss: -------------------------------------------------------------------------------- 1 | @import 'button.scss'; 2 | 3 | $color-gray: #37352f; 4 | *, 5 | *:focus { 6 | outline: 0; 7 | } 8 | * { 9 | box-sizing: border-box; 10 | } 11 | *, 12 | :after, 13 | :before { 14 | box-sizing: border-box; 15 | } 16 | 17 | ::-webkit-scrollbar { 18 | background: transparent; 19 | } 20 | ::-webkit-scrollbar { 21 | width: 10px; 22 | height: 10px; 23 | } 24 | ::-webkit-scrollbar-thumb { 25 | background: #d3d1cb; 26 | } 27 | ::-webkit-scrollbar-track { 28 | background: #edece9; 29 | } 30 | ::-webkit-scrollbar-thumb:hover { 31 | background: #aeaca6; 32 | } 33 | *::selection { 34 | background: rgba(45, 170, 219, 0.3); 35 | } 36 | body { 37 | margin: 0; 38 | margin: 8px; 39 | width: 450px; 40 | padding: 9px 15px; 41 | 42 | fill: currentcolor; 43 | line-height: 1.5; 44 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 45 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 46 | -webkit-font-smoothing: auto; 47 | } 48 | 49 | p { 50 | font-size: 13px; 51 | } 52 | 53 | .icon-nb { 54 | background-image: url('../assets/nb.svg'); 55 | width: 25px; 56 | height: 25px; 57 | background-size: contain; 58 | margin-right: 8px; 59 | } 60 | .twitter { 61 | display: inline-block; 62 | 63 | height: 10px; 64 | width: 10px; 65 | -webkit-mask-image: url('../assets/twitter.svg'); 66 | mask-image: url('../assets/twitter.svg'); 67 | background-color: rgba($color-gray, 0.6); 68 | -webkit-mask-repeat: no-repeat; 69 | mask-repeat: no-repeat; 70 | } 71 | 72 | .back.button { 73 | display: inline-block; 74 | .arrow { 75 | -webkit-mask-image: url('../assets/left-arrow.svg'); 76 | mask-image: url('../assets/left-arrow.svg'); 77 | background-color: rgba($color-gray, 0.6); 78 | display: inline-block; 79 | height: 15px; 80 | width: 15px; 81 | -webkit-mask-repeat: no-repeat; 82 | mask-repeat: no-repeat; 83 | padding-left: 60px; 84 | } 85 | cursor: pointer; 86 | margin-bottom: 5px; 87 | } 88 | 89 | $line: 1px solid rgba($color-gray, 0.1); 90 | 91 | .underline { 92 | border-bottom: $line; 93 | margin-bottom: 16px; 94 | padding-bottom: 8px; 95 | } 96 | 97 | .topline { 98 | border-top: $line; 99 | margin-top: 16px; 100 | padding-top: 16px; 101 | } 102 | 103 | .external-link { 104 | color: rgba($color-gray, 0.6); 105 | fill: rgba($color-gray, 0.6); 106 | cursor: pointer; 107 | // underline 108 | text-decoration: none; 109 | white-space: nowrap; 110 | overflow: hidden; 111 | text-overflow: ellipsis; 112 | background-image: linear-gradient(to right, rgba($color-gray, 0.16) 0%, rgba($color-gray, 0.16) 100%); 113 | background-repeat: repeat-x; 114 | background-position: 0px 100%; 115 | background-size: 100% 1px; 116 | } 117 | .title { 118 | &:hover { 119 | text-decoration: underline; 120 | } 121 | color: $color-gray; 122 | text-decoration: none; 123 | cursor: pointer; 124 | font-size: 18px; 125 | font-weight: 500; 126 | width: auto; 127 | margin-bottom: 10px; 128 | display: inline-block; 129 | } 130 | 131 | .sub-title { 132 | font-size: 14px; 133 | } 134 | .wrapper { 135 | flex-grow: 1; 136 | 137 | .footer { 138 | display: flex; 139 | justify-content: space-between; 140 | .footer-item { 141 | display: inline-flex; 142 | text-decoration: none; 143 | user-select: none; 144 | cursor: pointer; 145 | color: inherit; 146 | // margin-left: -6px; 147 | .btn2 { 148 | &:hover { 149 | background-color: rgba($color-gray, 0.08); 150 | } 151 | user-select: none; 152 | cursor: pointer; 153 | display: inline-flex; 154 | align-items: center; 155 | white-space: nowrap; 156 | height: 20px; 157 | border-radius: 3px; 158 | font-size: 12px; 159 | line-height: 1.2; 160 | padding-left: 5px; 161 | padding-right: 5px; 162 | color: rgba($color-gray, 0.6); 163 | } 164 | .button { 165 | &:hover { 166 | background-color: rgba($color-gray, 0.08); 167 | } 168 | user-select: none; 169 | cursor: pointer; 170 | display: inline-flex; 171 | align-items: center; 172 | flex-shrink: 0; 173 | white-space: nowrap; 174 | // height: 24px; 175 | border-radius: 3px; 176 | font-size: 13px; 177 | line-height: 1.2; 178 | min-width: 0px; 179 | padding: 3px 6px; 180 | color: rgba($color-gray, 0.5); 181 | } 182 | } 183 | } 184 | } 185 | 186 | .button.toggle { 187 | user-select: none; 188 | transition: background 20ms ease-in 0s; 189 | cursor: pointer; 190 | border-radius: 44px; 191 | margin-top: 5px; 192 | 193 | .knob { 194 | display: flex; 195 | flex-shrink: 0; 196 | height: 14px; 197 | width: 26px; 198 | border-radius: 44px; 199 | padding: 2px; 200 | box-sizing: content-box; 201 | 202 | @include button() { 203 | background: themed('background'); 204 | } 205 | transition: 206 | background 200ms ease 0s, 207 | box-shadow 200ms ease 0s; 208 | } 209 | .pos { 210 | width: 14px; 211 | height: 14px; 212 | border-radius: 44px; 213 | background: white; 214 | transition: 215 | transform 50ms ease-out 0s, 216 | background 50ms ease-out 0s; 217 | @include button() { 218 | transform: themed('transform'); 219 | } 220 | } 221 | } 222 | 223 | .payment { 224 | > div:nth-child(1) { 225 | margin-top: 5px; 226 | margin-bottom: 15px; 227 | font-weight: 500; 228 | line-height: 24px; 229 | } 230 | // > div:nth-child(2) { 231 | // margin: 3px 0px; 232 | // } 233 | display: flex; 234 | margin-bottom: 10px; 235 | font-size: 14px; 236 | color: rgb(55, 53, 47); 237 | fill: currentcolor; 238 | line-height: 1.5; 239 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 240 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 241 | -webkit-font-smoothing: auto; 242 | flex-direction: column; 243 | 244 | .pricing { 245 | border: 1px solid rgba($color-gray, 0.09); 246 | border-radius: 3px; 247 | padding: 15px 10px; 248 | .features { 249 | display: flex; 250 | flex-direction: column; 251 | > div { 252 | margin: 7px 0px; 253 | } 254 | } 255 | .payBtn { 256 | margin-top: 20px; 257 | margin-bottom: 10px; 258 | user-select: none; 259 | transition: background 20ms ease-in 0s; 260 | cursor: pointer; 261 | display: inline-flex; 262 | align-items: center; 263 | justify-content: center; 264 | width: 100%; 265 | flex-shrink: 0; 266 | white-space: nowrap; 267 | height: 32px; 268 | border-radius: 3px; 269 | box-shadow: 270 | #0f0f0f1a 0px 0px 0px 1px inset, 271 | #0f0f0f1a 0px 1px 2px; 272 | background: #2eaadc; 273 | color: white; 274 | fill: white; 275 | line-height: 1.2; 276 | padding-left: 12px; 277 | padding-right: 12px; 278 | font-size: 14px; 279 | font-weight: 500; 280 | } 281 | 282 | .loginWrapper { 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | .loginBtn { 287 | user-select: none; 288 | cursor: pointer; 289 | margin-left: 5px; 290 | text-decoration: underline; 291 | white-space: nowrap; 292 | font-size: 14px; 293 | font-weight: 500; 294 | } 295 | } 296 | } 297 | } 298 | 299 | .settings.search { 300 | &::placeholder { 301 | color: rgba($color-gray, 0.4); 302 | font-size: 14px; 303 | } 304 | 305 | width: 100%; 306 | border: none; 307 | // border: 1px solid rgba($color-gray, 1); 308 | border-radius: 2px; 309 | font-size: 14px; 310 | margin-bottom: 16px; 311 | padding: 8px; 312 | color: rgba($color-gray, 0.8); 313 | background: rgba(206, 205, 202, 0.3); 314 | } 315 | .settings.table { 316 | display: flex; 317 | flex-direction: column; 318 | row-gap: 20px; 319 | align-items: flex-start; 320 | width: 100%; 321 | height: auto; 322 | padding-left: 0px; 323 | padding-right: 15px; 324 | overflow: auto; 325 | max-height: 350px; 326 | // fix for firefox 327 | scrollbar-width: thin; 328 | .no-setting { 329 | font-size: 14px; 330 | color: rgba($color-gray, 1); 331 | } 332 | .row { 333 | display: flex; 334 | width: 100%; 335 | // align-items: center; 336 | user-select: none; 337 | cursor: pointer; 338 | 339 | .text-wrapper { 340 | flex: 1 1 0%; 341 | flex-wrap: wrap; 342 | min-height: 38px; 343 | display: flex; 344 | // align-items: center; 345 | .name { 346 | font-size: 14px; 347 | } 348 | .desc { 349 | font-size: 12px; 350 | line-height: 16px; 351 | color: rgba($color-gray, 0.6); 352 | width: 85%; 353 | margin-top: 4px; 354 | } 355 | } 356 | } 357 | 358 | .divider { 359 | &.last { 360 | height: 12px; 361 | } 362 | display: flex; 363 | align-items: center; 364 | justify-content: center; 365 | pointer-events: none; 366 | width: 100%; 367 | height: 24px; 368 | flex: 0 0 auto; 369 | .border { 370 | width: 100%; 371 | height: 1px; 372 | visibility: visible; 373 | border-bottom: 1px solid rgba($color-gray, 0.09); 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /styles/theme.scss: -------------------------------------------------------------------------------- 1 | $btn-shadow-light: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, 2 | rgba(15, 15, 15, 0.1) 0px 2px 4px; 3 | $btn-shadow-dark: rgba(15, 15, 15, 0.2) 0px 0px 0px 1px, 4 | rgba(15, 15, 15, 0.2) 0px 2px 4px; 5 | 6 | $themes: ( 7 | light: ( 8 | color: rgba(55, 53, 47, 0.6), 9 | color2: rgba(55, 53, 47, 0.4), 10 | colorSolid: rgb(55, 53, 47), 11 | fill: rgba(55, 53, 47, 0.6), 12 | background: rgba(55, 53, 47, 0.08), 13 | background-image: 14 | linear-gradient( 15 | to right, 16 | rgba(55, 53, 47, 0.16) 0%, 17 | rgba(55, 53, 47, 0.16) 100% 18 | ), 19 | floating-btn-bg: white, 20 | floating-btn-hover-bg: rgb(239, 239, 238), 21 | floating-btn-shadow: $btn-shadow-light, 22 | table-header: rgb(239, 239, 239), 23 | table-cell: white, 24 | table-cell-hover: rgb(239, 239, 238), 25 | ), 26 | dark: ( 27 | color: rgb(155, 155, 155), 28 | color2: rgba(255, 255, 255, 0.4), 29 | colorSolid: rgba(255, 255, 255, 0.9), 30 | fill: rgb(155, 155, 155), 31 | background: rgba(255, 255, 255, 0.055), 32 | background-image: 33 | linear-gradient( 34 | to right, 35 | rgba(255, 255, 255, 0.14) 0%, 36 | rgba(255, 255, 255, 0.14) 100% 37 | ), 38 | floating-btn-bg: rgb(80, 85, 88), 39 | floating-btn-hover-bg: rgb(98, 102, 104), 40 | floating-btn-shadow: $btn-shadow-dark, 41 | table-header: rgb(71, 76, 80), 42 | table-cell: rgb(47, 52, 55), 43 | table-cell-hover: rgb(98, 102, 104), 44 | ), 45 | ); 46 | 47 | /* 48 | * Implementation of themes 49 | */ 50 | @mixin themify() { 51 | @each $theme, $map in $themes { 52 | // set element colors based on notion theme 53 | //class: .notion-dark-theme / .notion-light-theme 54 | .notion-#{$theme}-theme & { 55 | $theme-map: () !global; 56 | @each $key, $submap in $map { 57 | $value: map-get(map-get($themes, $theme), "#{$key}"); 58 | $theme-map: map-merge( 59 | $theme-map, 60 | ( 61 | $key: $value, 62 | ) 63 | ) !global; 64 | } 65 | @content; 66 | $theme-map: null !global; 67 | } 68 | } 69 | } 70 | 71 | @function themed($key) { 72 | @return map-get($theme-map, $key); 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.wxt/tsconfig.json", 3 | "compilerOptions": { 4 | "allowImportingTsExtensions": true, 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web-ext.config.ts: -------------------------------------------------------------------------------- 1 | import { defineRunnerConfig } from 'wxt'; 2 | 3 | export default defineRunnerConfig({ 4 | disabled: true, // stop browser from being launched automatically after running `pnpm run dev` 5 | }); 6 | -------------------------------------------------------------------------------- /wxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'wxt'; 2 | 3 | // See https://wxt.dev/api/config.html 4 | export default defineConfig({ 5 | modules: ['@wxt-dev/module-react'], 6 | manifestVersion: 3, 7 | outDir: 'build', 8 | zip: { 9 | name: 'notion_boost', 10 | artifactTemplate: '{{name}}_{{browser}}.zip', 11 | sourcesTemplate: '{{name}}_sourcecode.zip', 12 | }, 13 | manifest: ({ browser }) => { 14 | // case: browser is firefox, add browser_specific_settings to avoid storage access error 15 | if (browser === 'firefox') { 16 | return { 17 | ...baseManifest, 18 | browser_specific_settings: { 19 | gecko: { 20 | id: '{0d49b33c-467a-4897-bea4-c82d6756e5c4}', 21 | }, 22 | }, 23 | }; 24 | } 25 | 26 | return baseManifest; 27 | }, 28 | }); 29 | 30 | // base manifest file 31 | const baseManifest = { 32 | name: 'Notion Boost', 33 | short_name: 'Notion Boost', 34 | version: '3.3.6', 35 | description: 36 | 'Boost Notion productivity with 20+ customizations like outline, small text full width for all, back to top button etc', 37 | author: 'Gourav Goyal', 38 | permissions: ['storage'], 39 | host_permissions: ['*://*.notion.so/*', '*://*.notion.site/*'], 40 | homepage_url: 'https://gourav.io/notion-boost', 41 | icons: { 42 | '16': '/icon/icon16.png', 43 | '48': '/icon/icon48.png', 44 | '128': '/icon/icon128.png', 45 | }, 46 | action: { 47 | default_icon: { 48 | '16': '/icon/icon16.png', 49 | '24': '/icon/icon24.png', 50 | '32': '/icon/icon32.png', 51 | }, 52 | }, 53 | }; 54 | --------------------------------------------------------------------------------