Compare commits

..

No commits in common. "dev" and "v0.5.0" have entirely different histories.
dev ... v0.5.0

187 changed files with 2016 additions and 13998 deletions

107
.gitignore vendored
View File

@ -1,9 +1,104 @@
# builds
dist/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# dependencies
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# yarn
.yarn/
.pnp.*
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@ -1,384 +1,7 @@
# changelog
### v0.11.0 (dev)
a complete redesign & rewrite of the enhancer, with new features and a port to the browser as a chrome extension.
#### new
- cross-environment and properly documented api to replace helpers.
- cross-environment mod loader structure.
- "integrations", a category of mods that can access/use an unofficial notion api.
- notifications sourced from an online endpoint for sending global user alerts.
- simplify user installations by depending on the chrome web store and [notion-repackaged](https://github.com/notion-enhancer/notion-repackaged).
- separate menu profiles for mod configurations.
- a hotkey option type that allows typing in/pressing a hotkey to enter it, instead of typing.
- a rainbow indentation lines style.
- border & background style options for the code line numbers extension.
- an icon sets option to encode images to data urls to prevent quality reduction.
- customisation of integrated titlebar & always on top window buttons.
- an open on startup option under the tray mod.
- optional icon or title-only tab labels.
- choice of tab layout styles: traditional tabbed, traditional, bubble and compact.
- a hotkey for reopening closed tabs.
- an option to remember last open tabs for a continue-where-you-left-off experience
(recently active tabs are reopened after an app relaunch).
#### improved
- split the core mod into separate mods for specific features.
- theming variables that are applied more specifically, less laggy, and less complicated.
- merged bracketed-links into tweaks.
- a redesigned menu with nicer ui, separate categories for mods and a sidebar for configuration.
- simplified and smoothened the side panel + moved it to the core so any mod can hook into it.
- font chooser option for heading fonts.
- renamed "property-layout" to "collapsible properties", added per-page memory of collapse state.
- chevron icon instead of arrow for scroll to top.
- moved word counter to display in the side panel instead of within the page,
implemented a more accurate word counter method.
- the topbar icons extension defaults to the notion default topbar icons for comment/updates/favorite/more,
but can revert them to text (it still adds a custom icon for the share button).
- relative indenting in outliner.
- rtl support for toggles, indentation lines, table of contents and databases + force inline math to ltr.
- replaced the "truncated table titles" extension with a "truncated titles" extension
with an option to truncate timeline item titles.
- renamed "notion icons" to "icon sets" with new support for uploading/reusing custom icons
directly within the icon picker.
- moved the tray to its own configurable and enable/disable-able mod, with window management enhancements
that follow more sensible defaults and work more reliably.
- tabs will auto shrink/expand to take up available space instead of wrapping to a second line.
- a visually revamped cli to more clearly and aesthetically communicate status and usage.
- cli can now detect and apply to user-only installations on macOS.
- a shortcut built into the cli to fix the "you do not have permission to open this app" error on macos.
#### removed
- integrated scrollbar tweak (notion now includes by default).
- js insert. css insert moved to tweaks mod.
- majority of layout and font size variables - better to leave former to notion and use `ctrl +`/`ctrl -` for latter.
- the "panel sites" extension, due to it's limited/buggy functionality and incompatibility with reimplementation.
#### fixed
- bypass csp restrictions.
- many. like many many. all the bugfixes. (mostly a side effect of completely rewriting everything,
but reported extension-specific bugs were all intentionally fixed.)
#### themes
- "nord" = an arctic, north-bluish color palette.
- "gruvbox light" = a sepia, 'retro groove' palette based on the vim theme of the same name.
- "gruvbox dark" = a gray, 'retro groove' palette based on the vim theme of the same name.
- "light+" = a simple white theme that brightens coloured text and blocks,
with configurable accents (formerly littlepig light).
- "playful purple" = a purple-shaded theme with bright highlights (formerly littlepig dark and gameish).
- "pinky boom" = pinkify your life.
#### extensions
- "calendar scroll" = add a button to jump down to the current week in fullpage/infinite-scroll calendars.
- "global block links" = easily copy the global link of a page or block.
- "collapsible headers" = adds toggles to collapse header sections of pages.
- "simpler databases" = adds a menu to inline databases to toggle ui elements.
- "view scale" = zoom in/out of the notion window with the mousewheel or a visual slider (`ctrl/cmd +/-` are available in-app by default).
#### tweaks
- wrap tables to page width. - hide "Type '/' for commands".
- quote block quotation marks.
- responsive columns breakpoint (%).
- accented links.
- full width pages.
- image alignment (center/left/right).
#### integrations
- "quick note" = adds a hotkey & a button in the bottom right corner to jump to a new page in a notes database (target database id must be set).
**below this point the enhancer was desktop-only. in v0.11.0 it was been ported to also**
**run as a chrome extension. changes made to both are indicated above.**
### v0.10.2 (2020-12-05)
again, an emergency release for bugfixes.
not properly documented and new features have not yet been fully reviewed/edited.
- new: side panel - adds an extra sidebar on the right for use by other mods,
toggleable with `ctrl+shift+backslash`.
- improved: notion icons uses spritesheets for faster loading of icons.
- improved: icon sets can be hidden/toggled.
- improved: toggles in the enhancer menu follow the same style as notion's toggles.
- improved: separate quote font variable & option in the font chooser mod (`--theme_[dark|light]--font_quote`).
- improved: option to hide the "page details" text for the word counter extension.
- bugfix: notion icons tab is now visible in fullpage databases.
- bugfix: code line numbers handles wrapped code blocks.
- bugfix: file explorer no longer opens when enhancer menu is opened.
- bugfix: enable the remote module in webviews (windows/tabs) for compatibility with the
updated version of electron used by new notion builds (>= 2.0.10).
- bugfix: add support for enhancing an `app` folder if there is no `app.asar` file present.
- extension: "outliner" = table of contents in right sidebar.
- extension: "panel sites" = embed sites on the site panel.
- extension: "indentation lines" = adds vertical relationship lines to make list trees easier to follow.
- extension: "truncated table titles" = see the full text of the truncated table titles on hover over.
> 📥 `npm i -g notion-enhancer@0.10.2`
### v0.10.1 (2020-11-18)
essentially a prerelease for v0.11.0: pushed out for urgent bugfixes during
exam/study weeks when there's no time to code a full release.
note that this means new features have not yet been fully documented and
may not be fully ready for ideal use yet. however, things overall will
work more reliably than v0.10.0.
- new: different css entrypoints for different components (tabs, menu, app).
- improved: use an svg for the scroll-to-top button.
- improved: use a better-matching icon and add transitions to the property layout toggle.
- improved: themes are directly applied to tabs and menu rather than sync-ed between (infinite loading).
- improved: error message "is notion running?" --> clearer "make sure notion isn't running!"
- improved: auto-shrink system for tabs (max of 15 open in a window).
- bugfix: disable fadein of selected block halo with snappy transitions.
- bugfix: increase contrast of `--theme_dark--interactive_hover` in dark+ and dracula.
- bugfix: tabs are focused properly for input.
- bugfix: keyboard shortcut listeners are stricter so they don't conflict.
- bugfix: dots indicating draggability are no longer next to the tabs mod in the menu.
- bugfix: prevent empty hotkeys from triggering every keypress.
- bugfix: don't try loading an empty default page url (infinite loading).
- bugfix: remove `* { z-index: 1}` rule so format dropdowns in table view can be opened.
- extension: "topbar icons" = replaces the topbar buttons with icons.
- extension: "code line numbers" = adds line numbers to code blocks.
- extension: "notion icons" = use custom icon sets directly in notion.
- tweak: vertical indentation/relationship lines for lists.
- tweak: scroll database toolbars horizontally if partially hidden.
- tweak: condense bullet points (decrease line spacing).
> 📥 `npm i -g notion-enhancer@0.10.1`
### v0.10.0 (2020-11-02)
a flexibility update.
- new: mods can be reordered in the menu to control what order styling/scripts are added/executed in.
higher up on the list = higher priority of application = loaded last in order to override others.
(excluding the core, which though pinned to the top of the list is always loaded first so theming
variables can be modified.)
- new: relaunch button in tray menu.
- new: a core mod option for a default page id/url (all new windows will load it instead of the
normal "most recent" page).
- new: css variables for increasing line spacing/paragraph margins.
- new: patch the notion:// url scheme/protocol to work on linux.
- new: menu shows theme conflicts + a core mod option to auto-resolve theme conflicts.
- new: a `-n` cli option.
- improved: menu will now respect integrated titlebar setting.
- improved: use keyup listeners instead of a globalShortcut for the enhancements menu toggle.
- improved: overwrite `app.asar.bak` if already exists (e.g. for app updates).
- improved: additional menu option descriptions on hover.
- improved: listen to prefers-color-scheme to better change theme in night shift.
- improved: platform-specific option overrides for features not required on macOS.
- improved: made extra padding at the bottom with the "focus mode" extension toggleable.
- bugfix: removed messenger emoji set as the provider no longer supports it.
- bugfix: remove shadow around light mode board headers.
- bugfix: properly detect/respond to `EACCES`/`EBUSY` errors.
- bugfix: night shift checks every interaction,
will respond to system changes without any manual changes.
- bugfix: toc blocks can have text colours.
- bugfix: bypass preview extension works with the back/forward keyboard shortcuts.
- bugfix: (maybe) fix csp issues under proxy.
- bugfix: remove focus mode footer from neutral theme + better contrast in calendar views.
- bugfix: improvements to the colour theming, particularly to make real- and fake-light/dark
modes (as applied by the night shift extension) look consistent.
relevant variables (assuming all are prefixed by `--theme_[dark|light]--`):
`box-shadow`, `box-shadow_strong`, `select_input`, and `ui-border`
- bugfix: font sizing applied to overlays/previews.
- bugfix: removed typo in variable name for brown text.
- bugfix: primary-colour text (mainly in "add a \_" popups) is now properly themed.
- bugfix: right-to-left extension applies to text in columns.
- bugfix: block text colour applies to text with backgrounds.
- bugfix: font applied to wrong mode with littlepig dark.
- bugfix: keep "empty" top bar visible in the menu.
- bugfix: set NSRequiresAquaSystemAppearance to false in /Applications/Notion.app/Contents/Info.plist
so system dark/light mode can be properly detected.
- bugfix: make ctrl+f popover shadow less extreme.
- bugfix: "weekly" calendar view name made case insensitive.
- bugfix: re-show hidden windows when clicking on the dock.
- tweak: sticky table/list rows.
- theme: "material ocean" = an oceanic colour palette.
- theme: "cherry cola" = a delightfully plummy, cherry cola flavored theme.
- theme: "dracula" = a theme based on the popular dracula color palette
originally by zeno rocha and friends.
- extension: "tabs" = have multiple notion pages open in a single window. tabs can be controlled
with keyboard shortcuts and dragged/reordered within/between windows.
- extension: "scroll to top" = add an arrow above the help button to scroll back to the top of a page.
- extension: "tweaks" = common style/layout changes. includes:
- new: make transitions snappy/0s.
- new: in-page columns are disabled/wrapped and pages are wider when
the window is narrower than 600px for improved responsiveness.
- new: thicker bold text for better visibility.
- new: more readable line spacing.
- moved: smooth scrollbars.
- moved: change dragarea height.
- moved: hide help.
a fork of notion-deb-builder that does generate an app.asar has been created and is once again supported.
> 📥 `npm i -g notion-enhancer@0.10.0`
### v0.9.1 (2020-09-26)
- bugfix: font chooser will continue iterating through fonts after encountering a blank option.
- bugfix: block indents are no longer overriden.
- bugfix: neutral does not force full width pages.
- bugfix: bypass preview extension works with the back/forward arrows.
- bugfix: check all views on a page for a weekly calendar.
- bugfix: emoji sets no longer modifies the user agent = doesn't break hotkeys.
> 📥 `npm i -g notion-enhancer@0.9.1`
### v0.9.0 (2020-09-20)
a feature and cleanup update.
- improved: halved the number of css rules used -> much better performance.
- improved: font imports must be define in the `mod.js` so that they can also be used in
the enhancements menu.
- improved: tiling window-manager support (can hide titlebars entirely without dragarea/buttons).
- improved: extensions menu search is now case insensitive and includes options, inputs and versions.
the search box can also for focused with `CMD/CTRL+F`.
- improved: extensions menu filters shown either a ✓ or × to help understand the current state.
- improved: added individual text-colour rules for different background colours.
- improved: added variables for callout colouring.
- improved: replaced with `helpers.getNotion()` with the constant `helpers.__notion` to reduce
repeated function calls.
- improved: added variables for page width.
- improved/bugfix: emoji sets extension should now work on macOS and will change user agent to use
real emojis instead of downloading images when system default is selected.
- bugfix: enhancer settings should no longer reset on update (though this will not have
effect until the release after this one).
- bugfix: blue select tags are no longer purple.
- bugfix: page titles now respond to small-text mode.
- bugfix: weekly calendar view height is now sized correctly according to its contents.
- bugfix: made the open enhancements menu hotkey configurable and changed the default to `ALT+E`.
to remove conflict with the inline code highlight shortcut.
- bugfix: update property-layout to match notion changes again.
- bugfix: updated some of the tweak styling to match notion changes.
- bugfix: block-level text colours are now changed properly.
- bugfix: do not require data folder during installation, to prevent `sudo` attempting to
create it in `/var/root/`.
- bugfix: bullet points/checkboxes will now align properly in the right-to-left extension.
- themes: "littlepig" (light + dark) = monospaced themes using emojis and colourful text.
- extension: "font chooser" = customize fonts. for each option, type in the name of the font you would like to use,
or leave it blank to not change anything.
- extension: "always on top" = add an arrow/button to show the notion window on top of other windows
even if it's not focused.
- extension: "calendar scroll" = add a button to scroll down to the current week in fullpage/infinite-scroll calendars.
- extension: "hide help button" = hide the help button if you don't need it.
- extension: "bypass preview" = go straight to the normal full view when opening a page.
- extension: "word counter" = add page details: word/character/sentence/block count & speaking/reading times.
notion-deb-builder has been discovered to not generate an app.asar and so is no longer supported.
> 📥 `npm i -g notion-enhancer@0.9.0`
### v0.8.5 (2020-08-29)
- bugfix: separate text highlight and select tag variables.
- bugfix: bypass CSP for the `enhancement://` protocol - was failing on some platforms?
> 📥 `npm i -g notion-enhancer@0.8.5`
### v0.8.4 (2020-08-29)
- bugfix: property-layout now works consistently with or without a banner.
> 📥 `npm i -g notion-enhancer@0.8.4`
### v0.8.3 (2020-08-29)
previous release was a mistake: it did as intended on linux, but broke windows.
this should achieve the same thing in a more compatible way.
> 📥 `npm i -g notion-enhancer@0.8.3`
### v0.8.2 (2020-08-28)
some things you just can't test until production... fixed the auto-installer
to use `./bin.js` instead of `notion-enhancer`
> 📥 `npm i -g notion-enhancer@0.8.2`
### v0.8.1 (2020-08-28)
a clarity and stability update.
- improved: more informative cli error messages (original ones can be accessed with the `-d/--dev` flag).
- bugfix: gallery variable didn't apply on fullpage.
- bugfix: date picker hid current date number.
- bugfix: small-text pages should now work as expected.
- bugfix: padding issues in page previews.
- bugfix: property-layout extension had been broken by internal notion changes.
- bugfix: linux installer path typo.
- bugfix: caret-color was being mistaken for color and block-level text colouring was broken.
- improved: auto-application on install.
> 📥 `npm i -g notion-enhancer@0.8.1`
### v0.8.0 (2020-08-27)
complete rewrite with node.js.
- new: simpler cli installation system (inc. commands: `apply`, `remove`, and `check`).
- new: mod loading system (easier to create new mods, adds to notion rather than overwriting).
- new: mod configuration menu.
- improved: more theming variable coverage - inc. light theme and sizing/spacing.
- bugfix: non-reproducable errors with python.
- bugfix: better launcher patching on linux.
- bugfix: fix frameless window issue introduced by notion desktop 2.0.9.
- extension: "custom inserts" = link files for small client-side tweaks.
- extension: "bracketed links" = render links surrounded with \[\[brackets]] instead of underlined.
- extension: "focus mode" = hide the titlebar/menubar if the sidebar is closed (will be shown on hover).
- theme: "dark+" = a vivid-colour near-black theme.
- theme: "neutral" = smoother colours and fonts, designed to be more pleasing to the eye.
- theme: "gameish" = a purple, "gamer-styled" theme with a blocky-font.
- theme: "pastel dark" = a smooth-transition true dark theme with a hint of pastel.
- extension: "emoji sets" = pick from a variety of emoji styles to use.
- extension: "night shift" = sync dark/light theme with the system (overrides normal theme setting).
- extension: "right-to-left" = enables auto rtl/ltr text direction detection. (ported from [github.com/obahareth/notion-rtl](https://github.com/obahareth/notion-rtl).)
- extension: "weekly view" = calendar views named "weekly" will show only the 7 days of this week. (ported from [github.com/adihd/notionweeklyview](https://github.com/adihd/notionweeklyview).)]
- extension: "property layout" = auto-collapse page properties that usually push down page content. (ported from [github.com/alexander-kazakov/notion-layout-extension](https://github.com/alexander-kazakov/notion-layout-extension).)
> 📥 `npm i -g notion-enhancer@0.8.0`
### v0.7.0 (2020-07-09)
- new: tray option to use system default emojis (instead of twitter's emojiset).
- new: mac support (identical functionality to other platforms with the
exception of the native minimise/maximise/close buttons being kept, as they integrate
better with the OS while not being out-of-place in notion).
- new: notion-deb-builder support for linux.
- new: an alert will be shown if there is an update available for the enhancer.
- improved: replaced button symbols with svgs for multi-platform support.
- improved: window close button is now red on hover (thanks to [@torchatlas](https://github.com/torchatlas)).
- bugfix: `cleaner.py` patched for linux.
- bugfix: tray now operates as expected on linux.
- bugfix: odd mix of `\\` and `/` being used for windows filepaths.
- bugfix: app no longer crashes when sidebar is toggled.
> 📥 [notion-enhancer.v0.7.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.7.0.zip)
### v0.6.0 (2020-06-30)
- style: custom fonts.
- style: font resizing.
- style: hide discussions (thanks to [u/Roosmaryn](https://www.reddit.com/user/Roosmaryn/)).
- new: custom colour theming, demonstrated via the dark+ theme.
- new: linux support (thanks to [@Blacksuan19](https://github.com/Blacksuan19)).
- improved: if hotkey is pressed while notion is unfocused, it will bring it to the front rather than hiding it.
- improved: stop window buttons breaking at smaller widths.
- improved: more obviously visible drag area.
- bugfix: specify UTF-8 encoding to prevent multibyte/gbk codec errors (thanks to [@etnperlong](https://github.com/etnperlong)).
> 📥 [notion-enhancer.v0.6.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.6.0.zip)
if something is ~~crossed out~~, then it is no longer a feature included by default,
but can still easily be enabled by following instructions in the [docs](README.md).
### v0.5.0 (2020-05-23)
@ -386,16 +9,11 @@ complete rewrite with node.js.
- new: reload window with f5.
- improved: code has been refactored and cleaned up,
inc. file renaming and a `customiser.py` that doesn't require
a run of `cleaner.py` to build modifications.
a run of `cleaner.py` to build updates.
improved: scrollbar colours that fit better with notion's theming.
- bugfix: un-break having multiple notion windows open.
> 📥 [notion-enhancer.v0.5.0.zip](https://github.com/notion-enhancer/desktop/archive/v0.5.0.zip)
**development here taken over by [@dragonwocky](https://github.com/dragonwocky).**
**the ~~crossed out~~ features below are no longer features included by default,**
**but can still easily be added as [custom tweaks](https://github.com/notion-enhancer/tweaks).**
_(forked by [@dragonwocky](https://github.com/dragonwocky).)_
### v0.4.1 (2020-02-13)

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
Copyright (c) 2020 TarasokUA
Copyright (c) 2020 dragonwocky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

242
README.md
View File

@ -1,5 +1,241 @@
# notion-enhancer/desktop
# notion enhancer
Customise the all-in-one productivity workspace Notion.
an enhancer/customiser for the all-in-one productivity workspace [notion.so](https://www.notion.so/)
[read the docs online](https://notion-enhancer.github.io/)
## installation
currently, only win10 is supported. it is possible to run this script via the wsl to modify the win10 notion app.
(the [styles](#styling) should also work for the web version.
these can be installed via an extension like [stylus](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=en)
or a built-in feature like [userChrome.css](https://www.userchrome.org/).)
1. install [node.js](https://nodejs.org/en/) (if using the wsl, it is recommended to install via [nvm](https://github.com/nvm-sh/nvm#install--update-script)).
2. install [python](https://www.python.org/) (if using the wsl, follow [this guide](https://docs.python-guide.org/starting/install3/linux/)).
3. reboot.
4. in cmd (on windows) or bash (with wsl), run `npm install -g asar` (check installation by running `asar`).
5. [download this enhancer](https://github.com/dragonwocky/notion-enhancer/archive/master.zip) & extract to a location it can safely remain (this must be in the windows filesystem,
even if you are running the script from the wsl).
6. ensure notion is closed.
7. optional: to remove previous versions of notion enhancer, run `cleaner.py`
8. optional: modify the `resources/user.css` files to your liking.
9. run `customiser.py` to build changes.
done: run notion and enjoy.
**oh no, now my app won't open!**
1. kill any notion tasks in the task manager (`ctrl+shift+esc`).
2. run `cleaner.py`.
3. reboot.
4. follow instructions above (ensuring notion _isn't_ running! again, check task manager).
## this is a fork
credit where credit is due, this was originally made by Uzver (github: [@TarasokUA](https://github.com/TarasokUA),
telegram: [UserFromUkraine](https://t.me/UserFromUkraine), discord: Uzver#8760).
he has approved my go-ahead with this fork, as he himself no longer wishes to continue development on the project.
## features
### titlebar
default windows titlebar/frame has been replaced by one more fitting to the theme of the app.
this includes the addition of an extra button, "always on top"
symbolised with an arrow (4th from the right). when toggled to point up,
notion will remain the top visible window even if not focused.
to customise which characters are used for these buttons, open in the `resources/preload.js` file,
find the relevant button (read the comments) and replace its icon with your chosen unicode character (e.g.
replacing `element.innerHTML = '▢';` with `element.innerHTML = '🙄';`).
### nicer scrollbars
i mean, yeah. get rid of those ugly default scrollbars and use nice inconspicuous
ones that actually look as if they're part of notion.
to add these to the web version, copy lines 44 - 75 from `user.css` into your css customiser.
### hotkeys
- **reload window**: in addition to the built-in `CmdOrCtrl+R` reload,
you can now reload a window with `F5`.
- **toggle all notion windows to/from the tray**: `CmdOrCtrl+Shift+A` by default.
to set your own toggle hotkey, open `customiser.py` and change line 16 (`hotkey = 'CmdOrCtrl+Shift+A'`)
to your preference. you will need to run or re-run `customiser.py` afterwards.
### tray
- single-click to toggle app visibility. right click to open menu.
- settings will be saved in `%localappdata%/Programs/Notion/resources/app/user-preferences.json`
- **run on startup**: run notion on boot/startup. (default: true)
- **hide on open**: hide the launch of notion to the tray. (default: false)
- **open maximised**: maximise the app on open. (default: false)
- **close to tray**: close window to tray rather than closing outright
on click of `⨉`. does not apply if multiple notion windows are open. (default: false)
### styling
due to `customiser.py` setting up a direct link to `resources/user.css`,
changes will be applied instantly on notion reload
(no need to re-run `customiser.py` every time you want to change some styles).
these should also work for the web version, if copied into your css customiser.
css below will work for every instance of the element, but if you wish to hide only a specific element
(e.g. the '+ new' table row) it is recommended that you prepend each selector with `[data-block-id='ID']` ([video tutorial on fetching IDs](https://www.youtube.com/watch?v=6V7eqShm_4w)).
#### wider page view
```css
.notion-peek-renderer > div:nth-child(2) {
max-width: 85vw !important;
}
```
#### thinner cover image
```css
[style^='position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'] {
height: 12vh !important;
}
[style^='position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;']
img {
height: 20vh !important;
}
```
#### table columns below 100px
**not recommended!** this is unreliable and will cause bugs.
coincidentally, this is also what the youtube video linked above shows how to do.
as it is a per-table-column style, unlike all others here, it must be prepended with the block ID.
```css
[data-block-id^='ID']
> [style^='display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);']
> div:nth-child(1)
> div:nth-child(10)
> div:nth-child(1),
[data-block-id^='ID']
> [style^='position: relative; min-width: calc(100% - 192px);']
> [data-block-id]
> div:nth-child(10),
[data-block-id^='ID'] > div:nth-child(5) > div:nth-child(10) {
width: 45px !important;
}
[data-block-id^='ID']
[style^='position: absolute; top: 0px; left: 0px; pointer-events: none;']:not(.notion-presence-container) {
display: none;
}
```
#### hide '+ new' table row
```css
.notion-table-view-add-row {
display: none !important;
}
```
#### hide calculations table row
```css
.notion-table-view-add-row + div {
display: none !important;
}
```
#### hide '+ new' board row
```css
.notion-board-group
[style='user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap; height: 32px; border-radius: 3px; font-size: 14px; line-height: 1.2; min-width: 0px; padding-left: 6px; padding-right: 8px; color: rgba(255, 255, 255, 0.4); width: 100%;'] {
display: none !important;
}
```
#### hide board view hidden columns
```css
.notion-board-view > [data-block-id] > div:nth-last-child(2),
.notion-board-view > [data-block-id] > div:first-child > div:nth-last-child(2) {
display: none !important;
}
```
#### hide board view 'add a group'
```css
.notion-board-view > [data-block-id] > div:last-child,
.notion-board-view > [data-block-id] > div:first-child > div:last-child {
display: none !important;
}
```
#### centre-align table column headers
```css
.notion-table-view-header-cell > div > div {
margin: 0px auto;
}
```
#### smaller table column header icons
```css
[style^='display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);']
div:nth-child(1)
svg {
height: 10px !important;
width: 10px !important;
margin-right: -4px;
}
```
#### remove icons from table column headers
```css
.notion-table-view-header-cell [style^='margin-right: 6px;'] {
display: none !important;
}
```
#### removing/decreasing side padding for tables
```css
[style^='flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;']
.notion-table-view,
[class='notion-scroller'] > .notion-table-view {
padding-left: 35px !important;
padding-right: 15px !important;
min-width: 0% !important;
}
[style^='flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;']
.notion-selectable
.notion-scroller.horizontal::-webkit-scrollbar-track {
margin-left: 10px;
margin-right: 10px;
}
```
#### removing/decreasing side padding for boards
```css
.notion-board-view {
padding-left: 10px !important;
padding-right: 10px !important;
}
```
## other details
i have an unhealthy habit of avoiding capital letters. nothing enforces this, i just do it.
the notion logo belongs entirely to the notion team, and was sourced from their
[media kit](https://www.notion.so/Media-Kit-205535b1d9c4440497a3d7a2ac096286).
if you have any questions, check [my website](https://dragonwocky.me/) for contact details.

331
bin.mjs
View File

@ -1,331 +0,0 @@
#!/usr/bin/env node
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import os from "node:os";
import { createRequire } from "node:module";
import chalk from "chalk-template";
import arg from "arg";
import {
backupApp,
enhanceApp,
getInsertVersion,
getResourcePath,
restoreApp,
setNotionPath,
} from "./scripts/enhance-desktop-app.mjs";
import { greaterThan } from "./src/core/updateCheck.mjs";
import { existsSync } from "node:fs";
const nodeRequire = createRequire(import.meta.url),
manifest = nodeRequire("./package.json");
let __quiet, __debug;
const print = (...args) => __quiet || process.stdout.write(chalk(...args)),
printObject = (value) => __quiet || console.dir(value, { depth: null }),
clearLine = `\r\x1b[K`,
showCursor = `\x1b[?25h`,
hideCursor = `\x1b[?25l`,
cursorUp = (n) => `\x1b[${n}A`,
cursorForward = (n) => `\x1b[${n}C`;
let __confirmation;
const readStdin = () => {
return new Promise((res) => {
process.stdin.resume();
process.stdin.setEncoding("utf8");
process.stdin.once("data", (key) => {
process.stdin.pause();
res(key);
});
});
},
promptConfirmation = async (prompt) => {
let input;
const validInputs = ["Y", "y", "N", "n"],
promptLength = ` > ${prompt} [Y/n]: `.length;
// prevent line clear remove existing stdout
print`\n`;
do {
// clear line and repeat prompt until valid input is received
print`${cursorUp(1)}${clearLine} {inverse > ${prompt} [Y/n]:} `;
// autofill prompt response if --yes, --no or --quiet flags passed
if (validInputs.includes(__confirmation)) {
input = __confirmation;
print`${__confirmation}\n`;
} else input = (await readStdin()).trim();
if (!input) {
// default to Y if enter is pressed w/out input
input = "Y";
print`${cursorUp(1)}${cursorForward(promptLength)}Y\n`;
}
} while (!validInputs.includes(input));
// move cursor to immediately after input
print`${cursorUp(1)}${cursorForward(promptLength + 1)}`;
return input;
};
let __spinner;
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
stopSpinner = () => {
if (!__spinner) return;
clearInterval(__spinner);
// show cursor and overwrite spinner with arrow on completion
print`\b{bold.yellow →}\n${showCursor}`;
__spinner = undefined;
},
startSpinner = () => {
// cleanup prev spinner if necessary
stopSpinner();
// hide cursor and print first frame
print`${hideCursor}{bold.yellow ${spinnerFrames[0]}}`;
let i = 0;
__spinner = setInterval(() => {
i++;
// overwrite spinner with next frame
print`\b{bold.yellow ${spinnerFrames[i % spinnerFrames.length]}}`;
}, 80);
};
const compileOptsToArgSpec = (options) => {
const args = {};
for (const [opt, [type]] of options) {
const aliases = opt.split(", ").map((alias) => alias.split("=")[0]),
param = aliases[1] ?? aliases[0];
args[param] = type;
for (let i = 0; i < aliases.length; i++) {
if (aliases[i] === param) continue;
args[aliases[i]] = param;
}
}
return args;
},
compileOptsToJsonOutput = (options) => {
// the structure used to define options above
// is convenient and compact, but requires additional
// parsing to understand. this function processes
// options into a more explicitly defined structure
return options.map(([opt, [type, description]]) => {
const option = {
aliases: opt.split(", ").map((alias) => alias.split("=")[0]),
type,
description,
},
example = opt
.split(", ")
.map((alias) => alias.split("=")[1])
.find((value) => value);
if (example) option.example = example;
return option;
});
};
let __json;
const printHelp = (commands, options) => {
const { name, version, homepage } = manifest,
usage = `${name} <command> [options]`;
if (__json) {
printObject({
name,
version,
homepage,
usage,
commands: Object.fromEntries(commands),
options: compileOptsToJsonOutput(options),
});
} else {
const cmdPad = Math.max(...commands.map(([cmd]) => cmd.length)),
optPad = Math.max(...options.map((opt) => opt[0].length)),
parseCmd = (cmd) =>
chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
parseOpt = (opt) =>
chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
print`{bold.whiteBright.underline ${name} v${version}}\n{grey ${homepage}}
\n{bold.whiteBright USAGE}\n${name} <command> [options]
\n{bold.whiteBright COMMANDS}\n${commands.map(parseCmd).join("\n")}
\n{bold.whiteBright OPTIONS}\n${options.map(parseOpt).join("\n")}\n`;
}
},
printVersion = () => {
if (__json) {
printObject({
[manifest.name]: manifest.version,
node: process.version.slice(1),
platform: process.platform,
architecture: process.arch,
os: os.release(),
});
} else {
const nodeVersion = `node@${process.version}`,
enhancerVersion = `${manifest.name}@v${manifest.version}`,
osVersion = `${process.platform}-${process.arch}/${os.release()}`;
print`${enhancerVersion} via ${nodeVersion} on ${osVersion}\n`;
}
};
try {
const commands = [
// ["command", "description"]
["apply", "Inject the notion-enhancer into Notion desktop."],
["remove", "Restore Notion desktop to its pre-enhanced state."],
["check", "Report Notion desktop's enhancement state."],
],
// prettier-ignore
options = [
// ["alias, option=example", [type, "description"]]
["--path=</path/to/notion/resources>", [String, "Manually provide a Notion installation location."]],
["--no-backup", [Boolean, "Skip backup; enhancement will be irreversible."]],
["--json", [Boolean, "Output JSON from the `check` and `--version` commands."]],
["-y, --yes", [Boolean, 'Skip prompts; assume "yes" and run non-interactively.']],
["-n, --no", [Boolean, 'Skip prompts; assume "no" and run non-interactively.']],
["-q, --quiet", [Boolean, 'Skip prompts; assume "no" unless -y and hide all output.']],
["-d, --debug", [Boolean, "Show detailed error messages and keep extracted files."]],
["-h, --help", [Boolean, "Display usage information for this CLI."]],
["-v, --version", [Boolean, "Display this CLI's version number."]],
];
const args = arg(compileOptsToArgSpec(options));
if (args["--debug"]) __debug = true;
if (args["--quiet"]) __quiet = true;
if (args["--json"]) __json = true;
if (args["--no"] || args["--quiet"]) __confirmation = "n";
if (args["--yes"]) __confirmation = "y";
if (args["--help"]) printHelp(commands, options), process.exit();
if (args["--version"]) printVersion(), process.exit();
if (args["--path"]) setNotionPath(args["--path"]);
const appPath = getResourcePath("app.asar"),
backupPath = getResourcePath("app.asar.bak"),
insertVersion = await getInsertVersion(),
updateAvailable = greaterThan(manifest.version, insertVersion);
const messages = {
"notion-found": insertVersion
? // prettier-ignore
`Notion desktop found with ${manifest.name} v${insertVersion
} applied${updateAvailable ? "" : " (up to date)"}.`
: `Notion desktop found (no enhancements applied).`,
"notion-not-found": `Notion desktop not found.`,
// prettier-ignore
"update-available": chalk`v${manifest.version
} is available! To apply, run {underline ${manifest.name} apply -y}.`,
// prettier-ignore
"update-confirm": `${updateAvailable ? "Upgrade" : "Downgrade"
} to ${manifest.name}${manifest.name} v${manifest.version}?`,
"backup-found": `Restoring to pre-enhanced state from backup...`,
"backup-not-found": chalk`No backup found: to restore Notion desktop to its pre-enhanced state,
uninstall it and reinstall Notion from {underline https://www.notion.so/desktop}.`,
"backup-app": `Backing up app before enhancement...`,
"enhance-app": `Enhancing and patching app sources...`,
};
const SUCCESS = chalk`{bold.whiteBright SUCCESS} {green ✔}`,
FAILURE = chalk`{bold.whiteBright FAILURE} {red ✘}`,
CANCELLED = chalk`{bold.whiteBright CANCELLED} {red ✘}`,
INCOMPLETE = Symbol();
const interactiveRestore = async () => {
if (!backupPath || !existsSync(backupPath)) {
print` {red * ${messages["backup-not-found"]}}\n`;
return FAILURE;
}
print` {grey * ${messages["backup-found"]}} `;
startSpinner();
await restoreApp();
stopSpinner();
return SUCCESS;
};
const getNotion = () => {
if (!appPath || !existsSync(appPath)) {
print` {red * ${messages["notion-not-found"]}}\n`;
return FAILURE;
} else {
print` {grey * ${messages["notion-found"]}}\n`;
return INCOMPLETE;
}
},
compareVersions = async () => {
if (!insertVersion) return INCOMPLETE;
// same version already applied
if (insertVersion === manifest.version) return SUCCESS;
// diff version already applied
print` {grey * ${messages["notion-found"]}}\n`;
const replace = await promptConfirmation(messages["update-confirm"]);
print`\n`;
return ["Y", "y"].includes(replace)
? (await interactiveRestore()) === SUCCESS
? INCOMPLETE
: FAILURE
: CANCELLED;
},
interactiveEnhance = async () => {
if (!args["--no-backup"]) {
print` {grey * ${messages["backup-app"]}} `;
startSpinner();
await backupApp();
stopSpinner();
}
print` {grey * ${messages["enhance-app"]}} `;
startSpinner();
await enhanceApp(__debug);
stopSpinner();
return SUCCESS;
};
switch (args["_"][0]) {
case "apply": {
print`{bold.whiteBright [${manifest.name.toUpperCase()}] APPLY}\n`;
let res = getNotion();
if (res === INCOMPLETE) res = await compareVersions();
if (res === INCOMPLETE) res = await interactiveEnhance();
print`${res}\n`;
break;
}
case "remove": {
print`{bold.whiteBright [${manifest.name.toUpperCase()}] REMOVE}\n`;
let res = getNotion();
if (res === INCOMPLETE) {
res = insertVersion ? await interactiveRestore() : SUCCESS;
}
print`${res}\n`;
break;
}
case "check": {
if (__json) {
const cliVersion = manifest.version,
state = { appPath, backupPath, insertVersion, cliVersion };
if (appPath && !existsSync(appPath)) state.appPath = null;
if (backupPath && !existsSync(backupPath)) state.backupPath = null;
printObject(state), process.exit();
}
print`{bold.whiteBright [${manifest.name.toUpperCase()}] CHECK}\n`;
let res = getNotion();
if (res === INCOMPLETE && updateAvailable) {
print` {grey * ${messages["update-available"]}}\n`;
}
break;
}
default:
printHelp(commands, options);
}
} catch (err) {
stopSpinner();
const message = err.message.split("\n")[0];
if (__debug) {
print`{bold.red ${err.name}:} ${message}\n{grey ${err.stack
.split("\n")
.splice(1)
.map((at) => at.replace(/\s{4}/g, " "))
.join("\n")}}`;
} else {
print`{bold.red Error:} ${message} {grey (Run with -d for more information.)}\n`;
}
}

58
cleaner.py Normal file
View File

@ -0,0 +1,58 @@
# Notion Enhancer
# (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
# (c) 2020 TarasokUA
# (https://dragonwocky.me/) under the MIT license
import os
import sys
import platform
import subprocess
from shutil import rmtree
from time import sleep
# f'{bold}=== title ==={normal}' = headers
# '*' = information
# '...' = actions
# '###' = warnings
# '>' = exit
bold = '\033[1m'
normal = '\033[0m'
print(f'{bold}=== NOTION ENHANCER CLEANING LOG ==={normal}\n')
try:
filepath = ''
if 'microsoft' in platform.uname()[3].lower() and sys.platform == 'linux':
filepath = '/mnt/c/' + \
subprocess.run(
['cmd.exe', '/c', 'echo', '%localappdata%'], stdout=subprocess.PIPE).stdout \
.rstrip().decode('utf-8')[3:].replace('\\', '/') + '/Programs/Notion/resources'
elif sys.platform == 'win32':
filepath = subprocess.run(['echo', '%localappdata%'], shell=True, capture_output=True).stdout \
.rstrip().decode('utf-8').replace('\\', '/') + '/Programs/Notion/resources'
else:
print(' > script not compatible with your os!\n (report this to dragonwocky#8449 on discord)')
exit()
if os.path.exists(filepath + '/app'):
print(
f' ...removing folder {filepath}/app/')
rmtree(filepath + '/app')
else:
print(
f' * {filepath}/app/ was not found: step skipped.')
if os.path.isfile(filepath + '/app.asar.bak'):
print(' ...renaming asar.app.bak to asar.app')
os.rename(filepath + '/app.asar.bak', filepath + '/app.asar')
else:
print(
f' * {filepath}/app.asar.bak was not found: step skipped.')
print(f'\n{bold}>>> SUCCESSFULLY CLEANED <<<{normal}')
except Exception as e:
print(f'\n{bold}### ERROR ###{normal}\n{str(e)}')
print(f'\n{bold}=== END OF LOG ==={normal}')

175
customiser.py Normal file
View File

@ -0,0 +1,175 @@
# Notion Enhancer
# (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
# (c) 2020 TarasokUA
# (https://dragonwocky.me/) under the MIT license
import re
import os
import sys
import platform
import subprocess
from shutil import copyfile
from time import sleep
# for toggling notion visibility
hotkey = 'CmdOrCtrl+Shift+A'
# f'{bold}=== title ==={normal}' = headers
# '*' = information
# '...' = actions
# '##' = warnings
# '>' = exit
bold = '\033[1m'
normal = '\033[0m'
print(f'{bold}=== NOTION ENHANCER CUSTOMISATION LOG ==={normal}\n')
try:
filepath = ''
__folder__ = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/')
if 'microsoft' in platform.uname()[3].lower() and sys.platform == 'linux':
filepath = '/mnt/c/' + \
subprocess.run(
['cmd.exe', '/c', 'echo', '%localappdata%'], stdout=subprocess.PIPE).stdout \
.rstrip().decode('utf-8')[3:].replace('\\', '/') + '/Programs/Notion/resources'
drive = __folder__[5].capitalize() if __folder__.startswith(
'/mnt/') else 'C'
__folder__ = drive + ':/' + __folder__[6:]
elif sys.platform == 'win32':
filepath = subprocess.run(['echo', '%localappdata%'], shell=True, capture_output=True).stdout \
.rstrip().decode('utf-8').replace('\\', '/') + '/Programs/Notion/resources'
else:
print(' > script not compatible with your os!\n (report this to dragonwocky#8449 on discord)')
exit()
if os.path.isfile(filepath + '/app.asar'):
print(' ...unpacking app.asar')
subprocess.run(['asar', 'extract', filepath +
'/app.asar', filepath + '/app'], shell=(True if sys.platform == 'win32' else False))
print(' ...renaming asar.app to asar.app.bak')
os.rename(filepath + '/app.asar', filepath + '/app.asar.bak')
else:
print(f' ## file {filepath}/app.asar not found!')
print(' * attempting to locate')
if os.path.exists(filepath + '/app'):
print(' * app.asar was already unpacked: step skipped.')
else:
print(' > nothing found: exiting.')
exit()
if os.path.isfile(filepath + '/app/renderer/preload.js'):
print(f' ...adding preload.js to {filepath}/app/renderer/preload.js')
with open(filepath + '/app/renderer/preload.js') as content:
if '/* === INJECTION MARKER === */' in content.read():
print(' * preload.js already added. replacing it.')
content.seek(0)
original = []
for num, line in enumerate(content):
if '/* === INJECTION MARKER === */' in line:
break
original += line
with open(filepath + '/app/renderer/preload.js', 'w') as write:
write.writelines(original)
else:
with open(filepath + '/app/renderer/preload.js', 'a') as append:
append.write('\n\n')
with open(filepath + '/app/renderer/preload.js', 'a') as append:
print(' ...linking to ./resources/user.css')
with open('./resources/preload.js') as insert:
append.write(insert.read().replace(
'___user.css___', __folder__
+ '/resources/user.css'))
else:
print(
f' * {filepath}/app/renderer/preload.js was not found: step skipped.')
if os.path.isfile(filepath + '/app/main/createWindow.js'):
with open(filepath + '/app/main/createWindow.js') as content:
content = content.read()
print(
f' ...making window frameless @ {filepath}/app/main/createWindow.js')
if '{ frame: false, show: false' not in content:
content = content.replace(
'{ show: false', '{ frame: false, show: false')
print(
f' ...adding "open hidden" capabilities to {filepath}/app/main/createWindow.js')
content = re.sub('\\s*\\/\\* === INJECTION START === \\*\\/.*?\\/\\* === INJECTION END === \\*\\/\\s*',
'window.show()', content, flags=re.DOTALL).replace('window.show()', """
/* === INJECTION START === */
const path = require('path'),
store = new (require(path.join(__dirname, '..', 'store.js')))({
config: 'user-preferences',
defaults: {
openhidden: false,
maximised: false
}
});
if (!store.get('openhidden') || electron_1.BrowserWindow.getAllWindows().some(win => win.isVisible()))
{ window.show(); if (store.get('maximised')) window.maximize(); }
/* === INJECTION END === */
""")
with open(filepath + '/app/main/createWindow.js', 'w') as write:
write.write(content)
else:
print(
f' * {filepath}/app/main/createWindow.js was not found: step skipped.')
if os.path.isfile(filepath + '/app/renderer/index.js'):
with open(filepath + '/app/renderer/index.js') as content:
print(
f' ...adjusting drag area for frameless window in {filepath}/app/renderer/index.js')
content = content.read()
top = content.rfind('top')
content = content[:top] + content[top:].replace(
'right: 0', 'right: 420').replace(
'top: 0', 'top: 1 ').replace(
'height: 34', 'height: 16')
with open(filepath + '/app/renderer/index.js', 'w') as write:
write.write(content)
else:
print(
f' * {filepath}/app/renderer/index.js was not found: step skipped.')
if os.path.isfile(filepath + '/app/main/main.js'):
with open(filepath + '/app/main/main.js') as content:
print(
f' ...adding tray support (inc. context menu with settings) to {filepath}/app/main/main.js')
print(
f' ...adding window toggle hotkey to {filepath}/app/main/main.js')
content = content.read()
with open(filepath + '/app/main/main.js', 'w') as write:
if '/* === INJECTION MARKER === */' in content:
print(' * hotkey.js already added. replacing it.')
original = []
for line in content.splitlines():
if '/* === INJECTION MARKER === */' in line:
break
original.append(line)
write.write('\n'.join(original))
else:
write.write(content.replace(
'electron_1.app.on("ready", handleReady);',
'electron_1.app.on("ready", () => handleReady() && enhancements());') + '\n')
with open(filepath + '/app/main/main.js', 'a') as append:
with open('./resources/hotkey.js') as insert:
append.write('\n' + insert.read().replace(
'___hotkey___', hotkey))
print(
f' ...copying tray icon ./resources/notion.ico to {filepath}/app/main/')
copyfile('./resources/notion.ico',
filepath + '/app/main/notion.ico')
print(
f' ...copying datastore wrapper ./resources/store.js to {filepath}/app/')
copyfile('./resources/store.js', filepath + '/app/store.js')
else:
print(
f' * {filepath}/app/main/main.js was not found: step skipped.')
print(f'\n{bold}>>> SUCCESSFULLY CUSTOMISED <<<{normal}')
except Exception as e:
print(f'\n{bold}### ERROR ###{normal}\n{str(e)}')
print(f'\n{bold}=== END OF LOG ==={normal}')

26
docs.json Normal file
View File

@ -0,0 +1,26 @@
{
"title": "notion enhancer",
"primary": "rgb(75, 133, 209)",
"git": "https://github.com/dragonwocky/notion-enhancer/blob/master/",
"footer": "[Edit on GitHub](__git__) // © 2020 dragonwocky & Uzver, under the [MIT license](https://choosealicense.com/licenses/mit/).",
"card": {
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
"url": "https://dragonwocky.me/notion-enhancer/"
},
"icon": {
"light": "web-logo.png"
},
"overwrite": true,
"exclude": ["cleaner.py", "customiser.py", "resources/*", ".gitignore"],
"nav": [
["index.html", "README.md"],
"resources",
["changelog.html", "CHANGELOG.md"],
[
"license",
"https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE"
],
["github", "https://github.com/dragonwocky/notion-enhancer/"],
["me (dragonwocky)", "https://dragonwocky.me/"]
]
}

22
docs/LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2020 TarasokUA
Copyright (c) 2020 dragonwocky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

93
docs/changelog.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html><!-- Documentative--><!-- (c) 2020 dragonwocky <thedragonring.bod@gmail.com>--><!-- (https://dragonwocky.me/) under the MIT license--><html prefix="og: http://ogp.me/ns#"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>changelog | notion enhancer</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Nunito+Sans"><link rel="stylesheet" href="docs.css"><script src="docs.js"></script><link rel="icon" href="web-logo.png" media="(prefers-color-scheme: dark)"><link rel="icon" href="web-logo.png"><meta name="title" content="changelog | notion enhancer"><meta name="description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta name="theme-color" content="rgb(75, 133, 209)"><meta property="og:type" content="article"><meta property="og:url" content="https://dragonwocky.me/notion-enhancer/changelog.html"><meta property="og:title" content="changelog"><meta property="og:site_name" content="notion enhancer"><meta property="og:description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta property="og:image" content="https://dragonwocky.me/notion-enhancer/web-logo.png"><meta property="twitter:card" content="summary"></head><body><aside class="menu"><div><div class="title"><h1>notion enhancer</h1><picture class="icon"><source srcset="web-logo.png" media="(prefers-color-scheme: dark)"><img src="web-logo.png"></picture></div></div><ul class="nav"><li class="entry"><a href="index.html">notion enhancer</a></li><li class="entry"><p>resources</p></li><li class="entry"><a href="#changelog">changelog</a><ul><li class="level-3"><a href="#v050-2020-05-23">v0.5.0 (2020-05-23)</a></li><li class="level-3"><a href="#v041-2020-02-13">v0.4.1 (2020-02-13)</a></li><li class="level-3"><a href="#v040">v0.4.0</a></li><li class="level-3"><a href="#v030">v0.3.0</a></li><li class="level-3"><a href="#v020">v0.2.0</a></li><li class="level-3"><a href="#v010">v0.1.0</a></li></ul></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE">license</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/">github</a></li><li class="entry"><a href="https://dragonwocky.me/">me (dragonwocky)</a></li></ul><p class="mark"><a href="https://dragonwocky.me/documentative">docs by documentative</a></p></aside><div class="wrapper"><div class="toggle"><button></button><h1>notion enhancer</h1></div><article class="documentative"><div class="content">
<section class="block" id="changelog">
<h1>
<a href="#changelog">changelog</a>
</h1>
<p>if something is <del>crossed out</del>, then it is no longer a feature included by default,
but can still easily be enabled by following instructions in the <a href="/index.html">docs</a>.</p>
</section>
<section class="block" id="v050-2020-05-23">
<h3>
<a href="#v050-2020-05-23">v0.5.0 (2020-05-23)</a>
</h3>
<ul>
<li>new: running from the wsl.</li>
<li>new: reload window with f5.</li>
<li>improved: code has been refactored and cleaned up,
inc. file renaming and a <code>customiser.py</code> that doesn&#39;t require
a run of <code>cleaner.py</code> to build updates.
improved: scrollbar colours that fit better with notion&#39;s theming.</li>
<li>bugfix: un-break having multiple notion windows open.</li>
</ul>
<p><em>(forked by <a href="https://github.com/dragonwocky">@dragonwocky</a>.)</em></p>
</section>
<section class="block" id="v041-2020-02-13">
<h3>
<a href="#v041-2020-02-13">v0.4.1 (2020-02-13)</a>
</h3>
<ul>
<li>bugfix: wider table &amp; the &quot;+&quot; button not working in database pages.</li>
</ul>
<blockquote>
<p>📥 <a href="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d239a3cf-d553-4ef3-ab04-8b47892d9f9a/Notion_Customization_v4.1.zip">notion-enhancer.v4.1.zip</a></p>
</blockquote>
</section>
<section class="block" id="v040">
<h3>
<a href="#v040">v0.4.0</a>
</h3>
<ul>
<li>new: tray icon.</li>
<li>new: app startup options (+ saving).</li>
<li>new: <code>Reset.py</code></li>
<li>improved: better output from <code>Customization Patcher.py</code>.</li>
<li>bugfix: wider tables in &quot;short page&quot; mode.</li>
<li>bugfix: unclickable buttons/draggable area (of titlebar).</li>
</ul>
</section>
<section class="block" id="v030">
<h3>
<a href="#v030">v0.3.0</a>
</h3>
<ul>
<li>new: show/hide window hotkey.</li>
<li>new: app startup options.</li>
<li><del>style: smaller table icons.</del></li>
</ul>
<blockquote>
<p>📥 <a href="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b01aa446-5727-476a-a25e-395472bfb1be/NotionScriptsV3.zip">notion-enhancer.v3.zip</a></p>
</blockquote>
</section>
<section class="block" id="v020">
<h3>
<a href="#v020">v0.2.0</a>
</h3>
<ul>
<li>new: light/dark theme support for window control buttons + scrollbars.</li>
<li>new: custom styles directly linked to the enhancer resources + compatible with web version.</li>
<li><del>improved: making table column width go below 100px.</del></li>
</ul>
</section>
<section class="block" id="v010">
<h3>
<a href="#v010">v0.1.0</a>
</h3>
<ul>
<li>new: custom window control buttons.</li>
<li>removed: default titlebar/menubar.</li>
<li><del>removed: huge padding of board view.</del></li>
<li><del>removed: huge padding of table view.</del></li>
<li><del>optional: making table column width go below 100px.</del></li>
<li><del>style: thinner cover image + higher content block.</del></li>
<li>style: scrollbars.</li>
</ul>
</section></div><footer class="footer"><hr><p><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/CHANGELOG.md">Edit on GitHub</a> // © 2020 dragonwocky &amp; Uzver, under the <a href="https://choosealicense.com/licenses/mit/">MIT license</a>.</p>
</footer><nav><a class="prev" href="index.html"></a></nav></article></div></body></html>

498
docs/docs.css Normal file
View File

@ -0,0 +1,498 @@
/*
* Documentative Styling
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (https://dragonwocky.me/) under the MIT license
*/
:root {
--primary: #4b85d1;
--absolute: #000;
--contrast: #fff;
--text: rgba(0, 0, 0, 0.84);
--link: var(--primary);
--grey: #6f6f6f;
--bg: #fbfcfc;
--box: #f2f3f4;
--code: #f7f9f9;
--button: #eee;
--border: #e5e7e9;
--shadow: #eee;
--glow: transparent;
--scroll: #e9e9e9;
--hover: #dedede;
--code-lang: #555;
--hljs-html: #000080;
--hljs-attr: #008080;
--hljs-obj: #2c426b;
--hljs-string: #d14;
--hljs-builtin: #0086b3;
--hljs-keyword: rgba(0, 0, 0, 0.84);
--hljs-selector: #900;
--hljs-type: #458;
--hljs-regex: #009926;
--hljs-symbol: #990073;
--hljs-meta: #999;
--hljs-comment: #707070;
--hljs-deletion: #e8b9b8;
--hljs-deletion-text: #4c232d;
--hljs-addition: #b9e0d3;
--hljs-addition-text: #1e4839;
}
@media (prefers-color-scheme: dark) {
:root {
--absolute: #fff;
--contrast: #000;
--text: #ddd;
--link: #a6c3e8;
--grey: #52555c;
--bg: #0e0f0f;
--box: #050505;
--code: #000;
--button: #2d2d2d;
--border: #2d2e2f;
--shadow: #070707;
--glow: var(--primary);
--scroll: #202225;
--hover: #36393f;
--code-lang: #ccc;
--hljs-html: #46db8c;
--hljs-attr: #dd1111;
--hljs-obj: #c6cbda;
--hljs-string: #abcdef;
--hljs-builtin: #b8528d;
/* bd1a79, 926956 */
--hljs-keyword: #2d8b59;
--hljs-comment: #a0a0a0;
--hljs-deletion: #4c232d;
--hljs-deletion-text: #e8b9b8;
--hljs-addition: #1e4839;
--hljs-addition-text: #b9e0d3;
}
}
* {
box-sizing: border-box;
word-break: break-word;
text-decoration: none;
text-size-adjust: 100%;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
color: var(--text);
background-color: var(--bg);
font-family: 'Nunito Sans', sans-serif;
}
::-webkit-scrollbar {
width: 2px;
height: 2px;
}
::-webkit-scrollbar-corner,
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--scroll);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--hover);
}
aside {
display: flex;
flex-direction: column;
background-color: var(--box);
overflow-x: auto;
}
aside .title {
display: flex;
flex-direction: row;
}
aside .title h1 {
font: 1.8em 'Source Code Pro', monospace;
margin: 0 0 1em 1.5rem;
padding: 1em 8px 2.5px 0;
letter-spacing: -2px;
border-bottom: 5px solid var(--primary);
color: var(--absolute);
}
aside .title .icon {
margin: auto 0.5em;
}
aside .title .icon img {
width: 2.5em;
margin: auto 0.5em;
}
aside > ul:first-child > li:first-child {
padding-top: 1em;
}
aside ul {
list-style-type: none;
padding-inline-start: 0;
margin: 0;
}
aside ul li p {
font-weight: bold;
letter-spacing: -0.5px;
margin-bottom: 0;
padding: 2px 1.3em;
font-size: 1.1em;
color: var(--hljs-comment);
}
aside ul li a {
color: var(--text);
padding-bottom: 0.1em 5em;
display: block;
padding: 2px 1.5em;
}
aside ul li a:hover,
aside ul li a:active {
background: var(--scroll);
}
aside ul li a.active {
color: var(--link);
font-weight: bold;
text-shadow: 0 0 0.75em var(--glow);
}
aside ul li.entry > a {
text-decoration: underline var(--border);
}
aside ul li.level-1 > a {
padding-left: 1.75em;
}
aside ul li.level-2 > a {
padding-left: calc(1.5em + calc(0.75em * 1));
}
aside ul li.level-3 > a {
padding-left: calc(1.5em + calc(0.75em * 2));
}
aside ul li.level-4 > a {
padding-left: calc(1.5em + calc(0.75em * 3));
}
aside ul li.level-5 > a {
padding-left: calc(1.5em + calc(0.75em * 4));
}
aside ul li.level-6 > a {
padding-left: calc(1.5em + calc(0.75em * 5));
}
aside .mark {
text-align: right;
margin-top: auto;
padding: 1.5em 1.5em 2px 1.5em;
font-size: 0.8em;
}
aside .mark a {
color: var(--grey);
}
.wrapper {
height: 100%;
width: 100%;
overflow-y: hidden;
}
.wrapper .documentative {
height: 100%;
overflow-y: auto;
padding: 0 1.5em;
padding-bottom: 4em;
display: flex;
flex-direction: column;
}
.wrapper .documentative .block {
margin: 1.5em;
word-wrap: break-word;
}
.wrapper .documentative .block:first-child {
margin: 0 1.5em 1.5em 1.5em;
}
.wrapper .documentative .example {
margin-top: 1em;
padding: 1em;
background-color: var(--box);
box-shadow: 0.4em 0.4em 1em var(--shadow);
}
.wrapper .documentative .example p:first-child {
margin-top: 0;
}
.wrapper .documentative .example p:last-child {
margin-bottom: 0;
}
.wrapper .documentative nav {
width: 75%;
position: fixed;
bottom: 1em;
right: 0;
pointer-events: none;
}
.wrapper .documentative nav .prev {
float: left;
padding-right: 0.13em;
}
.wrapper .documentative nav .next {
float: right;
padding-left: 0.13em;
}
.wrapper .documentative nav .prev,
.wrapper .documentative nav .next {
opacity: 1;
transition: opacity 200ms ease;
pointer-events: all;
border-radius: 50%;
width: 1.75em;
height: 1.75em;
margin: 0 1em;
font: 1.5em 'Source Code Pro', monospace;
line-height: 1.75em;
text-align: center;
color: var(--text);
text-shadow: none !important;
background-color: var(--button);
}
.wrapper .documentative .footer {
text-align: right;
color: var(--grey);
margin: auto 1.5em 0;
}
.wrapper .documentative .footer hr {
border-color: var(--grey);
}
.wrapper .documentative .footer a {
color: var(--grey);
font-weight: bold;
text-shadow: none;
text-decoration: dotted underline;
}
.wrapper .documentative h1,
.wrapper .documentative h2,
.wrapper .documentative h3,
.wrapper .documentative h4,
.wrapper .documentative h5,
.wrapper .documentative h6 {
margin: 0;
padding-top: 1em;
}
.wrapper .documentative h1 a,
.wrapper .documentative h2 a,
.wrapper .documentative h3 a,
.wrapper .documentative h4 a,
.wrapper .documentative h5 a,
.wrapper .documentative h6 a {
color: var(--text);
text-shadow: none;
}
.wrapper .documentative h1 {
padding-top: 1.5em;
}
.wrapper .documentative a {
color: var(--link);
text-shadow: 0 0 0.75em var(--glow);
}
.wrapper .documentative blockquote {
margin-left: 0;
padding-left: 1em;
border-left: 0.25em solid var(--border);
}
.wrapper .documentative h1 + table,
.wrapper .documentative h2 + table,
.wrapper .documentative h3 + table,
.wrapper .documentative h4 + table,
.wrapper .documentative h5 + table,
.wrapper .documentative h6 + table {
margin-top: 1em;
}
.wrapper .documentative table {
width: 100%;
border-collapse: collapse;
}
.wrapper .documentative table,
.wrapper .documentative th,
.wrapper .documentative td {
padding: 0.2em 0.7em;
border: 1px solid var(--border);
}
.wrapper .documentative code {
font-size: 0.8em;
background-color: var(--code);
overflow-x: auto;
position: relative;
display: block;
font-family: 'Source Code Pro', monospace;
}
.wrapper .documentative *:not(pre) > code {
padding: 0.275em 0.35em;
border-radius: 2px;
display: inline;
}
.wrapper .documentative pre {
position: relative;
}
.wrapper .documentative pre code {
padding: 1.8em;
border-radius: 5px;
position: static;
}
.wrapper .documentative pre code::before {
position: absolute;
right: 0;
top: 0;
color: var(--code-lang);
font-size: 0.65em;
padding: 0.5em 0.8em;
}
@media (min-width: 769px) {
body {
display: grid;
grid-template-columns: 25% 75%;
}
aside::-webkit-scrollbar-corner,
aside::-webkit-scrollbar-track {
background-color: var(--bg);
}
.toggle {
display: none;
}
}
@media (max-width: 768px) {
aside {
z-index: 1;
height: 100%;
display: flex;
position: fixed;
top: 0;
left: calc(4.5em - 100%);
width: calc(100% - 4.5em);
transition: left 300ms ease;
}
.wrapper {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
transition: left 300ms ease;
}
.wrapper .documentative {
flex-shrink: 1;
}
.wrapper .documentative nav {
width: 100%;
}
.wrapper .toggle {
display: flex;
flex-direction: row;
flex-shrink: 0;
padding: 0.8em 0;
background-color: var(--box);
}
.wrapper .toggle h1 {
letter-spacing: -2px;
font-size: 1.8em;
padding-top: 1.5px;
margin: auto 1.5rem auto 0;
}
.wrapper .toggle button {
font-size: 1.8em;
width: 2.5em;
margin: auto 0.5em;
color: var(--absolute);
border: none;
background: none;
text-align: center;
transition: transform 150ms ease;
-webkit-appearance: none;
-moz-appearance: none;
}
.wrapper .toggle button:hover,
.wrapper .toggle button:focus {
color: var(--text);
}
.wrapper .toggle button:active {
transform: scale(0.95);
}
.mobilemenu aside {
left: 0;
}
.mobilemenu .wrapper {
left: calc(100% - 4.75em);
}
.mobilemenu .wrapper .prev,
.mobilemenu .wrapper .next {
opacity: 0 !important;
pointer-events: none !important;
}
}
.hljs-subst {
color: var(--text);
}
.hljs-comment,
.hljs-quote {
color: var(--hljs-comment);
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--hljs-keyword);
font-weight: bold;
}
.hljs-attr {
color: var(--hljs-obj);
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: var(--hljs-attr);
}
.hljs-string,
.hljs-doctag {
color: var(--hljs-string);
}
.hljs-name,
.hljs-attribute {
color: var(--hljs-html);
}
.hljs-built_in,
.hljs-builtin-name {
color: var(--hljs-builtin);
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: var(--hljs-selector);
font-weight: bold;
}
.hljs-type,
.hljs-class .hljs-title {
color: var(--hljs-type);
font-weight: bold;
}
.hljs-regexp,
.hljs-link {
color: var(--hljs-regex);
}
.hljs-symbol,
.hljs-bullet {
color: var(--hljs-symbol);
}
.hljs-meta {
color: var(--hljs-meta);
font-weight: bold;
}
.hljs-deletion {
background: var(--hljs-deletion);
color: var(--hljs-deletion-text);
}
.hljs-addition {
background: var(--hljs-addition);
color: var(--hljs-addition-text);
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.documentative pre .lang-css::before { content: 'CSS'; }

193
docs/docs.js Normal file
View File

@ -0,0 +1,193 @@
/*
* Documentative
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (https://dragonwocky.me/) under the MIT license
*/
class Scrollnav {
constructor(menu, content, options) {
if (!(menu instanceof HTMLElement))
throw Error('scrollnav: invalid <menu> element provided');
if (!(content instanceof HTMLElement))
throw Error('scrollnav: invalid <content> element provided');
if (typeof options !== 'object') options = {};
if (Scrollnav.prototype.INITIATED)
throw Error('scrollnav: only 1 instance per page allowed!');
Scrollnav.prototype.INITIATED = true;
this.ID;
this.ticking = [];
this._menu = menu;
this._content = content;
this._links = [];
this._sections = [...this._menu.querySelectorAll('ul li a')].reduce(
(list, link) => {
if (!link.getAttribute('href').startsWith('#')) return list;
let section = this._content.querySelector(link.getAttribute('href'));
if (!section) return list;
this._links.push(link);
link.onclick = async ev => {
ev.preventDefault();
const ID = link.getAttribute('href');
this.highlightHeading(ID);
this.scrollContent(ID);
this.setHash(ID);
};
return [...list, section];
},
[]
);
this._topheading = `#${this._sections[0].id}`;
window.onhashchange = this.watchHash.bind(this);
this._content.addEventListener('scroll', ev => {
if (!this.ticking.length) {
this.ticking.push(1);
requestAnimationFrame(() => {
this.watchScroll(ev);
this.ticking.pop();
});
}
});
this.set(null, false);
return this;
}
set(ID, smooth) {
this.highlightHeading(ID);
this.scrollMenu(ID, smooth);
this.scrollContent(ID, smooth);
this.setHash(ID);
}
parseID(ID) {
if (!ID || typeof ID !== 'string') ID = location.hash || this._topheading;
if (!ID.startsWith('#')) ID = `#${ID}`;
if (!this._links.find(el => el.getAttribute('href') === ID))
ID = this._topheading;
this.ID = ID;
return ID;
}
highlightHeading(ID) {
this.parseID(ID);
this._links.forEach(el =>
el.getAttribute('href') === this.ID
? el.classList.add('active')
: el.classList.remove('active')
);
return true;
}
watchHash(ev) {
ev.preventDefault();
if (ev.newURL !== ev.oldURL) {
this.set();
}
}
setHash(ID) {
if (!history.replaceState) return false;
this.parseID(ID);
history.replaceState(null, null, ID === this._topheading ? '#' : this.ID);
return true;
}
scrollContent(ID, smooth = true) {
this.ticking.push(1);
this.parseID(ID);
let offset = this._sections.find(el => `#${el.id}` === this.ID).offsetTop;
if (offset < this._content.clientHeight / 2) offset = 0;
this._content.scroll({
top: offset,
behavior: smooth ? 'smooth' : 'auto'
});
setTimeout(() => this.ticking.pop(), 1000);
return true;
}
scrollMenu(ID, smooth = true) {
this.parseID(ID);
let offset = this._links.find(el => el.getAttribute('href') === this.ID)
.offsetTop;
if (offset < this._menu.clientHeight / 2) offset = 0;
this._menu.scroll({
top: offset,
behavior: smooth ? 'smooth' : 'auto'
});
return true;
}
watchScroll(ev) {
const viewport = this._content.clientHeight,
ID = this._sections.reduce(
(carry, el) => {
const rect = el.getBoundingClientRect(),
height = rect.bottom - rect.top,
visible = {
top: rect.top >= 0 && rect.top < viewport,
bottom: rect.bottom > 0 && rect.top < viewport
};
let pixels = 0;
if (visible.top && visible.bottom) {
pixels = height; // whole el
} else if (visible.top) {
pixels = viewport - rect.top;
} else if (visible.bottom) {
pixels = rect.bottom;
} else if (height > viewport && rect.top < 0) {
const absolute = Math.abs(rect.top);
if (absolute < height) pixels = height - absolute; // part of el
}
pixels = (pixels / height) * 100;
return pixels > carry[0] ? [pixels, el] : carry;
},
[0, null]
)[1].id;
this.ID = ID;
this.scrollMenu(this.ID);
clearTimeout(this.afterScroll);
this.afterScroll = setTimeout(
() => void (this.highlightHeading(this.ID) && this.setHash(this.ID)),
100
);
}
}
let constructed = false;
const construct = () => {
if (document.readyState !== 'complete' || constructed) return false;
constructed = true;
if (
location.pathname.endsWith('index.html') &&
window.location.protocol === 'https:'
)
location.replace('./' + location.hash);
new Scrollnav(
document.querySelector('aside'),
document.querySelector('.documentative')
);
document.querySelector('.toggle button').onclick = () =>
document.body.classList.toggle('mobilemenu');
if (window.matchMedia) {
let prev;
const links = [...document.head.querySelectorAll('link[rel*="icon"]')],
pointer = document.createElement('link');
pointer.setAttribute('rel', 'icon');
document.head.appendChild(pointer);
setInterval(() => {
const match = links.find(link => window.matchMedia(link.media).matches);
if (!match || match.media === prev) return;
prev = match.media;
pointer.setAttribute('href', match.getAttribute('href'));
}, 500);
links.forEach(link => document.head.removeChild(link));
}
};
construct();
document.addEventListener('readystatechange', construct);

282
docs/index.html Normal file
View File

@ -0,0 +1,282 @@
<!DOCTYPE html><!-- Documentative--><!-- (c) 2020 dragonwocky <thedragonring.bod@gmail.com>--><!-- (https://dragonwocky.me/) under the MIT license--><html prefix="og: http://ogp.me/ns#"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>notion enhancer</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Nunito+Sans"><link rel="stylesheet" href="docs.css"><script src="docs.js"></script><link rel="icon" href="web-logo.png" media="(prefers-color-scheme: dark)"><link rel="icon" href="web-logo.png"><meta name="title" content="notion enhancer"><meta name="description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta name="theme-color" content="rgb(75, 133, 209)"><meta property="og:type" content="article"><meta property="og:url" content="https://dragonwocky.me/notion-enhancer/index.html"><meta property="og:title" content="notion enhancer"><meta property="og:site_name" content="notion enhancer"><meta property="og:description" content="an enhancer/customiser for the all-in-one productivity workspace notion.so"><meta property="og:image" content="https://dragonwocky.me/notion-enhancer/web-logo.png"><meta property="twitter:card" content="summary"></head><body><aside class="menu"><div><div class="title"><h1>notion enhancer</h1><picture class="icon"><source srcset="web-logo.png" media="(prefers-color-scheme: dark)"><img src="web-logo.png"></picture></div></div><ul class="nav"><li class="entry"><a href="#notion-enhancer">notion enhancer</a><ul><li class="level-2"><a href="#installation">installation</a></li><li class="level-2"><a href="#this-is-a-fork">this is a fork</a></li><li class="level-2"><a href="#features">features</a></li><li class="level-3"><a href="#titlebar">titlebar</a></li><li class="level-3"><a href="#nicer-scrollbars">nicer scrollbars</a></li><li class="level-3"><a href="#hotkeys">hotkeys</a></li><li class="level-3"><a href="#tray">tray</a></li><li class="level-3"><a href="#styling">styling</a></li><li class="level-4"><a href="#wider-page-view">wider page view</a></li><li class="level-4"><a href="#thinner-cover-image">thinner cover image</a></li><li class="level-4"><a href="#table-columns-below-100px">table columns below 100px</a></li><li class="level-4"><a href="#hide--new-table-row">hide '+ new' table row</a></li><li class="level-4"><a href="#hide-calculations-table-row">hide calculations table row</a></li><li class="level-4"><a href="#hide--new-board-row">hide '+ new' board row</a></li><li class="level-4"><a href="#hide-board-view-hidden-columns">hide board view hidden columns</a></li><li class="level-4"><a href="#hide-board-view-add-a-group">hide board view 'add a group'</a></li><li class="level-4"><a href="#centre-align-table-column-headers">centre-align table column headers</a></li><li class="level-4"><a href="#smaller-table-column-header-icons">smaller table column header icons</a></li><li class="level-4"><a href="#remove-icons-from-table-column-headers">remove icons from table column headers</a></li><li class="level-4"><a href="#removingdecreasing-side-padding-for-tables">removing/decreasing side padding for tables</a></li><li class="level-4"><a href="#removingdecreasing-side-padding-for-boards">removing/decreasing side padding for boards</a></li><li class="level-2"><a href="#other-details">other details</a></li></ul></li><li class="entry"><p>resources</p></li><li class="entry"><a href="changelog.html">changelog</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/LICENSE">license</a></li><li class="entry"><a href="https://github.com/dragonwocky/notion-enhancer/">github</a></li><li class="entry"><a href="https://dragonwocky.me/">me (dragonwocky)</a></li></ul><p class="mark"><a href="https://dragonwocky.me/documentative">docs by documentative</a></p></aside><div class="wrapper"><div class="toggle"><button></button><h1>notion enhancer</h1></div><article class="documentative"><div class="content">
<section class="block" id="notion-enhancer">
<h1>
<a href="#notion-enhancer">notion enhancer</a>
</h1>
<p>an enhancer/customiser for the all-in-one productivity workspace <a href="https://www.notion.so/">notion.so</a></p>
</section>
<section class="block" id="installation">
<h2>
<a href="#installation">installation</a>
</h2>
<p>currently, only win10 is supported. it is possible to run this script via the wsl to modify the win10 notion app.</p>
<p>(the <a href="#styling">styles</a> should also work for the web version.
these can be installed via an extension like <a href="https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=en">stylus</a>
or a built-in feature like <a href="https://www.userchrome.org/">userChrome.css</a>.)</p>
<ol>
<li>install <a href="https://nodejs.org/en/">node.js</a> (if using the wsl, it is recommended to install via <a href="https://github.com/nvm-sh/nvm#install--update-script">nvm</a>).</li>
<li>install <a href="https://www.python.org/">python</a> (if using the wsl, follow <a href="https://docs.python-guide.org/starting/install3/linux/">this guide</a>).</li>
<li>reboot.</li>
<li>in cmd (on windows) or bash (with wsl), run <code>npm install -g asar</code> (check installation by running <code>asar</code>).</li>
<li><a href="https://github.com/dragonwocky/notion-enhancer/archive/master.zip">download this enhancer</a> &amp; extract to a location it can safely remain (this must be in the windows filesystem,
even if you are running the script from the wsl).</li>
<li>ensure notion is closed.</li>
<li>optional: to remove previous versions of notion enhancer, run <code>cleaner.py</code></li>
<li>optional: modify the <code>resources/user.css</code> files to your liking.</li>
<li>run <code>customiser.py</code> to build changes.</li>
</ol>
<p>done: run notion and enjoy.</p>
<p><strong>oh no, now my app won&#39;t open!</strong></p>
<ol>
<li>kill any notion tasks in the task manager (<code>ctrl+shift+esc</code>).</li>
<li>run <code>cleaner.py</code>.</li>
<li>reboot.</li>
<li>follow instructions above (ensuring notion <em>isn&#39;t</em> running! again, check task manager).</li>
</ol>
</section>
<section class="block" id="this-is-a-fork">
<h2>
<a href="#this-is-a-fork">this is a fork</a>
</h2>
<p>credit where credit is due, this was originally made by Uzver (github: <a href="https://github.com/TarasokUA">@TarasokUA</a>,
telegram: <a href="https://t.me/UserFromUkraine">UserFromUkraine</a>, discord: Uzver#8760).</p>
<p>he has approved my go-ahead with this fork, as he himself no longer wishes to continue development on the project.</p>
</section>
<section class="block" id="features">
<h2>
<a href="#features">features</a>
</h2>
</section>
<section class="block" id="titlebar">
<h3>
<a href="#titlebar">titlebar</a>
</h3>
<p>default windows titlebar/frame has been replaced by one more fitting to the theme of the app.</p>
<p>this includes the addition of an extra button, &quot;always on top&quot;
symbolised with an arrow (4th from the right). when toggled to point up,
notion will remain the top visible window even if not focused.</p>
<p>to customise which characters are used for these buttons, open in the <code>resources/preload.js</code> file,
find the relevant button (read the comments) and replace its icon with your chosen unicode character (e.g.
replacing <code>element.innerHTML = &#39;&#39;;</code> with <code>element.innerHTML = &#39;🙄&#39;;</code>).</p>
</section>
<section class="block" id="nicer-scrollbars">
<h3>
<a href="#nicer-scrollbars">nicer scrollbars</a>
</h3>
<p>i mean, yeah. get rid of those ugly default scrollbars and use nice inconspicuous
ones that actually look as if they&#39;re part of notion.</p>
<p>to add these to the web version, copy lines 44 - 75 from <code>user.css</code> into your css customiser.</p>
</section>
<section class="block" id="hotkeys">
<h3>
<a href="#hotkeys">hotkeys</a>
</h3>
<ul>
<li><strong>reload window</strong>: in addition to the built-in <code>CmdOrCtrl+R</code> reload,
you can now reload a window with <code>F5</code>.</li>
<li><strong>toggle all notion windows to/from the tray</strong>: <code>CmdOrCtrl+Shift+A</code> by default.</li>
</ul>
<p>to set your own toggle hotkey, open <code>customiser.py</code> and change line 16 (<code>hotkey = &#39;CmdOrCtrl+Shift+A&#39;</code>)
to your preference. you will need to run or re-run <code>customiser.py</code> afterwards.</p>
</section>
<section class="block" id="tray">
<h3>
<a href="#tray">tray</a>
</h3>
<ul>
<li>single-click to toggle app visibility. right click to open menu.</li>
<li>settings will be saved in <code>%localappdata%/Programs/Notion/resources/app/user-preferences.json</code></li>
<li><strong>run on startup</strong>: run notion on boot/startup. (default: true)</li>
<li><strong>hide on open</strong>: hide the launch of notion to the tray. (default: false)</li>
<li><strong>open maximised</strong>: maximise the app on open. (default: false)</li>
<li><strong>close to tray</strong>: close window to tray rather than closing outright
on click of <code></code>. does not apply if multiple notion windows are open. (default: false)</li>
</ul>
</section>
<section class="block" id="styling">
<h3>
<a href="#styling">styling</a>
</h3>
<p>due to <code>customiser.py</code> setting up a direct link to <code>resources/user.css</code>,
changes will be applied instantly on notion reload
(no need to re-run <code>customiser.py</code> every time you want to change some styles).</p>
<p>these should also work for the web version, if copied into your css customiser.</p>
<p>css below will work for every instance of the element, but if you wish to hide only a specific element
(e.g. the &#39;+ new&#39; table row) it is recommended that you prepend each selector with <code>[data-block-id=&#39;ID&#39;]</code> (<a href="https://www.youtube.com/watch?v=6V7eqShm_4w">video tutorial on fetching IDs</a>).</p>
</section>
<section class="block" id="wider-page-view">
<h4>
<a href="#wider-page-view">wider page view</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-peek-renderer</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(2)</span> {
<span class="hljs-attribute">max-width</span>: <span class="hljs-number">85vw</span> <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="thinner-cover-image">
<h4>
<a href="#thinner-cover-image">thinner cover image</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'</span>]</span> {
<span class="hljs-attribute">height</span>: <span class="hljs-number">12vh</span> <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; height: 30vh;'</span>]</span>
<span class="hljs-selector-tag">img</span> {
<span class="hljs-attribute">height</span>: <span class="hljs-number">20vh</span> <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="table-columns-below-100px">
<h4>
<a href="#table-columns-below-100px">table columns below 100px</a>
</h4>
<p><strong>not recommended!</strong> this is unreliable and will cause bugs.
coincidentally, this is also what the youtube video linked above shows how to do.
as it is a per-table-column style, unlike all others here, it must be prepended with the block ID.</p>
<pre><code class="lang-css"><span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
&gt; <span class="hljs-selector-attr">[style^=<span class="hljs-string">'display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);'</span>]</span>
&gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>
&gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span>
&gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>,
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
&gt; <span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: relative; min-width: calc(100% - 192px);'</span>]</span>
&gt; <span class="hljs-selector-attr">[data-block-id]</span>
&gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span>,
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(5)</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(10)</span> {
<span class="hljs-attribute">width</span>: <span class="hljs-number">45px</span> <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-attr">[data-block-id^=<span class="hljs-string">'ID'</span>]</span>
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'position: absolute; top: 0px; left: 0px; pointer-events: none;'</span>]</span><span class="hljs-selector-pseudo">:not(.notion-presence-container)</span> {
<span class="hljs-attribute">display</span>: none;
}</code></pre>
</section>
<section class="block" id="hide--new-table-row">
<h4>
<a href="#hide--new-table-row">hide '+ new' table row</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-add-row</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="hide-calculations-table-row">
<h4>
<a href="#hide-calculations-table-row">hide calculations table row</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-add-row</span> + <span class="hljs-selector-tag">div</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="hide--new-board-row">
<h4>
<a href="#hide--new-board-row">hide '+ new' board row</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-group</span>
<span class="hljs-selector-attr">[style=<span class="hljs-string">'user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap; height: 32px; border-radius: 3px; font-size: 14px; line-height: 1.2; min-width: 0px; padding-left: 6px; padding-right: 8px; color: rgba(255, 255, 255, 0.4); width: 100%;'</span>]</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="hide-board-view-hidden-columns">
<h4>
<a href="#hide-board-view-hidden-columns">hide board view hidden columns</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> &gt; <span class="hljs-selector-attr">[data-block-id]</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-last-child(2)</span>,
<span class="hljs-selector-class">.notion-board-view</span> &gt; <span class="hljs-selector-attr">[data-block-id]</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:first-child</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-last-child(2)</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="hide-board-view-add-a-group">
<h4>
<a href="#hide-board-view-add-a-group">hide board view 'add a group'</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> &gt; <span class="hljs-selector-attr">[data-block-id]</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:last-child</span>,
<span class="hljs-selector-class">.notion-board-view</span> &gt; <span class="hljs-selector-attr">[data-block-id]</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:first-child</span> &gt; <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:last-child</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="centre-align-table-column-headers">
<h4>
<a href="#centre-align-table-column-headers">centre-align table column headers</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-header-cell</span> &gt; <span class="hljs-selector-tag">div</span> &gt; <span class="hljs-selector-tag">div</span> {
<span class="hljs-attribute">margin</span>: <span class="hljs-number">0px</span> auto;
}</code></pre>
</section>
<section class="block" id="smaller-table-column-header-icons">
<h4>
<a href="#smaller-table-column-header-icons">smaller table column header icons</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'display: flex; position: absolute; background: rgb(47, 52, 55); z-index: 82; height: 33px; color: rgba(255, 255, 255, 0.6);'</span>]</span>
<span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:nth-child(1)</span>
<span class="hljs-selector-tag">svg</span> {
<span class="hljs-attribute">height</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
<span class="hljs-attribute">width</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
<span class="hljs-attribute">margin-right</span>: -<span class="hljs-number">4px</span>;
}</code></pre>
</section>
<section class="block" id="remove-icons-from-table-column-headers">
<h4>
<a href="#remove-icons-from-table-column-headers">remove icons from table column headers</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-table-view-header-cell</span> <span class="hljs-selector-attr">[style^=<span class="hljs-string">'margin-right: 6px;'</span>]</span> {
<span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="removingdecreasing-side-padding-for-tables">
<h4>
<a href="#removingdecreasing-side-padding-for-tables">removing/decreasing side padding for tables</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-attr">[style^=<span class="hljs-string">'flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;'</span>]</span>
<span class="hljs-selector-class">.notion-table-view</span>,
<span class="hljs-selector-attr">[class=<span class="hljs-string">'notion-scroller'</span>]</span> &gt; <span class="hljs-selector-class">.notion-table-view</span> {
<span class="hljs-attribute">padding-left</span>: <span class="hljs-number">35px</span> <span class="hljs-meta">!important</span>;
<span class="hljs-attribute">padding-right</span>: <span class="hljs-number">15px</span> <span class="hljs-meta">!important</span>;
<span class="hljs-attribute">min-width</span>: <span class="hljs-number">0%</span> <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-attr">[style^=<span class="hljs-string">'flex-shrink: 0; flex-grow: 1; width: 100%; max-width: 100%; display: flex; align-items: center; flex-direction: column; font-size: 16px; color: rgba(255, 255, 255, 0.9); padding: 0px 96px 30vh;'</span>]</span>
<span class="hljs-selector-class">.notion-selectable</span>
<span class="hljs-selector-class">.notion-scroller</span><span class="hljs-selector-class">.horizontal</span><span class="hljs-selector-pseudo">::-webkit-scrollbar-track</span> {
<span class="hljs-attribute">margin-left</span>: <span class="hljs-number">10px</span>;
<span class="hljs-attribute">margin-right</span>: <span class="hljs-number">10px</span>;
}</code></pre>
</section>
<section class="block" id="removingdecreasing-side-padding-for-boards">
<h4>
<a href="#removingdecreasing-side-padding-for-boards">removing/decreasing side padding for boards</a>
</h4>
<pre><code class="lang-css"><span class="hljs-selector-class">.notion-board-view</span> {
<span class="hljs-attribute">padding-left</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
<span class="hljs-attribute">padding-right</span>: <span class="hljs-number">10px</span> <span class="hljs-meta">!important</span>;
}</code></pre>
</section>
<section class="block" id="other-details">
<h2>
<a href="#other-details">other details</a>
</h2>
<p>i have an unhealthy habit of avoiding capital letters. nothing enforces this, i just do it.</p>
<p>the notion logo belongs entirely to the notion team, and was sourced from their
<a href="https://www.notion.so/Media-Kit-205535b1d9c4440497a3d7a2ac096286">media kit</a>.</p>
<p>if you have any questions, check <a href="https://dragonwocky.me/">my website</a> for contact details.</p>
</section></div><footer class="footer"><hr><p><a href="https://github.com/dragonwocky/notion-enhancer/blob/master/README.md">Edit on GitHub</a> // © 2020 dragonwocky &amp; Uzver, under the <a href="https://choosealicense.com/licenses/mit/">MIT license</a>.</p>
</footer><nav><a class="next" href="changelog.html"></a></nav></article></div></body></html>

BIN
docs/web-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

9
docs/web-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

155
package-lock.json generated
View File

@ -1,155 +0,0 @@
{
"name": "notion-enhancer",
"version": "0.11.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "notion-enhancer",
"version": "0.11.1",
"license": "MIT",
"dependencies": {
"@electron/asar": "^3.2.9",
"arg": "^5.0.2",
"chalk-template": "^1.1.0"
},
"bin": {
"notion-enhancer": "bin.mjs"
},
"engines": {
"node": ">=18.x.x"
},
"funding": {
"url": "https://github.com/sponsors/dragonwocky"
}
},
"node_modules/@electron/asar": {
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz",
"integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
},
"bin": {
"asar": "bin/asar.js"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/arg": {
"version": "5.0.2",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/chalk": {
"version": "5.2.0",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"chalk": "^5.2.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/commander": {
"version": "5.1.0",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"license": "MIT"
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"license": "ISC"
},
"node_modules/glob": {
"version": "7.2.3",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "3.1.2",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/once": {
"version": "1.4.0",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"license": "ISC"
}
}
}

View File

@ -1,42 +0,0 @@
{
"name": "notion-enhancer",
"version": "0.11.1",
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
"description": "Customise the all-in-one productivity workspace Notion.",
"homepage": "https://notion-enhancer.github.io",
"repository": "github:notion-enhancer/desktop",
"bugs": "https://github.com/notion-enhancer/desktop/issues",
"funding": "https://github.com/sponsors/dragonwocky",
"license": "MIT",
"bin": "bin.mjs",
"type": "module",
"scripts": {
"build": "./scripts/build-browser-extension.sh",
"vendor": "node ./scripts/vendor-dependencies.mjs"
},
"engines": {
"node": ">=18.x.x"
},
"keywords": [
"windows",
"macos",
"linux",
"productivity",
"hack",
"extensions",
"themes",
"integrations",
"addons",
"mod",
"mods",
"mod-loader",
"enhancer",
"notion",
"notion-enhancer"
],
"dependencies": {
"@electron/asar": "^3.2.9",
"chalk-template": "^1.1.0",
"arg": "^5.0.2"
}
}

109
resources/hotkey.js Normal file
View File

@ -0,0 +1,109 @@
/* === INJECTION MARKER === */
/*
* Notion Enhancer
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (c) 2020 TarasokUA
* (https://dragonwocky.me/) under the MIT license
*/
// adds: tray support (inc. context menu with settings), window toggle hotkey
// DO NOT REMOVE THE INJECTION MARKER ABOVE.
// DO NOT CHANGE THE NAME OF THE 'enhancements()' FUNCTION.
let tray;
function enhancements() {
const { Tray, Menu } = require('electron'),
path = require('path'),
store = new (require(path.join(__dirname, '..', 'store.js')))({
config: 'user-preferences',
defaults: {
openhidden: false,
maximised: false,
tray: false
}
}),
states = {
startup: electron_1.app.getLoginItemSettings().openAtLogin,
openhidden: store.get('openhidden'),
maximised: store.get('maximised'),
tray: store.get('tray')
};
tray = new Tray(path.join(__dirname, './notion.ico'));
const contextMenu = Menu.buildFromTemplate([
{
id: 'startup',
label: 'run on startup',
type: 'checkbox',
checked: states.startup,
click: () =>
contextMenu.getMenuItemById('startup').checked
? electron_1.app.setLoginItemSettings({ openAtLogin: true })
: electron_1.app.setLoginItemSettings({ openAtLogin: false })
},
{
id: 'openhidden',
label: 'hide on open',
type: 'checkbox',
checked: states.openhidden,
click: () =>
contextMenu.getMenuItemById('openhidden').checked
? store.set('openhidden', true)
: store.set('openhidden', false)
},
{
id: 'maximised',
label: 'open maximised',
type: 'checkbox',
checked: states.maximised,
click: () =>
contextMenu.getMenuItemById('maximised').checked
? store.set('maximised', true)
: store.set('maximised', false)
},
{
id: 'tray',
label: 'close to tray',
type: 'checkbox',
checked: states.tray,
click: () =>
contextMenu.getMenuItemById('tray').checked
? store.set('tray', true)
: store.set('tray', false)
},
{
type: 'separator'
},
{
label: '(x) quit',
role: 'quit'
}
]);
tray.setContextMenu(contextMenu);
tray.on('click', function () {
const win = electron_1.BrowserWindow.getAllWindows()[0];
if (win.isVisible()) {
if (win.isMinimized()) {
win.show();
} else win.hide();
} else {
if (contextMenu.getMenuItemById('maximised').checked) {
win.maximize();
} else win.show();
}
});
const hotkey = '___hotkey___'; // will be set by python script
electron_1.globalShortcut.register(hotkey, () => {
const windows = electron_1.BrowserWindow.getAllWindows();
if (windows.some(win => !win.isVisible())) {
if (contextMenu.getMenuItemById('maximised').checked) {
windows.forEach(win => win.maximize());
} else windows.forEach(win => win.show());
} else windows.forEach(win => win.hide());
});
}

BIN
resources/notion.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

94
resources/preload.js Normal file
View File

@ -0,0 +1,94 @@
/* === INJECTION MARKER === */
/*
* Notion Enhancer
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (c) 2020 TarasokUA
* (https://dragonwocky.me/) under the MIT license
*/
// adds: custom styles, nicer window control buttons
// DO NOT REMOVE THE INJECTION MARKER ABOVE
require('electron').remote.getGlobal('setTimeout')(() => {
/* style injection */
const fs = require('fs'),
css = fs.readFileSync('___user.css___'), // will be set by python script
style = document.createElement('style'),
head = document.getElementsByTagName('head')[0];
if (!head) return;
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
const intervalID = setInterval(injection, 100);
function injection() {
if (document.querySelector('div.notion-topbar > div') == undefined) return;
const appwindow = require('electron').remote.getCurrentWindow();
/* window control buttons */
let node = document.querySelector('div.notion-topbar > div'),
element = document.createElement('div');
element.id = 'window-buttons-area';
node.appendChild(element);
node = document.querySelector('#window-buttons-area');
// always-on-top
element = document.createElement('button');
element.classList.add('window-buttons');
element.innerHTML = '🠛';
element.onclick = function () {
const state = appwindow.isAlwaysOnTop();
appwindow.setAlwaysOnTop(!state);
this.innerHTML = state ? '🠛' : '🠙';
};
node.appendChild(element);
// minimise
element = document.createElement('button');
element.classList.add('window-buttons');
element.innerHTML = '⚊';
element.onclick = () => appwindow.minimize();
node.appendChild(element);
// maximise
element = document.createElement('button');
element.classList.add('window-buttons');
element.innerHTML = '▢';
element.onclick = () =>
appwindow.isMaximized() ? appwindow.unmaximize() : appwindow.maximize();
node.appendChild(element);
// close
const path = require('path');
element = document.createElement('button');
element.classList.add('window-buttons');
element.innerHTML = '⨉';
element.onclick = () => {
const store = new (require(path.join(__dirname, '..', 'store.js')))({
config: 'user-preferences',
defaults: {
tray: false
}
});
if (
store.get('tray') &&
require('electron').remote.BrowserWindow.getAllWindows().length === 1
) {
appwindow.hide();
} else appwindow.close();
};
node.appendChild(element);
clearInterval(intervalID);
/* reload window */
document.defaultView.addEventListener(
'keyup',
ev => void (ev.code === 'F5' ? appwindow.reload() : 0),
true
);
}
}, 100);

35
resources/store.js Normal file
View File

@ -0,0 +1,35 @@
/*
* Notion Enhancer
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (c) 2020 TarasokUA
* (https://dragonwocky.me/) under the MIT license
*/
// a wrapper for accessing data stored in a JSON file
const path = require('path'),
fs = require('fs');
class Store {
constructor(opts) {
this.path = path.join(__dirname, opts.config + '.json');
this.data = parseDataFile(this.path, opts.defaults);
}
get(key) {
return this.data[key];
}
set(key, val) {
this.data[key] = val;
fs.writeFileSync(this.path, JSON.stringify(this.data));
}
}
function parseDataFile(path, defaults) {
try {
return JSON.parse(fs.readFileSync(path));
} catch (error) {
return defaults;
}
}
module.exports = Store;

75
resources/user.css Normal file
View File

@ -0,0 +1,75 @@
/*
* Notion Enhancer
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com>
* (c) 2020 TarasokUA
* (https://dragonwocky.me/) under the MIT license
*/
/* window control buttons: block */
#window-buttons-area {
padding-left: 14px;
user-select: none;
}
/* window control buttons: light theme */
.notion-light-theme .window-buttons {
background: rgb(255, 255, 255);
color: black;
border: 0;
margin: 0px 0px 0px 9px;
width: 32px;
line-height: 26px;
border-radius: 4px;
font-size: 16px;
transition-duration: 0.2s;
font-weight: bold;
}
.notion-light-theme .window-buttons:hover {
background: rgb(239, 239, 239);
}
/* window control buttons: dark theme */
.notion-dark-theme .window-buttons {
background: rgb(47, 52, 55);
border: 0;
margin: 0px 0px 0px 9px;
width: 32px;
line-height: 26px;
border-radius: 4px;
font-size: 16px;
transition-duration: 0.2s;
}
.notion-dark-theme .window-buttons:hover {
background: rgb(71, 76, 80);
}
/* scrollbar: pointer */
.notion-scroller {
cursor: auto;
}
/* scrollbar: size */
::-webkit-scrollbar {
width: 8px; /* for vertical */
height: 8px; /* for horizontal */
}
/* scrollbar: light theme */
.notion-light-theme ::-webkit-scrollbar-corner {
background-color: transparent; /* for overlap */
}
.notion-light-theme ::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #d9d8d6;
border: 1px solid #cacac8;
}
.notion-light-theme ::-webkit-scrollbar-thumb:hover {
background: #cacac8;
}
/* scrollbar: dark theme */
.notion-dark-theme ::-webkit-scrollbar-corner {
background-color: transparent; /* for overlap */
}
.notion-dark-theme ::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #505457;
}
.notion-dark-theme ::-webkit-scrollbar-thumb:hover {
background: #696d6f;
}

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
version=$(node -p "require('./package.json').version")
cd src
mkdir -p ../dist
rm -f "../dist/notion-enhancer-$version.zip"
zip -r9 "../dist/notion-enhancer-$version.zip" .

View File

@ -1,160 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import os from "node:os";
import fsp from "node:fs/promises";
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { execSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import asar from "@electron/asar";
import patch from "./patch-desktop-app.mjs";
const nodeRequire = createRequire(import.meta.url),
platform =
process.platform === "linux" &&
os.release().toLowerCase().includes("microsoft")
? "wsl"
: process.platform,
getEnv = (name) => {
if (platform !== "wsl" || process.env[name]) return process.env[name];
// read windows environment variables and convert
// windows paths to paths mounted in the wsl fs
const pipe = { encoding: "utf8", stdio: "pipe" },
value = execSync(`cmd.exe /c echo %${name}%`, pipe).trim(),
isAbsolutePath = /^[a-zA-Z]:[\\\/]/.test(value),
isSystemPath = /^[\\\/]/.test(value);
if (isAbsolutePath) {
// e.g. C:\Program Files
const drive = value[0].toLowerCase(),
path = value.slice(2).replace(/\\/g, "/");
process.env[name] = `/mnt/${drive}${path}`;
} else if (isSystemPath) {
// e.g. \Program Files
const drive = getEnv("SYSTEMDRIVE")[0].toLowerCase(),
path = value.replace(/\\/g, "/");
process.env[name] = `/mnt/${drive}${path}`;
} else process.env[name] = value;
return process.env[name];
};
let __notionResources;
const setNotionPath = (path) => {
// sets notion resource path to user provided value
// e.g. with the --path cli option
__notionResources = path;
},
getResourcePath = (...paths) => {
if (__notionResources) return resolve(__notionResources, ...paths);
// prettier-ignore
for (const [platforms, notionResources] of [
[['win32', 'wsl'], resolve(`${getEnv("LOCALAPPDATA")}/Programs/Notion/resources`)],
[['win32', 'wsl'], resolve(`${getEnv("PROGRAMW6432")}/Notion/resources`)],
[['darwin'], `/Users/${getEnv("USER")}/Applications/Notion.app/Contents/Resources`],
[['darwin'], "/Applications/Notion.app/Contents/Resources"],
[['linux'], "/opt/notion-app"],
]) {
if (!platforms.includes(platform)) continue;
if (!existsSync(notionResources)) continue;
__notionResources = notionResources;
return resolve(__notionResources, ...paths);
}
},
extractFile = (path) => {
const archive = getResourcePath("app.asar");
return asar.extractFile(archive, path);
};
const getInsertPath = (...paths) => {
return "node_modules/notion-enhancer/" + paths.join("/");
},
getInsertVersion = () => {
try {
const manifest = extractFile(getInsertPath("package.json")).toString();
return JSON.parse(manifest).version;
} catch {
return null;
}
};
const backupApp = async () => {
const archive = getResourcePath("app.asar");
if (!existsSync(archive)) return false;
await fsp.cp(archive, archive + ".bak");
return true;
},
restoreApp = async () => {
const archive = getResourcePath("app.asar");
if (!existsSync(archive + ".bak")) return false;
await fsp.rename(archive + ".bak", archive);
return true;
},
enhanceApp = async (debug = false, directoryMode = false) => {
const app = getResourcePath("app"),
archive = getResourcePath("app.asar");
// directory mode acts on pre-extracted sources
// as part of the notion-repackaged build process
if (directoryMode) {
if (!existsSync(app)) return false;
for (let file of await fsp.readdir(app, { recursive: true })) {
file = file.replace(/^\//g, "");
const appPath = resolve(app, file),
stat = await fsp.stat(appPath);
if (stat.isFile()) {
const content = await fsp.readFile(appPath);
await fsp.writeFile(appPath, patch(file, content));
}
}
} else {
if (!existsSync(archive)) return false;
if (existsSync(app)) await fsp.rm(app, { recursive: true, force: true });
await fsp.mkdir(app);
// extract archive to folder and apply patches
for (let file of asar.listPackage(archive)) {
file = file.replace(/^\//g, "");
const stat = asar.statFile(archive, file),
isFolder = !!stat.files,
isSymlink = !!stat.link,
isExecutable = stat.executable,
appPath = resolve(app, file);
if (isFolder) {
await fsp.mkdir(appPath);
} else if (isSymlink) {
await fsp.symlink(appPath, resolve(app, link));
} else {
await fsp.writeFile(appPath, patch(file, extractFile(file)));
if (isExecutable) await fsp.chmod(appPath, "755");
}
}
}
// insert the notion-enhancer/src folder into notion's node_modules
const insertSrc = fileURLToPath(new URL("../src", import.meta.url)),
insertDest = resolve(app, getInsertPath());
await fsp.cp(insertSrc, insertDest, { recursive: true });
// create package.json with cli-specific fields removed
const insertManifest = resolve(insertDest, "package.json"),
manifest = { ...nodeRequire("../package.json"), main: "init.js" },
excludes = ["bin", "type", "scripts", "engines", "dependencies"];
for (const key of excludes) delete manifest[key];
await fsp.writeFile(insertManifest, JSON.stringify(manifest));
if (!directoryMode) {
// re-package enhanced sources into executable archive
await asar.createPackage(app, archive);
// cleanup extracted files unless in debug mode
if (!debug) await fsp.rm(app, { recursive: true });
}
return true;
};
export {
backupApp,
restoreApp,
enhanceApp,
getInsertVersion,
getResourcePath,
setNotionPath,
};

View File

@ -1,893 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
// paste this in the devtools console at to generate theme css
// at https://www.notion.so/9390e51f458940a5a339dc4b8fdea2fb.
// to detect fonts, open the ... menu before running.
// repeat for both light and dark modes, pass the css through
// https://css-minifier.com/ and https://css.github.io/csso/csso.html
// and then save it to core/variables.css and core/theme.css
// todo: svg page & property icons
const darkMode = document.body.classList.contains("dark"),
modeSelector = darkMode ? ".dark" : ":not(.dark)",
bodySelector = `.notion-body${modeSelector}`;
let cssRoot = "",
cssBody = "",
cssRefs = {};
const getComputedPropertyValue = (el, prop) => {
const styles = window.getComputedStyle(el),
value = styles.getPropertyValue(prop);
return value;
},
cssVariable = ({ name, value, alias, splitValues = false }) => {
const values = splitValues ? value.split(", ") : [value],
rgbPattern = /^rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,\d{1,3})?\)$/,
isColor = rgbPattern.test(value.replace(/\s/g, ""));
if (isColor) {
values[0] = values[0].replace(/\s/g, "");
const hasOpaqueAlpha =
values[0].trim().startsWith("rgba(") &&
values[0].trim().endsWith(",1)");
if (hasOpaqueAlpha) values[0] = `rgb(${values[0].slice(5, -3)})`;
}
if (!cssRoot.includes(`--theme--${name}:`)) {
cssRoot += `--theme--${name}:${
alias ? `var(--theme--${alias})` : value
};`;
}
return {
name,
value,
ref: `var(--theme--${name},${values[0]})${
values.length > 1 ? ", " : ""
}${values.slice(1).join(", ")} !important`,
};
},
overrideStyle = ({
element,
selector = "",
property,
variable,
variableAliases = {},
valueAliases = [],
specificity = ["mode", "value"],
cssProps = {},
postProcessor = (selector, cssProps) => [selector, cssProps],
}) => {
if (selector) element ??= document.querySelector(selector);
const style = element?.getAttribute("style") ?? "",
pattern = String.raw`(?:^|(?:;\s*))${property}:\s*([^;]+);?`,
match = style.match(new RegExp(pattern));
if (typeof variable === "string") {
let value = match?.[1];
if (element) {
value ??= getComputedPropertyValue(
element,
property === "background" ? "background-color" : property
);
}
if (!value) throw new Error(`${property} not found for ${selector}`);
variable = cssVariable({
name: variable,
value: value,
alias: variableAliases[value],
splitValues: property === "font-family",
});
}
if (specificity.includes("value")) {
if (/(?<!rgb\()[^\s\d,]+,/g.test(selector) && !selector.includes(":is")) {
selector = `:is(${selector})`;
}
if (match?.[0]) selector += `[style*="${match[0].replace(/"/g, `\\"`)}"]`;
else {
const propSelector = [variable.value, ...valueAliases]
.map((value) =>
property === "color"
? `[style^="color: ${value}"],
[style^="color:${value}"],
[style*=";color: ${value}"],
[style*=";color:${value}"],
[style*=" color: ${value}"],
[style*=" color:${value}"],
[style*="fill: ${value}"],
[style*="fill:${value}"]`
: property === "background"
? `[style^="background: ${value}"],
[style^="background:${value}"],
[style*=";background: ${value}"],
[style*=";background:${value}"],
[style*=" background: ${value}"],
[style*=" background:${value}"],
[style*="background-color: ${value}"],
[style*="background-color:${value}"]`
: `[style*="${property}: ${value}"],
[style*="${property}:${value}"]`
)
.join(",");
selector += selector ? `:is(${propSelector})` : propSelector;
}
}
if (specificity.includes("mode")) {
selector =
/(?<!rgb\()[^\s\d,]+,/g.test(selector) && !selector.includes(":is")
? `${bodySelector} :is(${selector})`
: `${bodySelector} ${selector}`;
}
cssProps[property] = variable;
cssProps["fill"] ??= cssProps["color"];
[selector, cssProps] = postProcessor(selector, cssProps);
const body = Object.entries(cssProps)
.filter(([prop, val]) => prop && val)
.map(([prop, val]) => `${prop}:${val?.ref ?? val}`)
.join(";");
cssRefs[body] ??= [];
cssRefs[body].push(selector);
variableAliases[variable.value] ??= variable.name;
};
const styleText = () => {
const primary = cssVariable({
name: "fg-primary",
value: darkMode ? "rgba(255, 255, 255, 0.81)" : "rgb(55, 53, 47)",
}),
primaryAliases = darkMode
? [
"rgb(211, 211, 211)",
"rgb(255, 255, 255)",
"rgba(255, 255, 255, 0.8",
"rgba(255, 255, 255, 0.9",
"rgba(255, 255, 255, 1",
]
: [
"rgba(255, 255, 255, 0.9)",
"rgba(55, 53, 47, 0.8",
"rgba(55, 53, 47, 0.9",
"rgba(55, 53, 47, 1",
];
const secondary = cssVariable({
name: "fg-secondary",
value: darkMode ? "rgb(155, 155, 155)" : "rgba(25, 23, 17, 0.6)",
}),
secondaryAliases = darkMode
? [
"rgb(127, 127, 127)",
"rgba(255, 255, 255, 0.0",
"rgba(255, 255, 255, 0.1",
"rgba(255, 255, 255, 0.2",
"rgba(255, 255, 255, 0.3",
"rgba(255, 255, 255, 0.4",
"rgba(255, 255, 255, 0.5",
"rgba(255, 255, 255, 0.6",
"rgba(255, 255, 255, 0.7",
]
: [
"rgba(206, 205, 202, 0.6)",
"rgba(55, 53, 47, 0.0",
"rgba(55, 53, 47, 0.1",
"rgba(55, 53, 47, 0.2",
"rgba(55, 53, 47, 0.3",
"rgba(55, 53, 47, 0.4",
"rgba(55, 53, 47, 0.5",
"rgba(55, 53, 47, 0.6",
"rgba(55, 53, 47, 0.7",
];
overrideStyle({
property: "color",
variable: primary,
valueAliases: primaryAliases,
cssProps: {
"caret-color": primary,
"text-decoration-color": "currentColor",
fill: primary,
},
});
overrideStyle({
property: "color",
variable: secondary,
valueAliases: secondaryAliases,
cssProps: {
"caret-color": secondary,
"text-decoration-color": "currentColor",
fill: secondary,
},
postProcessor(selector, cssProps) {
return [
`${bodySelector} :is(.rdp-nav_icon, .rdp-head_cell,
.rdp-day.rdp-day_outside, ::placeholder), ${selector}`,
cssProps,
];
},
});
overrideStyle({
property: "caret-color",
variable: primary,
valueAliases: primaryAliases,
});
overrideStyle({
property: "caret-color",
variable: secondary,
valueAliases: secondaryAliases,
});
overrideStyle({
selector: `[style*="-webkit-text-fill-color:"]`,
property: "-webkit-text-fill-color",
variable: secondary,
specificity: ["mode"],
});
// light mode tags have coloured text,
// replace with primary text for inter-mode consistency
for (const tagSelector of [
`[style*="height: 20px; border-radius: 3px; padding-left: 6px;"][style*="background:"]`,
`.notion-collection_view-block [style*="height: 14px; border-radius: 3px; padding-left: 6px;"]`,
`.notion-timeline-item-properties [style*="height: 18px; border-radius: 3px; padding-left: 8px;"]`,
]) {
for (const el of document.querySelectorAll(tagSelector)) {
if (darkMode) continue;
overrideStyle({
element: el,
selector: tagSelector,
property: "color",
variable: "fg-primary",
});
}
}
};
const styleBorders = () => {
const border = cssVariable({
name: "fg-border",
value: darkMode ? "rgb(47, 47, 47)" : "rgb(233, 233, 231)",
}),
borderColors = darkMode
? [border.value.slice(4, -1), "37, 37, 37", "255, 255, 255"]
: [border.value.slice(4, -1), "238, 238, 237", "55, 53, 47"],
boxShadows = darkMode
? [
"; box-shadow: rgba(255, 255, 255, 0.094) 0px -1px 0px;",
"; box-shadow: rgba(15, 15, 15, 0.2) 0px 0px 0px 1px inset;",
"; box-shadow: rgb(25, 25, 25) -3px 0px 0px, rgb(47, 47, 47) 0px 1px 0px;",
]
: [
"; box-shadow: rgba(55, 53, 47, 0.09) 0px -1px 0px;",
"; box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px inset;",
"; box-shadow: white -3px 0px 0px, rgb(233, 233, 231) 0px 1px 0px;",
];
for (const el of document.querySelectorAll(`[style*="box-shadow:"]`)) {
const boxShadow = el
.getAttribute("style")
.match(/(?:^|(?:;\s*))box-shadow:\s*([^;]+);?/)?.[0];
if (borderColors.some((color) => boxShadow.includes(color))) {
boxShadows.push(boxShadow);
}
}
overrideStyle({
selector: `[style*="height: 1px;"][style*="background"]`,
property: "background",
variable: border,
specificity: ["mode"],
});
cssBody += `
${bodySelector} :is(${[...new Set(borderColors)]
.map(
(color) =>
`[style*="px solid rgb(${color}"], [style*="px solid rgba(${color}"]`
)
.join(", ")}):is([style*="border:"], [style*="border-top:"],
[style*="border-left:"], [style*="border-bottom:"],
[style*="border-right:"]) { border-color: ${border.ref}; }
${[...new Set(boxShadows)]
.map((shadow) => {
if (shadow.startsWith(";")) shadow = shadow.slice(1);
return `${bodySelector} [style*="${shadow}"] { ${shadow
.replace(
/rgba?\([^\)]+\)/g,
shadow.includes("-3px 0px 0px, ")
? "transparent"
: `var(--theme--fg-border, ${border.value})`
)
.slice(0, -1)} !important; }`;
})
.join("")}
`;
};
const styleColoredText = () => {
// inline text
for (const el of document.querySelectorAll(
'.notion-selectable .notion-enable-hover[style*="color:"][style*="fill:"]:not([style*="mono"])'
)) {
if (!el.innerText || /\s/.test(el.innerText)) continue;
overrideStyle({
element: el,
selector: `
.notion-selectable .notion-enable-hover,
.notion-code-block span.token
`,
property: "color",
variable: `fg-${el.innerText}`,
});
}
// block text
for (const el of document.querySelectorAll(
'.notion-text-block > [style*="color:"][style*="fill:"]'
)) {
if (!el.innerText || /\s/.test(el.innerText)) continue;
overrideStyle({
element: el,
selector: `.notion-text-block > [style*="color:"][style*="fill:"]`,
property: "color",
variable: `fg-${el.innerText}`,
});
}
// board text
for (const group of document.querySelectorAll(
".notion-board-view .notion-board-group"
)) {
// get color name from card
const card = group.querySelector('a[style*="background"]'),
innerText = card.innerText.replace("Drag image to reposition\n", "");
if (!innerText || /\s/.test(innerText)) continue;
const el = group.querySelector('[style*="height: 32px"]'),
groupStyle = group
.getAttribute("style")
.match(/background(?:-color)?:\s*([^;]+);?/)[1];
overrideStyle({
element: el,
selector: `.notion-board-view :is(
.notion-board-group[style*="${groupStyle}"] [style*="height: 32px"],
[style*="${groupStyle}"] > [style*="color"]:nth-child(2),
[style*="${groupStyle}"] > div > svg
)`,
property: "color",
// light_gray text doesn't exist
variable: `fg-${innerText === "light_gray" ? "secondary" : innerText}`,
specificity: ["mode"],
});
}
};
const styleBackgrounds = () => {
const primary = cssVariable({
name: "bg-primary",
value: darkMode ? "rgb(25, 25, 25)" : "white",
}),
secondary = cssVariable({
name: "bg-secondary",
value: darkMode ? "rgb(32, 32, 32)" : "rgb(251, 251, 250)",
});
overrideStyle({
property: "background",
variable: primary,
valueAliases: darkMode ? [] : ["rgb(255, 255, 255)", "rgb(247, 247, 247)"],
postProcessor(selector, cssProps) {
return [`${selector}:not(.notion-timeline-view)`, cssProps];
},
});
overrideStyle({
property: "background",
variable: secondary,
valueAliases: darkMode
? ["rgb(37, 37, 37)", "rgb(47, 47, 47)"]
: ["rgb(253, 253, 253)"],
});
// patch: remove overlay from settings sidebar
// to match notion-enhancer menu sidebar colour
cssBody += `.notion-overlay-container .notion-space-settings > div > div > [style*="height: 100%; background: rgba(255, 255, 255, 0.03);"] { background: transparent !important }`;
// cards
overrideStyle({
selector: `.notion-timeline-item,
.notion-calendar-view .notion-collection-item > a,
.notion-gallery-view .notion-collection-item > a`,
property: "background",
variable: secondary,
});
// popups
overrideStyle({
selector: `.notion-overlay-container [style*="border-radius: 4px;"
][style*="position: relative; max-width: calc(100vw - 24px); box-shadow:"],
[style*="font-size: 12px;"][style*="box-shadow:"][
style*="border-radius: 3px; max-width: calc(100% - 16px); min-height: 24px; overflow: hidden;"
][style*="position: absolute; right: 8px; bottom: 8px; z-index:"],
[style*="height: 32px;"][style*="font-size: 14px; line-height: 1.2; border-radius: 5px; box-shadow:"],
[style*="transition: background"][style*="cursor: pointer;"][
style*="border-radius: 3px; height: 24px; width: 24px;"][style*="box-shadow:"],
[style*="right: 6px; top: 4px;"][style*="border-radius: 4px;"][style*="gap: 1px;"][style*="box-shadow:"]`,
property: "background",
variable: secondary,
});
// modals
overrideStyle({
selector: `.notion-overlay-container [data-overlay] :is(
[style*="height: 100%; width: 275px;"][style*="flex-direction: column;"],
.notion-space-settings [style*="flex-grow: 1"] > [style*="background-color"])`,
property: "background",
variable: primary,
specificity: ["mode"],
});
overrideStyle({
selector: `.notion-overlay-container [data-overlay] :is(
[style*="height: 100%; width: 275px;"][style*="flex-direction: column;"] + [style*="width: 100%;"],
.notion-space-settings [style*="height: 100%; background:"][style*="max-width: 250px;"])`,
property: "background",
variable: secondary,
specificity: ["mode"],
});
// timeline fades
overrideStyle({
selector: `.notion-timeline-view`,
property: "background",
variable: primary,
specificity: ["mode"],
});
cssBody += `[style*="linear-gradient(to left, ${
darkMode ? primary.value : "white"
} 20%, rgba(${
darkMode ? primary.value.slice(4, -1) : "255, 255, 255"
}, 0) 100%)"] { background-image: linear-gradient(to left,
var(--theme--bg-primary, ${primary.value}) 20%, transparent
100%) !important; }
[style*="linear-gradient(to right, ${
darkMode ? primary.value : "white"
} 20%, rgba(${
darkMode ? primary.value.slice(4, -1) : "255, 255, 255"
}, 0) 100%)"] { background-image: linear-gradient(to right,
var(--theme--bg-primary, ${primary.value}) 20%, transparent
100%) !important; }
`;
// hovered elements, inputs and unchecked toggle backgrounds
overrideStyle({
property: "background",
variable: cssVariable({
name: "bg-hover",
value: darkMode ? "rgba(255, 255, 255, 0.055)" : "rgba(55, 53, 47, 0.08)",
}),
valueAliases: darkMode
? []
: [
"rgba(242, 241, 238, 0.6)",
"rgb(225, 225, 225)",
"rgb(239, 239, 238)",
],
postProcessor(selector, cssProps) {
selector += `, ${bodySelector} [style*="height: 14px; width: 26px; border-radius: 44px;"][style*="rgba"]`;
if (darkMode) {
selector += `, ${bodySelector} :is([style*="background: rgb(47, 47, 47)"],
[style*="background-color: rgb(47, 47, 47)"])[style*="transition: background"]:hover`;
}
return [selector, cssProps];
},
});
// modal shadow
overrideStyle({
selector: `.notion-overlay-container [data-overlay]
> div > [style*="position: absolute"]:first-child`,
property: "background",
variable: cssVariable({
name: "bg-overlay",
value: darkMode ? "rgba(15, 15, 15, 0.8)" : "rgba(15, 15, 15, 0.6)",
}),
specificity: ["mode"],
});
};
const styleColoredBackgrounds = () => {
for (const targetSelector of [
// database tags
`[style*="height: 20px; border-radius: 3px; padding-left: 6px;"]`,
`.notion-collection_view-block [style*="height: 14px; border-radius: 3px; padding-left: 6px;"]`,
`:is(.notion-timeline-item-properties [style*="height: 18px; border-radius: 3px; padding-left: 8px;"],
.notion-collection_view-block .notion-collection-item a > .notion-focusable)`,
// inline highlights
`.notion-selectable .notion-enable-hover[style*="background:"]`,
// block highlights and hovered board items
`:is(.notion-text-block > [style*="background:"],
.notion-collection_view-block .notion-collection-item a > .notion-focusable)`,
]) {
for (const el of document.querySelectorAll(targetSelector)) {
if (!el.innerText || /\s/.test(el.innerText)) continue;
overrideStyle({
element: el,
selector: targetSelector,
property: "background",
variable: `bg-${el.innerText}`,
});
}
}
// board cards
for (const group of document.querySelectorAll(
".notion-board-view .notion-board-group"
)) {
const card = group.querySelector('a[style*="background"]'),
innerText = card.innerText.replace("Drag image to reposition\n", "");
if (!innerText || /\s/.test(innerText)) continue;
const groupStyle = group
.getAttribute("style")
.match(/background(?:-color)?:\s*([^;]+);?/)[1];
// in light mode pages in board views all have bg "white"
// by default, must be styled based on parent
overrideStyle({
element: card,
selector: `.notion-board-view .notion-board-group[style*="${groupStyle}"] a`,
property: "background",
variable: `bg-${innerText}`,
specificity: ["mode"],
});
overrideStyle({
element: group,
selector: `.notion-board-view [style*="${groupStyle}"]:is(
.notion-board-group,
[style*="border-top-left-radius: 5px;"]
)`,
property: "background",
variable: `dim-${innerText}`,
specificity: ["mode"],
});
}
// use dim for callout blocks
for (const el of document.querySelectorAll(
'.notion-callout-block > div > [style*="background:"]'
)) {
if (!el.innerText || /\s/.test(el.innerText)) continue;
overrideStyle({
element: el,
selector: ".notion-callout-block > div > div",
property: "background",
variable: `dim-${el.innerText}`,
});
}
// use yellow for notification highlights
overrideStyle({
property: "background",
variable: cssVariable({
name: "bg-yellow",
value: "rgba(255, 212, 0, 0.14)",
}),
specificity: ["value"],
});
// use light gray for taglikes e.g. file property values
overrideStyle({
selector: `[style*="height: 18px; border-radius: 3px; background"]`,
property: "background",
variable: "bg-light_gray",
});
};
const styleTooltips = () => {
cssBody += `.notion-overlay-container [style*="border-radius: 3px; background:"
][style*="max-width: calc(100vw - 24px); box-shadow:"
][style*="padding: 4px 8px; font-size: 12px; line-height: 1.4; font-weight: 500;"] {
background: rgb(15, 15, 15) !important;
color: rgba(255, 255, 255, 0.9) !important;
}
.notion-overlay-container [style*="border-radius: 3px; background:"
][style*="max-width: calc(100vw - 24px); box-shadow:"
][style*="padding: 4px 8px; font-size: 12px; line-height: 1.4; font-weight: 500;"]
> [style*="color"] { color: rgb(127, 127, 127) !important; }`;
};
const styleAccents = () => {
const primary = cssVariable({
name: "accent-primary",
value: "rgb(35, 131, 226)",
}),
primaryHover = cssVariable({
name: "accent-primary_hover",
value: "rgb(0, 117, 211)",
}),
primaryContrast = cssVariable({
name: "accent-primary_contrast",
value: "rgb(255, 255, 255)",
}),
primaryTransparent = cssVariable({
name: "accent-primary_transparent",
value: "rgba(35, 131, 226, 0.14)",
});
overrideStyle({
property: "color",
variable: primary,
specificity: ["value"],
});
overrideStyle({
property: "background",
variable: primary,
specificity: ["value"],
cssProps: {
fill: primaryContrast,
color: primaryContrast,
},
});
overrideStyle({
property: "background",
variable: primaryHover,
specificity: ["value"],
cssProps: {
fill: primaryContrast,
color: primaryContrast,
},
});
overrideStyle({
selector: `.notion-table-selection-overlay [style*="border: 2px solid"]`,
property: "border-color",
variable: primary,
specificity: [],
});
overrideStyle({
selector: `
[style*="background: ${primary.value}"] svg[style*="fill"],
[style*="background-color: ${primary.value}"] svg[style*="fill"]
`,
property: "fill",
variable: primaryContrast,
specificity: [],
});
overrideStyle({
selector: `[style*="border-radius: 44px;"] > [style*="border-radius: 44px; background: white;"]`,
property: "background",
variable: primaryContrast,
specificity: [],
});
overrideStyle({
selector: `
*::selection,
.notion-selectable-halo,
#notion-app .rdp-day:not(.rdp-day_disabled):not(.rdp-day_selected
):not(.rdp-day_value):not(.rdp-day_start):not(.rdp-day_end):hover,
[style*="background: ${primaryTransparent.value.split(".")[0]}."],
[style*="background:${primaryTransparent.value.split(".")[0]}."],
[style*="background-color: ${primaryTransparent.value.split(".")[0]}."],
[style*="background-color:${primaryTransparent.value.split(".")[0]}."]
`,
property: "background",
variable: primaryTransparent,
specificity: [],
});
const secondary = cssVariable({
name: "accent-secondary",
value: "rgb(235, 87, 87)",
}),
secondaryAliases = [
"rgb(180, 65, 60)",
"rgb(211, 79, 67)",
"rgb(205, 73, 69)",
],
secondaryHover = cssVariable({
name: "accent-secondary_hover",
value: "rgba(235, 87, 87, 0.1)",
}),
secondaryContrast = cssVariable({
name: "accent-secondary_contrast",
value: "white",
});
overrideStyle({
property: "color",
variable: secondary,
valueAliases: secondaryAliases,
specificity: ["value"],
});
overrideStyle({
property: "background",
variable: secondary,
valueAliases: secondaryAliases,
specificity: ["value"],
cssProps: {
fill: secondaryContrast,
color: secondaryContrast,
},
postProcessor(selector, cssProps) {
return [
`#notion-app .rdp-day_today:not(.rdp-day_selected):not(.rdp-day_value
):not(.rdp-day_start):not(.rdp-day_end)::after, ${selector}`,
cssProps,
];
},
});
overrideStyle({
property: "background",
variable: secondary,
valueAliases: secondaryAliases,
specificity: ["value"],
cssProps: {
fill: secondaryContrast,
color: secondaryContrast,
},
postProcessor(selector, cssProps) {
delete cssProps["background"];
return [
`#notion-app .rdp-day_today:not(.rdp-day_selected):not(.rdp-day_value):not(.rdp-day_start
):not(.rdp-day_end), :is(${selector}) + :is([style*="fill: ${secondaryContrast.value};"],
[style*="color: ${secondaryContrast.value};"]), :is(${selector})
:is([style*="fill: ${secondaryContrast.value};"], [style*="color: ${secondaryContrast.value};"])`,
cssProps,
];
},
});
overrideStyle({
property: "background",
variable: secondaryHover,
specificity: ["value"],
});
// box-shadows are complicated, style manually
cssBody += `.notion-focusable-within:focus-within {
box-shadow:
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 1px inset,
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 2px
!important;
}
.notion-focusable:focus-visible {
box-shadow:
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 1px inset,
var(--theme--accent-primary, ${primary.value}) 0px 0px 0px 2px
!important;
}
${["box-shadow: rgb(35, 131, 226) 0px 0px 0px 2px inset"]
.map((shadow) => {
return `[style*="${shadow}"] { ${shadow.replace(
/rgba?\([^\)]+\)/g,
`var(--theme--accent-primary, ${primary.value})`
)} !important; }`;
})
.join("")}
${[
"border: 1px solid rgb(110, 54, 48)",
"border: 1px solid rgba(235, 87, 87, 0.5)",
"border: 2px solid rgb(110, 54, 48)",
"border: 2px solid rgb(227, 134, 118)",
"border-right: 1px solid rgb(180, 65, 60)",
"border-right: 1px solid rgb(211, 79, 67)",
]
.map((border) => `[style*="${border}"]`)
.join(", ")} { border-color: ${secondary.ref}; }`;
};
const styleScrollbars = () => {
const scrollbarTrack = cssVariable({
name: "scrollbar-track",
value: darkMode ? "rgba(202, 204, 206, 0.04)" : "#EDECE9",
});
overrideStyle({
selector: "::-webkit-scrollbar-track",
property: "background",
variable: scrollbarTrack,
specificity: ["mode"],
});
overrideStyle({
selector: "::-webkit-scrollbar-corner",
property: "background",
variable: scrollbarTrack,
specificity: ["mode"],
});
overrideStyle({
selector: "::-webkit-scrollbar-thumb",
property: "background",
variable: cssVariable({
name: "scrollbar-thumb",
value: darkMode ? "#474c50" : "#D3D1CB",
}),
specificity: ["mode"],
});
overrideStyle({
selector: "::-webkit-scrollbar-thumb:hover",
property: "background",
variable: cssVariable({
name: "scrollbar-thumb_hover",
value: darkMode ? "rgba(202, 204, 206, 0.3)" : "#AEACA6",
}),
specificity: ["mode"],
});
};
const styleCode = () => {
overrideStyle({
selector: `.notion-text-block .notion-enable-hover[style*="mono"]`,
property: "color",
variable: "code-inline_fg",
});
overrideStyle({
selector: `.notion-text-block .notion-enable-hover[style*="mono"]`,
property: "background",
variable: "code-inline_bg",
});
overrideStyle({
selector: `.notion-code-block > [style*="mono"]`,
property: "color",
variable: "code-block_fg",
});
overrideStyle({
selector: `.notion-code-block > div > [style*="background"]`,
property: "background",
variable: "code-block_bg",
});
const aliases = {},
code = document.querySelector(".notion-code-block .token");
for (const token of [
// standard tokens from https://prismjs.com/tokens.html
"keyword",
"builtin",
"class-name",
"function",
"boolean",
"number",
"string",
"char",
"symbol",
"regex",
"url",
"operator",
"variable",
"constant",
"property",
"punctuation",
"important",
"comment",
"tag",
"attr-name",
"attr-value",
"namespace",
"prolog",
"doctype",
"cdata",
"entity",
"atrule",
"selector",
"inserted",
"deleted",
]) {
code.className = `token ${token}`;
overrideStyle({
target: code,
selector: `.notion-code-block .token.${token}`,
property: "color",
variable: `code-${token.replace(/-/g, "_")}`,
variableAliases: aliases,
specificity: ["mode"],
});
}
// patch: remove individual backgrounds from prism tokens
cssBody += `.token:is(
.operator, .entity, .url,
:is(.language-css, .style) .string
) { background: transparent !important; }`;
};
styleText();
styleBorders();
styleColoredText();
styleBackgrounds();
styleColoredBackgrounds();
styleTooltips();
styleAccents();
styleScrollbars();
styleCode();
console.log(
`body${modeSelector} { ${cssRoot} } ${Object.entries(cssRefs)
.map(([body, selectors]) => `${[...new Set(selectors)].join(",")}{${body}}`)
.join("")} ${cssBody}`.replace(/\s+/g, " ")
);

View File

@ -1,76 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
// patch scripts within notion's sources to
// activate and respond to the notion-enhancer
const injectTriggerOnce = (file, content) =>
content +
(!/require\(['|"]notion-enhancer['|"]\)/.test(content)
? `\n\nrequire("notion-enhancer")("${file}",exports,(js)=>eval(js));`
: ""),
replaceIfNotFound = ({ string, mode = "replace" }, search, replacement) =>
string.includes(replacement)
? string
: string.replace(
search,
typeof replacement === "string" && mode === "append"
? `$&${replacement}`
: typeof replacement === "string" && mode === "prepend"
? `${replacement}$&`
: replacement
);
const patches = {
// prettier-ignore
".webpack/main/index.js": (file, content) => {
content = injectTriggerOnce(file, content);
const replace = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "replace" },
...args
)),
prepend = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "prepend" },
...args
)),
append = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "append" },
...args
));
// https://github.com/notion-enhancer/notion-enhancer/issues/160:
// run the app in windows mode on linux (instead of macos mode)
const isWindows =
/(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g,
isWindowsOrLinux = '["win32","linux"].includes(process.platform)';
replace(isWindows, isWindowsOrLinux);
// restore node integration in the renderer process
// so the notion-enhancer can be require()-d into it
replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`);
// expose the app's config + cache + preferences to the global namespace
// e.g. to enable development mode or check if keep in background is enabled
prepend(/\w\.exports=JSON\.parse\('\{"env":"production"/, "globalThis.__notionConfig=");
prepend(/\w\.updatePreferences=\w\.updatePreferences/, "globalThis.__updatePreferences=");
prepend(/\w\.Store=\(0,\w\.configureStore\)/, "globalThis.__notionStore=");
return content;
},
".webpack/renderer/tabs/preload.js": injectTriggerOnce,
".webpack/renderer/tab_browser_view/preload.js": injectTriggerOnce,
};
const decoder = new TextDecoder(),
encoder = new TextEncoder();
export default (file, content) => {
if (!patches[file]) return content;
content = decoder.decode(content);
content = patches[file](file, content);
return encoder.encode(content);
};

View File

@ -1,55 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import fsp from "node:fs/promises";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const esmVersion = "135",
esTarget = "es2022",
esmBundle = ({ name, version, path = "", exports = [] }) => {
const scopedName = name;
if (name.startsWith("@")) name = name.split("/")[1];
path ||= `${name}.bundle.mjs`;
let bundleSrc = `https://esm.sh/v${esmVersion}/${scopedName}@${version}/${esTarget}/${path}`;
if (exports.length) bundleSrc += `?bundle&exports=${exports.join()}`;
return { [`${scopedName.replace(/\//g, "-")}.mjs`]: bundleSrc };
};
const uno = "0.59.4",
coloris = "https://cdn.jsdelivr.net/gh/mdbassit/coloris@v0.24.0/dist",
dependencies = {
...esmBundle({ name: "htm", version: "3.1.1" }),
...esmBundle({
name: "lucide",
version: "0.372.0",
path: "dist/umd/lucide.mjs",
}),
...esmBundle({
name: "@unocss/core",
version: uno,
exports: ["createGenerator", "expandVariantGroup"],
}),
...esmBundle({
name: "@unocss/preset-uno",
version: uno,
exports: ["presetUno"],
}),
"@unocss-preflight-tailwind.css": `https://esm.sh/@unocss/reset@${uno}/tailwind.css`,
"coloris.min.js": `${coloris}/coloris.min.js`,
"coloris.min.css": `${coloris}/coloris.min.css`,
};
const output = fileURLToPath(new URL("../src/vendor", import.meta.url)),
write = (file, data) => fsp.writeFile(resolve(`${output}/${file}`), data);
if (existsSync(output)) await fsp.rm(output, { recursive: true });
await fsp.mkdir(output);
for (const file in dependencies) {
const source = dependencies[file],
res = await (await fetch(source)).text();
await write(file, res);
}

View File

@ -1,307 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import htm from "../vendor/htm.mjs";
import lucide from "../vendor/lucide.mjs";
import {
createGenerator,
expandVariantGroup,
} from "../vendor/@unocss-core.mjs";
import { presetUno } from "../vendor/@unocss-preset-uno.mjs";
import "../assets/icons.svg.js";
// prettier-ignore
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const svgElements = ["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","use","view"],
htmlAttributes = ["accept","accept-charset","accesskey","action","align","allow","alt","async","autocapitalize","autocomplete","autofocus","autoplay","background","bgcolor","border","buffered","capture","challenge","charset","checked","cite","class","code","codebase","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","crossorigin","csp","data","data-*","datetime","decoding","default","defer","dir","dirname","disabled","download","draggable","enctype","enterkeyhint","for","form","formaction","formenctype","formmethod","formnovalidate","formtarget","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","importance","integrity","inputmode","ismap","itemprop","keytype","kind","label","lang","loading","list","loop","low","max","maxlength","minlength","media","method","min","multiple","muted","name","novalidate","open","optimum","pattern","ping","placeholder","playsinline","poster","preload","radiogroup","readonly","referrerpolicy","rel","required","reversed","role","rows","rowspan","sandbox","scope","selected","shape","size","sizes","slot","span","spellcheck","src","srcdoc","srclang","srcset","start","step","style","tabindex","target","title","translate","type","usemap","value","width","wrap","accent-height","accumulate","additive","alignment-baseline","alphabetic","amplitude","arabic-form","ascent","attributeName","attributeType","azimuth","baseFrequency","baseline-shift","baseProfile","bbox","begin","bias","by","calcMode","cap-height","clip","clipPathUnits","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominant-baseline","dur","dx","dy","edgeMode","elevation","enable-background","end","exponent","fill","fill-opacity","fill-rule","filter","filterRes","filterUnits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","format","from","fr","fx","fy","g1","g2","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","glyphRef","gradientTransform","gradientUnits","hanging","horiz-adv-x","horiz-origin-x","ideographic","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letter-spacing","lighting-color","limitingConeAngle","local","marker-end","marker-mid","marker-start","markerHeight","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overline-position","overline-thickness","panose-1","paint-order","path","pathLength","patternContentUnits","patternTransform","patternUnits","pointer-events","points","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","referrerPolicy","refX","refY","rendering-intent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","scale","seed","shape-rendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","string","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","surfaceScale","systemLanguage","tableValues","targetX","targetY","text-anchor","text-decoration","text-rendering","textLength","to","transform","transform-origin","u1","u2","underline-position","underline-thickness","unicode","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","values","vector-effect","version","vert-adv-y","vert-origin-x","vert-origin-y","viewBox","viewTarget","visibility","widths","word-spacing","writing-mode","x","x-height","x1","x2","xChannelSelector","xlink:actuate","xlink:arcrole","xlink:href","xlink:role","xlink:show","xlink:title","xlink:type","xml:base","xml:lang","xml:space","y","y1","y2","yChannelSelector","z","zoomAndPan"];
// accelerators approximately match electron accelerators.
// logic used when recording hotkeys in menu matches logic used
// when triggering hotkeys => detection should be reliable.
// default hotkeys using "alt" may trigger an altcode or
// accented character on some keyboard layouts (not recommended).
let keyListeners = [];
const modifierAliases = [
["metaKey", ["meta", "os", "win", "cmd", "command"]],
["ctrlKey", ["ctrl", "control"]],
["shiftKey", ["shift"]],
["altKey", ["alt"]],
],
addKeyListener = (accelerator, callback, waitForKeyup = false) => {
if (typeof accelerator === "string") accelerator = accelerator.split("+");
accelerator = accelerator.map((key) => key.toLowerCase());
keyListeners.push([accelerator, callback, waitForKeyup]);
},
removeKeyListener = (callback) => {
keyListeners = keyListeners.filter(([, c]) => c !== callback);
},
handleKeypress = (event, keyListeners) => {
for (const [accelerator, callback] of keyListeners) {
const acceleratorModifiers = [],
combinationTriggered =
accelerator.every((key) => {
for (const [modifier, aliases] of modifierAliases) {
if (aliases.includes(key)) {
acceleratorModifiers.push(modifier);
return true;
}
}
if (key === "space") key = " ";
if (key === "plus") key = "equal";
if (key === "minus") key = "-";
if (key === "\\") key = "backslash";
if (key === ",") key = "comma";
if (key === ".") key = "period";
const keyPressed = [
event.key.toLowerCase(),
event.code.toLowerCase(),
].includes(key);
return keyPressed;
}) &&
modifierAliases.every(([modifier]) => {
// required && used -> matches accelerator
// !required && !used -> matches accelerator
// (required && !used) || (!required && used) -> no match
// differentiates e.g.ctrl + x from ctrl + shift + x
return acceleratorModifiers.includes(modifier) === event[modifier];
});
if (combinationTriggered) callback(event);
}
},
onKeyup = (event) => {
const keyupListeners = keyListeners //
.filter(([, , waitForKeyup]) => waitForKeyup);
handleKeypress(event, keyupListeners);
},
onKeydown = (event) => {
const keydownListeners = keyListeners //
.filter(([, , waitForKeyup]) => !waitForKeyup);
handleKeypress(event, keydownListeners);
};
document.removeEventListener("keyup", onKeyup);
document.removeEventListener("keydown", onKeydown);
document.addEventListener("keyup", onKeyup);
document.addEventListener("keydown", onKeydown);
// mutation listeners observe updates to the dom.
// by default, the criteria for matching a selector
// is very broad. custom opts can be passed when
// adding a listener to reduce handler calls
let documentObserver,
observerDefaults = {
// whether to observe attribute updates
attributes: true,
// whether to observe innerText updates
characterData: true,
// whether to observe added/removed nodes
childList: true,
// whether to observe nested nodes
subtree: true,
},
mutationListeners = [];
const _mutations = [],
addMutationListener = (selector, callback, opts) => {
opts = { ...observerDefaults, ...opts };
mutationListeners.push([selector, callback, opts]);
},
removeMutationListener = (callback) => {
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
},
selectorMutated = (mutation, selector, opts) => {
if (!opts.attributes && mutation.type === "attributes") return false;
if (!opts.characterData && mutation.type === "characterData") return false;
const target =
mutation.type === "characterData"
? mutation.target.parentElement
: mutation.target;
if (!target) return false;
const matchesTarget = target.matches(selector),
matchesParent = opts.subtree && target.matches(`${selector} *`),
matchesChild = opts.subtree && target.querySelector(selector),
matchesAdded =
opts.childList &&
[...(mutation.addedNodes || [])].some((node) => {
if (!(node instanceof HTMLElement)) node = node.parentElement;
return node?.querySelector(selector);
});
return matchesTarget || matchesParent || matchesChild || matchesAdded;
},
handleMutations = () => {
let mutation;
while ((mutation = _mutations.shift())) {
for (const [selector, callback, subtree] of mutationListeners)
if (selectorMutated(mutation, selector, subtree)) callback(mutation);
}
},
attachObserver = () => {
if (document.readyState !== "complete") return;
document.removeEventListener("readystatechange", attachObserver);
(documentObserver ??= new MutationObserver((mutations, _observer) => {
if (!_mutations.length) requestIdleCallback(handleMutations);
_mutations.push(...mutations);
})).disconnect();
documentObserver.observe(document.body, observerDefaults);
};
document.addEventListener("readystatechange", attachObserver);
attachObserver();
const kebabToPascalCase = (string) =>
string[0].toUpperCase() +
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
hToString = (type, props, ...children) =>
`<${type}${Object.entries(props)
.map(([attr, value]) => ` ${attr}="${value}"`)
.join("")}>${children
.map((child) => (Array.isArray(child) ? hToString(...child) : child))
.join("")}</${type}>`,
// combines instance-provided element props
// with a template of element props such that
// island/component/template props handlers
// and styles can be preserved and extended
// rather than overwritten
extendProps = (props, extend) => {
for (const key in extend) {
const { [key]: value } = props;
if (typeof extend[key] === "function") {
props[key] = (...args) => {
extend[key](...args);
if (typeof value === "function") value(...args);
};
} else if (key === "class") {
props[key] = value ? `${value} ${extend[key]}` : extend[key];
} else props[key] = extend[key] ?? value;
}
return props;
},
// enables use of the jsx-like htm syntax
// for building components and interfaces
// with tagged templates. instantiates dom
// elements directly, does not use a vdom.
// e.g. html`<div class=${className}></div>`
h = function (type, props, ...children) {
// disables element caching
this[0] = 3;
children = children.flat(Infinity);
if (typeof type === "function") {
// html`<${Component} attr="value">Click Me<//>`
return type(props ?? {}, ...children);
}
const elem = svgElements.includes(type)
? document.createElementNS("http://www.w3.org/2000/svg", type)
: document.createElement(type);
for (const prop in props ?? {}) {
if (typeof props[prop] === "undefined") continue;
if (["class", "className"].includes(prop)) {
// collapse multiline classes &
// expand utility variant class groups
props[prop] = props[prop].replace(/\s+/g, " ");
props[prop] = expandVariantGroup(props[prop]).trim();
elem.setAttribute("un-cloak", "");
}
if (htmlAttributes.includes(prop) || prop.includes("-")) {
if (typeof props[prop] === "boolean") {
if (!props[prop]) continue;
elem.setAttribute(prop, "");
} else elem.setAttribute(prop, props[prop]);
} else elem[prop] = props[prop];
}
if (type === "style") {
elem.append(children.join("").replace(/\s+/g, " "));
} else elem.append(...children);
return elem;
},
html = htm.bind(h);
const iconPattern = /^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/,
svgToUri = (svg) => {
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
const xlmns = ~svg.indexOf("xmlns")
? "<svg"
: '<svg xmlns="http://www.w3.org/2000/svg"';
return `url("data:image/svg+xml;utf8,${svg
.replace("<svg", xlmns)
.replace(/"/g, "'")
.replace(/%/g, "%25")
.replace(/#/g, "%23")
.replace(/{/g, "%7B")
.replace(/}/g, "%7D")
.replace(/</g, "%3C")
.replace(/>/g, "%3E")
.replace(/\s+/g, " ")
.trim()}")`;
},
// prefer custom preset over @unocss/preset-icons:
// limits icons to single set, avoids loading over
// cdn (otherwise could cause issues when submitting
// to the chrome webstore). also makes custom icon
// handling straightforward
presetIcons = ([, icon, mode]) => {
let svg,
mask = mode === "mask";
if (icon === "notion-enhancer") {
const { iconColour, iconMonochrome } = globalThis.__enhancerApi;
svg = mask ? iconMonochrome : iconColour;
} else {
icon = kebabToPascalCase(icon);
if (!lucide[icon]) return;
const [type, props, children] = lucide[icon];
svg = hToString(type, props, ...children);
}
mask ||= mode !== "bg" && svg.includes("currentColor");
return {
// https://antfu.me/posts/icons-in-pure-css
display: "inline-block",
height: "1em",
width: "1em",
[mask ? "mask" : "background"]: `${svgToUri(svg)} no-repeat`,
[mask ? "mask-size" : "background-size"]: "100% 100%",
"background-color": mask ? "currentColor" : "transparent",
};
};
let _renderedTokens = -1;
const _tokens = new Set(),
_stylesheet = html`<style id="__unocss"></style>`,
preflight = `[un-cloak]{display:none!important}
.notion-emoji{display:inline-block!important}`,
uno = createGenerator({
presets: [presetUno()],
preflights: [{ getCSS: () => preflight }],
rules: [[iconPattern, presetIcons, { layer: "icons" }]],
layers: { preflights: -2, icons: -1, default: 1 },
}),
extractTokens = ($root) => {
if (!$root?.classList) return;
for (const t of $root.classList) _tokens.add(t);
for (const $ of $root.children) extractTokens($);
$root.removeAttribute("un-cloak");
},
renderStylesheet = async () => {
if (_renderedTokens === _tokens.size) return;
_renderedTokens = _tokens.size;
const res = await uno.generate(_tokens);
if (!document.contains(_stylesheet)) document.head.append(_stylesheet);
if (_stylesheet.innerHTML !== res.css) _stylesheet.innerHTML = res.css;
};
addMutationListener("*", (mutation) => {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) extractTokens(node);
} else if (mutation.type === "attributes") extractTokens(mutation.target);
else return;
renderStylesheet();
});
renderStylesheet();
Object.assign((globalThis.__enhancerApi ??= {}), {
html,
extendProps,
addKeyListener,
removeKeyListener,
addMutationListener,
removeMutationListener,
});

View File

@ -1,367 +0,0 @@
/**
* notion-enhancer: api
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
/**
* a basic wrapper around notion's content apis
* @namespace notion
*/
import { web, fs, fmt } from './index.mjs';
const standardiseUUID = (uuid) => {
if (uuid?.length === 32 && !uuid.includes('-')) {
uuid = uuid.replace(
/([\d\w]{8})([\d\w]{4})([\d\w]{4})([\d\w]{4})([\d\w]{12})/,
'$1-$2-$3-$4-$5'
);
}
return uuid;
};
/**
* unofficial content api: get a block by id
* (requires user to be signed in or content to be public).
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {string} id - uuidv4 record id
* @param {string=} table - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @returns {Promise<object>} record data. type definitions can be found here:
* https://github.com/NotionX/react-notion-x/tree/master/packages/notion-types/src
*/
export const get = async (id, table = 'block') => {
id = standardiseUUID(id);
const json = await fs.getJSON('https://www.notion.so/api/v3/getRecordValues', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests: [{ table, id }] }),
method: 'POST',
});
return json?.results?.[0]?.value || json;
};
/**
* get the id of the current user (requires user to be signed in)
* @returns {string} uuidv4 user id
*/
export const getUserID = () =>
JSON.parse(localStorage['LRU:KeyValueStore2:current-user-id'] || {}).value;
/**
* get the id of the currently open page
* @returns {string} uuidv4 page id
*/
export const getPageID = () =>
standardiseUUID(
web.queryParams().get('p') || location.pathname.split(/(-|\/)/g).reverse()[0]
);
let _spaceID;
/**
* get the id of the currently open workspace (requires user to be signed in)
* @returns {string} uuidv4 space id
*/
export const getSpaceID = async () => {
if (!_spaceID) _spaceID = (await get(getPageID())).space_id;
return _spaceID;
};
/**
* unofficial content api: search all blocks in a space
* (requires user to be signed in or content to be public).
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {string=} query - query to search blocks in the space for
* @param {number=} limit - the max number of results to return (default: 20)
* @param {string=} spaceID - uuidv4 workspace id
* @returns {object} the number of total results, the list of matches, and related record values.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/api.ts
*/
export const search = async (query = '', limit = 20, spaceID = getSpaceID()) => {
spaceID = standardiseUUID(await spaceID);
const json = await fs.getJSON('https://www.notion.so/api/v3/search', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'BlocksInSpace',
query,
spaceId: spaceID,
limit,
filters: {
isDeletedOnly: false,
excludeTemplates: false,
isNavigableOnly: false,
requireEditPermissions: false,
ancestors: [],
createdBy: [],
editedBy: [],
lastEditedTime: {},
createdTime: {},
},
sort: 'Relevance',
source: 'quick_find',
}),
method: 'POST',
});
return json;
};
/**
* unofficial content api: update a property/the content of an existing record
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {object} pointer - the record being updated
* @param {object} recordValue - the new raw data values to set to the record.
* for examples, use notion.get to fetch an existing block record.
* to use this to update content, set pointer.path to ['properties', 'title]
* and recordValue to an array of rich text segments. a segment is an array
* where the first value is the displayed text and the second value
* is an array of decorations. a decoration is an array where the first value
* is a modifier and the second value specifies it. e.g.
* [
* ['bold text', [['b']]],
* [' '],
* ['an italicised link', [['i'], ['a', 'https://github.com']]],
* [' '],
* ['highlighted text', [['h', 'pink_background']]],
* ]
* more examples can be creating a block with the desired content/formatting,
* then find the value of blockRecord.properties.title using notion.get.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/core.ts
* @param {string} pointer.recordID - uuidv4 record id
* @param {string=} pointer.recordTable - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {string=} pointer.property - the record property to update.
* for record content, it will be the default: 'title'.
* for page properties, it will be the property id (the key used in pageRecord.properties).
* other possible values are unknown/untested
* @param {string=} pointer.spaceID - uuidv4 workspace id
* @param {string=} pointer.path - the path to the key to be set within the record
* (default: [], the root of the record's values)
* @returns {boolean|object} true if success, else an error object
*/
export const set = async (
{ recordID, recordTable = 'block', spaceID = getSpaceID(), path = [] },
recordValue = {}
) => {
spaceID = standardiseUUID(await spaceID);
recordID = standardiseUUID(recordID);
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: fmt.uuidv4(),
transactions: [
{
id: fmt.uuidv4(),
spaceId: spaceID,
operations: [
{
pointer: {
table: recordTable,
id: recordID,
spaceId: spaceID,
},
path,
command: path.length ? 'set' : 'update',
args: recordValue,
},
],
},
],
}),
method: 'POST',
});
return json.errorId ? json : true;
};
/**
* unofficial content api: create and add a new block to a page
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {object} insert - the new record.
* @param {object} pointer - where to insert the new block
* for examples, use notion.get to fetch an existing block record.
* type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/block.ts
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {object=} insert.recordValue - the new raw data values to set to the record.
* @param {object=} insert.recordTable - record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @param {string=} pointer.prepend - insert before pointer.siblingID. if false, will be appended after
* @param {string=} pointer.siblingID - uuidv4 sibling id. if unset, the record will be
* inserted at the end of the page start (or the start if pointer.prepend is true)
* @param {string=} pointer.parentID - uuidv4 parent id
* @param {string=} pointer.parentTable - parent record type (default: 'block').
* @param {string=} pointer.spaceID - uuidv4 space id
* @param {string=} pointer.userID - uuidv4 user id
* instead of the end
* @returns {string|object} error object or uuidv4 of the new record
*/
export const create = async (
{ recordValue = {}, recordTable = 'block' } = {},
{
prepend = false,
siblingID = undefined,
parentID = getPageID(),
parentTable = 'block',
spaceID = getSpaceID(),
userID = getUserID(),
} = {}
) => {
spaceID = standardiseUUID(await spaceID);
parentID = standardiseUUID(parentID);
siblingID = standardiseUUID(siblingID);
const recordID = standardiseUUID(recordValue?.id ?? fmt.uuidv4()),
path = [],
args = {
type: 'text',
id: recordID,
version: 0,
created_time: new Date().getTime(),
last_edited_time: new Date().getTime(),
parent_id: parentID,
parent_table: parentTable,
alive: true,
created_by_table: 'notion_user',
created_by_id: userID,
last_edited_by_table: 'notion_user',
last_edited_by_id: userID,
space_id: spaceID,
permissions: [{ type: 'user_permission', role: 'editor', user_id: userID }],
};
if (parentTable === 'space') {
parentID = spaceID;
args.parent_id = spaceID;
path.push('pages');
args.type = 'page';
} else if (parentTable === 'collection_view') {
path.push('page_sort');
args.type = 'page';
} else {
path.push('content');
}
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: fmt.uuidv4(),
transactions: [
{
id: fmt.uuidv4(),
spaceId: spaceID,
operations: [
{
pointer: {
table: parentTable,
id: parentID,
spaceId: spaceID,
},
path,
command: prepend ? 'listBefore' : 'listAfter',
args: {
...(siblingID ? { after: siblingID } : {}),
id: recordID,
},
},
{
pointer: {
table: recordTable,
id: recordID,
spaceId: spaceID,
},
path: [],
command: 'set',
args: {
...args,
...recordValue,
},
},
],
},
],
}),
method: 'POST',
});
return json.errorId ? json : recordID;
};
/**
* unofficial content api: upload a file to notion's aws servers
* (requires user to be signed in or content to be public).
* TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
* to be unable to parse and render content properly and throw errors.
* why not use the official api?
* 1. cors blocking prevents use on the client
* 2. the majority of blocks are still 'unsupported'
* @param {File} file - the file to upload
* @param {object=} pointer - where the file should be accessible from
* @param {string=} pointer.pageID - uuidv4 page id
* @param {string=} pointer.spaceID - uuidv4 space id
* @returns {string|object} error object or the url of the uploaded file
*/
export const upload = async (file, { pageID = getPageID(), spaceID = getSpaceID() } = {}) => {
spaceID = standardiseUUID(await spaceID);
pageID = standardiseUUID(pageID);
const json = await fs.getJSON('https://www.notion.so/api/v3/getUploadFileUrl', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
bucket: 'secure',
name: file.name,
contentType: file.type,
record: {
table: 'block',
id: pageID,
spaceId: spaceID,
},
}),
});
if (json.errorId) return json;
fetch(json.signedPutUrl, {
method: 'PUT',
headers: { 'content-type': file.type },
body: file,
});
return json.url;
};
/**
* redirect through notion to a resource's signed aws url for display outside of notion
* (requires user to be signed in or content to be public)
* @param src source url for file
* @param {string} recordID uuidv4 record/block/file id
* @param {string=} recordTable record type (default: 'block').
* may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
* @returns {string} url signed if necessary, else string as-is
*/
export const sign = (src, recordID, recordTable = 'block') => {
if (src.startsWith('/')) src = `https://notion.so${src}`;
if (src.includes('secure.notion-static.com')) {
src = new URL(src);
src = `https://www.notion.so/signed/${encodeURIComponent(
src.origin + src.pathname
)}?table=${recordTable}&id=${recordID}`;
}
return src;
};

View File

@ -1,87 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
const _isManifestValid = (modManifest) => {
const { platform } = globalThis.__enhancerApi,
hasRequiredFields =
modManifest.id &&
modManifest.name &&
modManifest.version &&
modManifest.description &&
modManifest.authors,
meetsThemeRequirements =
!modManifest._src.startsWith("themes/") ||
((modManifest.tags?.includes("dark") ||
modManifest.tags?.includes("light")) &&
modManifest.thumbnail),
targetsCurrentPlatform =
!modManifest.platforms || //
modManifest.platforms.includes(platform);
return hasRequiredFields && meetsThemeRequirements && targetsCurrentPlatform;
};
let _mods;
const getMods = async (asyncFilter) => {
const { readJson } = globalThis.__enhancerApi;
// prettier-ignore
_mods ??= (await Promise.all((await readJson("registry.json")).map(async (_src) => {
const modManifest = { ...(await readJson(`${_src}/mod.json`)), _src };
return _isManifestValid(modManifest) ? modManifest : undefined;
}))).filter((mod) => mod);
// prettier-ignore
return (await Promise.all(_mods.map(async (mod) => {
return !asyncFilter || (await asyncFilter(mod)) ? mod : undefined;
}))).filter((mod) => mod);
},
getProfile = async () => {
const db = globalThis.__enhancerApi.initDatabase();
let activeProfile = await db.get("activeProfile");
activeProfile ??= (await db.get("profileIds"))?.[0];
return activeProfile ?? "default";
};
const isEnabled = async (id) => {
const { version, initDatabase } = globalThis.__enhancerApi,
mod = (await getMods()).find((mod) => mod.id === id);
if (mod._src === "core") return true;
const agreedToTerms = await initDatabase().get("agreedToTerms"),
enabledInProfile = await initDatabase([
await getProfile(),
"enabledMods",
]).get(id);
return agreedToTerms === version && enabledInProfile;
},
setEnabled = async (id, enabled) => {
return await globalThis.__enhancerApi
.initDatabase([await getProfile(), "enabledMods"])
.set(id, enabled);
};
const modDatabase = async (id) => {
const optionDefaults = (await getMods())
.find((mod) => mod.id === id)
?.options?.map?.((opt) => {
let value = opt.value;
value ??= opt.values?.[0]?.value;
value ??= opt.values?.[0];
return [opt.key, value];
})
?.filter?.(([, value]) => typeof value !== "undefined");
return globalThis.__enhancerApi.initDatabase(
[await getProfile(), id],
Object.fromEntries(optionDefaults ?? [])
);
};
Object.assign((globalThis.__enhancerApi ??= {}), {
getMods,
getProfile,
isEnabled,
setEnabled,
modDatabase,
});

View File

@ -1,70 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
// batch event callbacks to avoid over-handling
// and any conflicts / perf.issues that may
// otherwise result. initial call is immediate,
// following calls are delayed. a wait time of
// ~200ms is recommended (the avg. human visual
// reaction time is ~180-200ms)
const sleep = async (ms) => {
return new Promise((res, rej) => setTimeout(res, ms));
},
debounce = (callback, ms = 200) => {
let delay, update;
const next = () =>
sleep(ms).then(() => {
if (!update) return (delay = undefined);
update(), (update = undefined);
delay = next();
});
return (...args) => {
if (delay) update = callback.bind(this, ...args);
return delay || ((delay = next()), callback(...args));
};
};
// provides basic key/value reactivity:
// this is shared between all active mods,
// i.e. mods can read and update other mods'
// reactive states. this enables interop
// between a mod's component islands and
// supports inter-mod communication if so
// required. caution should be used in
// naming keys to avoid conflicts
const _subscribers = [],
dumpState = () => (document.__enhancerState ??= {}),
getKeysFromState = (keys) => keys.map((key) => dumpState()[key]),
setState = (state) => {
Object.assign(dumpState(), state);
const updates = Object.keys(state);
_subscribers
.filter(([keys]) => updates.some((key) => keys.includes(key)))
.forEach(([keys, callback]) => callback(getKeysFromState(keys)));
},
// useState(["keyA", "keyB"]) => returns [valueA, valueB]
// useState(["keyA", "keyB"], callback) => registers callback
// to be triggered after each update to either keyA or keyB,
// with [valueA, valueB] passed to the callback's first arg
useState = (keys, callback) => {
const state = getKeysFromState(keys);
if (callback) {
callback = debounce(callback);
_subscribers.push([keys, callback]);
callback(state);
}
return state;
};
Object.assign((globalThis.__enhancerApi ??= {}), {
sleep,
debounce,
setState,
useState,
dumpState,
});

View File

@ -1,154 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
const IS_ELECTRON = typeof module !== "undefined",
IS_RENDERER = IS_ELECTRON && process.type === "renderer";
// expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox'
// and 'chromium' (inc. chromium-based browsers like edge and brave)
// other possible values: 'aix', 'freebsd', 'openbsd', 'sunos'
const platform = IS_ELECTRON
? process.platform
: navigator.userAgent.includes("Firefox")
? "firefox"
: "chromium",
// currently installed version of the notion-enhancer
version = IS_ELECTRON
? require("notion-enhancer/package.json").version
: chrome.runtime.getManifest().version,
// packages a url to access notion-enhancer assets and sources,
// proxies via api in desktop app to bypass service worker cache
enhancerUrl = (target = "") =>
IS_ELECTRON
? "https://www.notion.so/api/__notion-enhancer/" +
target.replace(/^\//, "")
: chrome.runtime.getURL(target),
// require a file from the root of notion's app/ folder,
// only available in an electron main process
notionRequire = (target) =>
IS_ELECTRON && !IS_RENDERER ? require(`../../../${target}`) : undefined;
let __port;
const connectToPort = () => {
if (__port) return;
__port = chrome.runtime.connect();
__port.onDisconnect.addListener(() => (__port = null));
},
onMessage = (channel, listener) => {
// from worker to client
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.on(channel, (event, message) => listener(message));
} else if (!IS_ELECTRON) {
const onMessage = (msg) => {
if (msg?.channel !== channel || msg?.invocation) return;
listener(msg.message);
};
connectToPort();
__port.onMessage.addListener(onMessage);
chrome.runtime.onMessage.addListener(onMessage);
}
},
sendMessage = (channel, message) => {
// to worker from client
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.send(channel, message);
} else if (!IS_ELECTRON) {
connectToPort();
__port.postMessage({ channel, message });
}
},
invokeInWorker = (channel, message) => {
// sends a payload to the worker/main
// process and waits for a response
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
return ipcRenderer.invoke(channel, message);
} else if (!IS_ELECTRON) {
// polyfills the electron.ipcRenderer.invoke method in
// the browser: uses a long-lived ipc connection to
// pass messages and handle responses asynchronously
let fulfilled;
connectToPort();
const id = crypto.randomUUID();
return new Promise((res, rej) => {
__port.onMessage.addListener((msg) => {
if (msg?.invocation !== id || fulfilled) return;
fulfilled = true;
res(msg.message);
});
__port.postMessage({ channel, message, invocation: id });
});
}
};
const readFile = (file) => {
if (IS_ELECTRON) {
// read directly from filesys if possible,
// treating notion-enhancer/src as fs root
if (!file.startsWith("http")) {
const fsp = require("fs/promises"),
{ resolve } = require("path");
return fsp.readFile(resolve(`${__dirname}/../${file}`), "utf-8");
}
} else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.text());
},
readJson = (file) => {
// as above, uses require instead of readFile
// and res.json() instead of res.text() to return
// json content of file in object form
if (IS_ELECTRON) {
if (!file.startsWith("http")) {
const { resolve } = require("path");
return require(resolve(`${__dirname}/../${file}`));
}
} else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.json());
};
const initDatabase = (namespace, fallbacks = {}) => {
// all db operations are performed via ipc:
// with nodeintegration disabled, sqlite cannot
// be require()-d from the renderer process
const query = (query, args = {}) =>
IS_ELECTRON && !IS_RENDERER
? globalThis.__enhancerApi.queryDatabase(namespace, query, args)
: invokeInWorker("notion-enhancer", {
action: "query-database",
data: { namespace, query, args },
});
return {
get: (key) => query("get", { key, fallbacks }),
set: (key, value) => query("set", { key, value }),
remove: (keys) => query("remove", { keys }),
export: () => query("export"),
import: (obj) => query("import", { obj }),
};
},
reloadApp = () => {
if (IS_ELECTRON && !IS_RENDERER) {
const { app } = require("electron");
app.relaunch(), app.exit();
} else sendMessage("notion-enhancer", "reload-app");
};
Object.assign((globalThis.__enhancerApi ??= {}), {
platform,
version,
enhancerUrl,
notionRequire,
onMessage,
sendMessage,
invokeInWorker,
readFile,
readJson,
initDatabase,
reloadApp,
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,157 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
// telemetry endpoint not ready, disabled for current release
import { checkForUpdate } from "./updateCheck.mjs";
// import { sendTelemetryPing } from "./sendTelemetry.mjs";
import { Modal, Frame } from "./islands/Modal.mjs";
import { MenuButton } from "./islands/MenuButton.mjs";
import { Tooltip } from "./islands/Tooltip.mjs";
import { Panel } from "./islands/Panel.mjs";
const shouldLoadThemeOverrides = async (api, db) => {
const { getMods, isEnabled } = api,
loadThemeOverrides = await db.get("loadThemeOverrides");
if (loadThemeOverrides === "Enabled") return true;
if (loadThemeOverrides === "Disabled") return false;
// prettier-ignore
// loadThemeOverrides === "Auto"
return (await getMods(async (mod) => {
if (!mod._src.startsWith("themes/")) return false;
return await isEnabled(mod.id);
})).length;
},
loadThemeOverrides = async (api, db) => {
const { html, enhancerUrl } = api;
if (!(await shouldLoadThemeOverrides(api, db))) return;
document.head.append(html`<link
rel="stylesheet"
href=${enhancerUrl("core/theme.css")}
/>`);
},
insertCustomStyles = async (api, db) => {
const { html } = api,
customStyles = (await db.get("customStyles"))?.content;
if (!customStyles) return;
const $customStyles = html`<style
id="__custom"
innerHTML=${customStyles}
></style>`;
return document.head.append($customStyles);
};
const insertMenu = async (api, db) => {
const inviteMember = `.notion-sidebar-container .notion-sidebar [role="button"]:has(.inviteMember)`,
{ html, addMutationListener, removeMutationListener } = api,
{ addKeyListener, platform, enhancerUrl, onMessage } = api,
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
menuButtonLabel = await db.get("menuButtonLabel"),
openMenuHotkey = await db.get("openMenuHotkey"),
menuPing = {
channel: "notion-enhancer",
hotkey: openMenuHotkey,
icon: menuButtonIconStyle,
};
let _contentWindow;
const updateMenuTheme = () => {
const darkMode = document.body.classList.contains("dark"),
notionTheme = darkMode ? "dark" : "light";
menuPing.theme = notionTheme;
_contentWindow?.postMessage?.(menuPing, "*");
};
const $modal = html`<${Modal}>
<${Frame}
title="notion-enhancer menu"
src="${enhancerUrl("core/menu/index.html")}"
onload=${function () {
// pass notion-enhancer api to electron menu process
if (["linux", "win32", "darwin"].includes(platform)) {
const apiKey = "__enhancerApi";
this.contentWindow[apiKey] = { ...globalThis[apiKey] };
}
_contentWindow = this.contentWindow;
updateMenuTheme();
}}
/>
<//>`,
$button = html`<${MenuButton}
onclick=${$modal.open}
notifications=${(await checkForUpdate()) ? 1 : 0}
themeOverridesLoaded=${await shouldLoadThemeOverrides(api, db)}
icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
? "?mask"
: " text-[16px]"}"
>${menuButtonLabel}
<//>`;
const appendToDom = () => {
if (!document.body.contains($modal)) document.body.append($modal);
else if (!document.body.contains($button)) {
document.querySelector(inviteMember)?.after($button);
} else removeMutationListener(appendToDom);
};
html`<${Tooltip}>
<b>Configure the notion-enhancer and its mods</b>
<//>`.attach($button, "right");
addMutationListener(inviteMember, appendToDom);
addMutationListener(".notion-app-inner", updateMenuTheme, { subtree: false });
appendToDom();
addKeyListener(openMenuHotkey, (event) => {
event.preventDefault();
event.stopPropagation();
$modal.open();
});
addEventListener("message", (event) => {
// from embedded menu
if (event.data?.channel !== "notion-enhancer") return;
if (event.data?.action === "close-menu") $modal.close();
if (event.data?.action === "open-menu") $modal.open();
});
onMessage("notion-enhancer", (message) => {
// from worker
if (message === "open-menu") $modal.open();
});
};
const insertPanel = async (api, db) => {
const notionFrame = ".notion-frame",
togglePanelHotkey = await db.get("togglePanelHotkey"),
{ html, setState, addMutationListener, removeMutationListener } = api;
const $panel = html`<${Panel}
hotkey="${togglePanelHotkey}"
...${Object.assign(
...["Width", "Open", "View"].map((key) => ({
[`_get${key}`]: () => db.get(`panel${key}`),
[`_set${key}`]: async (value) => {
await db.set(`panel${key}`, value);
setState({ rerender: true });
},
}))
)}
/>`,
appendToDom = () => {
const $frame = document.querySelector(notionFrame);
if (!$frame) return;
$frame.append($panel);
$frame.style.flexDirection = "row";
removeMutationListener(appendToDom);
};
addMutationListener(notionFrame, appendToDom);
appendToDom();
};
export default async (api, db) =>
Promise.all([
insertMenu(api, db),
insertPanel(api, db),
insertCustomStyles(api, db),
loadThemeOverrides(api, db),
// sendTelemetryPing(),
]).then(() => api.sendMessage("notion-enhancer", "load-complete"));

View File

@ -1,49 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
const getPreference = (key) => {
const { preferences = {} } = globalThis.__notionStore?.getState()?.app;
return preferences[key];
},
setPreference = (key, value) => {
const action = globalThis.__updatePreferences?.({ [key]: value });
globalThis.__notionStore?.dispatch?.(action);
};
module.exports = async ({}, db) => {
const toggleWindowHotkey = await db.get("toggleWindowHotkey"),
developerMode = await db.get("developerMode");
// enable developer mode, access extra debug tools
Object.assign((globalThis.__notionConfig ??= {}), {
env: developerMode ? "development" : "production",
});
// listen for the global window toggle hotkey
const { app, globalShortcut, BrowserWindow } = require("electron");
app.whenReady().then(() => {
globalShortcut.register(toggleWindowHotkey, () => {
const windows = BrowserWindow.getAllWindows()
// filter out quick search window
.filter((win) => win.fullScreenable),
focused = windows.some((win) => win.isFocused() && win.isVisible());
windows.forEach((win) =>
// check if notion is set to run in the background
getPreference("isHideLastWindowOnCloseEnabled")
? focused
? win.hide()
: win.show()
: focused
? win.minimize()
: win.isMinimized()
? win.restore()
: win.focus()
);
});
});
};

View File

@ -1,54 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
let __$wrapper;
const setupWrapper = () => {
const notionAi = ".notion-help-button, .notion-ai-button",
{ html, addMutationListener } = globalThis.__enhancerApi,
{ removeMutationListener } = globalThis.__enhancerApi;
return (__$wrapper ??= new Promise((res) => {
const addToDom = () => {
const $notionAi = document.querySelector(notionAi);
if (!$notionAi) return;
const $wrapper = html`<div
class="notion-enhancer--floating-buttons z-50 gap-[12px]
flex absolute bottom-[calc(16px+env(safe-area-inset-bottom))]"
></div>`;
removeMutationListener(addToDom);
$notionAi.after($wrapper);
res($wrapper);
};
addMutationListener(notionAi, addToDom);
addToDom();
}));
},
addFloatingButton = async ($btn) => {
if (document.contains($btn)) return;
(await setupWrapper()).prepend($btn);
// button positioning is calculated by panel
},
removeFloatingButton = ($btn) => $btn.remove();
function FloatingButton({ icon, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
tabindex: 0,
class: `notion-enhancer--floating-button
size-[36px] flex items-center justify-center rounded-full
text-([20px] [color:var(--theme--fg-primary)]) select-none cursor-pointer
bg-[color:var(--theme--bg-secondary)] hover:bg-[color:var(--theme--bg-hover)]
shadow-[rgba(15,15,15,0.2)_0px_0px_0px_1px,rgba(15,15,15,0.2)_0px_2px_4px]`,
});
return html`<button ...${props}>${children}</button>`;
}
if (globalThis.document) setupWrapper();
Object.assign((globalThis.__enhancerApi ??= {}), {
addFloatingButton,
removeFloatingButton,
});
export { addFloatingButton, FloatingButton };

View File

@ -1,43 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function MenuButton(
{ icon, notifications, themeOverridesLoaded, ...props },
...children
) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
tabindex: 0,
role: "button",
class: `notion-enhancer--menu-button flex select-none
cursor-pointer rounded-[6px] text-[14px] font-medium
transition hover:bg-[color:var(--theme--bg-hover)]
w-full h-[30px] px-[10px] py-[4px] items-center`,
});
return html`<div ...${props}>
<div class="flex items-center justify-center text-[18px] mr-[10px]">
<i class="i-${icon}"></i>
</div>
<div>${children}</div>
<div class="ml-auto my-auto${notifications > 0 ? "" : " hidden"}">
<!-- accents are squashed into one variable for theming:
use rgb to match notion if overrides not loaded -->
<div
class="flex justify-center size-[16px] font-semibold mb-[2px]
text-([10px] [color:var(--theme--accent-secondary\\_contrast)])
bg-[color:var(--theme--accent-secondary)] rounded-[3px]
dark:bg-[color:${themeOverridesLoaded
? "var(--theme--accent-secondary)"
: "rgb(180,65,60)"}]"
>
<span class="ml-[-0.5px]">${notifications}</span>
</div>
</div>
</div>`;
}
export { MenuButton };

View File

@ -1,67 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Modal(props, ...children) {
const { html, extendProps, addKeyListener } = globalThis.__enhancerApi;
extendProps(props, {
class: `notion-enhancer--menu-modal z-[999]
fixed inset-0 w-screen h-screen group/modal
transition pointer-events-none opacity-0
open:(pointer-events-auto opacity-100)`,
});
const $modal = html`<div ...${props}>
<div
class="fixed inset-0 bg-[color:var(--theme--bg-overlay)]"
onclick=${() => $modal.close()}
></div>
<div
class="fixed inset-0 flex w-screen h-screen
items-center justify-center pointer-events-none"
>
${children}
</div>
</div>`;
let _openQueued;
$modal.open = async () => {
_openQueued = true;
while (!document.contains($modal)) {
if (!_openQueued) return;
// dont trigger open until menu is in dom,
// to ensure transition is shown when menu
// does initially open
await new Promise(requestAnimationFrame);
}
$modal.setAttribute("open", "");
setTimeout(() => $modal.onopen?.(), 200);
};
$modal.close = () => {
_openQueued = false;
$modal.removeAttribute("open");
if ($modal.contains(document.activeElement)) {
document.activeElement.blur();
}
setTimeout(() => $modal.onclose?.(), 200);
};
addKeyListener("Escape", () => {
if (document.activeElement?.nodeName === "INPUT") return;
$modal.close();
});
return $modal;
}
function Frame(props) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `rounded-[12px] w-[1150px] h-[calc(100vh-100px)] opacity-0
max-w-[calc(100vw-100px)] max-h-[715px] overflow-hidden scale-95
bg-[color:var(--theme--bg-primary)] drop-shadow-xl transition
group-open/modal:(pointer-events-auto opacity-100 scale-100)`,
});
return html`<iframe ...${props}></iframe>`;
}
export { Modal, Frame };

View File

@ -1,433 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Tooltip } from "./Tooltip.mjs";
import { TopbarButton } from "./TopbarButton.mjs";
import { Select } from "../menu/islands/Select.mjs";
const coreId = "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
topbarId = "e0700ce3-a9ae-45f5-92e5-610ded0e348d",
tweaksId = "5174a483-c88d-4bf8-a95f-35cd330b76e2";
// note: these islands are not reusable.
// panel views can be added via addPanelView,
// do not instantiate additional panels
let panelViews = [],
// "$icon" may either be an actual dom element,
// or an icon name from the lucide icons set
addPanelView = ({ title, $icon, $view }) => {
panelViews.push([{ title, $icon }, $view]);
panelViews.sort(([{ title: a }], [{ title: b }]) => a.localeCompare(b));
const { setState } = globalThis.__enhancerApi;
setState?.({ panelViews });
},
removePanelView = ($view) => {
panelViews = panelViews.filter(([, v]) => v !== $view);
const { setState } = globalThis.__enhancerApi;
setState?.({ panelViews });
};
function View({ _get }) {
const { html, useState } = globalThis.__enhancerApi,
$container = html`<div
class="overflow-(y-auto x-hidden)
h-[calc(100%-46px)] min-w-[var(--panel--width)]"
></div>`;
useState(["rerender"], async () => {
const openView = await _get?.(),
$view =
panelViews.find(([{ title }]) => {
return title === openView;
})?.[1] || panelViews[0]?.[1];
if (!$container.contains($view)) {
$container.innerHTML = "";
$container.append($view);
}
});
return $container;
}
function Switcher({ _get, _set, minWidth, maxWidth }) {
const { html, useState } = globalThis.__enhancerApi,
$select = html`<${Select}
popupMode="dropdown"
class="w-full text-left"
maxWidth=${maxWidth - 56}
minWidth=${minWidth - 56}
...${{ _get, _set }}
/>`;
useState(["panelViews"], ([panelViews = []]) => {
const values = panelViews.map(([{ title, $icon }]) => {
// panel switcher internally uses the select island,
// which expects an option value rather than a title
return { value: title, $icon };
});
$select.setValues(values);
});
return html`<div
class="relative flex items-center grow
font-medium p-[8.5px] ml-[4px] select-none"
>
${$select}
</div>`;
}
function Panel({
hotkey,
_getWidth,
_setWidth,
_getOpen,
_setOpen,
_getView,
_setView,
minWidth = 256,
maxWidth = 640,
transitionDuration = 300,
}) {
const { modDatabase, isEnabled } = globalThis.__enhancerApi,
{ html, useState, addKeyListener, MODS_LOADED } = globalThis.__enhancerApi,
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi,
$panelToggle = html`<button
aria-label="Toggle side panel"
class="select-none size-[24px] duration-[20ms]
transition inline-flex items-center justify-center mr-[10px]
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]"
>
<i
class="i-chevrons-left size-[20px]
text-[color:var(--theme--fg-secondary)] transition-transform
group-[&[data-pinned]]/panel:rotate-180 duration-[${transitionDuration}ms]"
/>
</button>`,
$panel = html`<div
class="notion-enhancer--panel group/panel order-2
shrink-0 [&[data-pinned]]:w-[var(--panel--width,0)]"
>
<style>
.notion-frame {
flex-direction: row !important;
}
/* prevent page load skeletons overlapping with panel */
.notion-frame [role="progressbar"] {
padding-right: var(--panel--width);
}
.notion-frame [role="progressbar"] > div {
overflow-x: clip;
}
</style>
<aside
class="border-(l-1 [color:var(--theme--fg-border)]) w-0
group-[&[data-pinned]]/panel:(w-[var(--panel--width,0)]) h-[calc(100vh-45px)] bottom-0)
absolute right-0 z-20 bg-[color:var(--theme--bg-primary)] group-[&[data-peeked]]/panel:(
w-[var(--panel--width,0)] h-[calc(100vh-120px)] bottom-[60px] rounded-l-[8px] border-(t-1 b-1))"
>
<div
class="flex justify-between items-center
border-(b [color:var(--theme--fg-border)])"
>
<${Switcher}
...${{ _get: _getView, _set: _setView, minWidth, maxWidth }}
/>
${$panelToggle}
</div>
<${View} ...${{ _get: _getView }} />
</aside>
</div>`;
const notionTopbar = ".notion-topbar",
topbarFavorite = ".notion-topbar-favorite-button",
$topbarToggle = html`<${TopbarButton}
aria-label="Toggle side panel"
icon="panel-right"
/>`,
addToTopbar = () => {
if (document.contains($topbarToggle)) return;
document.querySelector(topbarFavorite)?.after($topbarToggle);
};
$panelToggle.onclick = $topbarToggle.onclick = () => $panel.toggle();
addMutationListener(notionTopbar, addToTopbar, { subtree: false });
addToTopbar();
isEnabled(topbarId).then(async (topbarEnabled) => {
if (!topbarEnabled) return;
const topbarDatabase = await modDatabase(topbarId),
panelButton = await topbarDatabase.get("panelButton"),
panelIcon = await topbarDatabase.get("panelIcon");
if (panelButton === "Text") {
$topbarToggle.innerHTML = `<span>${$topbarToggle.ariaLabel}</span>`;
} else if (panelIcon?.content) $topbarToggle.innerHTML = panelIcon.content;
});
let preDragWidth, dragStartX, _animatedAt;
const getWidth = async (width) => {
if (width && !isNaN(width)) {
width = Math.max(width, minWidth);
width = Math.min(width, maxWidth);
} else width = await _getWidth?.();
if (isNaN(width)) width = minWidth;
return width;
},
setInteractive = (interactive) => {
$panel
.querySelectorAll("[tabindex]")
.forEach(($el) => ($el.tabIndex = interactive ? 1 : -1));
},
isAnimated = () => {
if (!_animatedAt) return false;
return Date.now() - _animatedAt <= transitionDuration;
},
isDragging = () => !isNaN(preDragWidth) && !isNaN(dragStartX),
isPinned = () => $panel.hasAttribute("data-pinned"),
isPeeked = () => $panel.hasAttribute("data-peeked"),
isClosed = () => !isPinned() && !isPeeked();
const closedWidth = { width: "0px" },
openWidth = { width: "var(--panel--width, 0px)" },
peekAnimation = {
height: "calc(100vh - 120px)",
bottom: "60px",
borderTopWidth: "1px",
borderBottomWidth: "1px",
borderTopLeftRadius: "8px",
borderBottomLeftRadius: "8px",
boxShadow: document.body.classList.contains("dark")
? "rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.2) 0px 3px 6px, rgba(15, 15, 15, 0.4) 0px 9px 24px"
: "rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px",
},
pinAnimation = {
height: "calc(100vh - 45px)",
bottom: "0px",
borderTopWidth: "0px",
borderBottomWidth: "0px",
borderTopLeftRadius: "0px",
borderBottomLeftRadius: "0px",
boxShadow: "none",
};
const animationState = { ...closedWidth },
animate = ($target, keyframes) => {
const opts = {
fill: "forwards",
duration: transitionDuration,
easing: "ease",
};
$target.animate(keyframes, opts);
},
animatePanel = (to) => {
_animatedAt = Date.now();
animate($panel.lastElementChild, [animationState, to]);
Object.assign(animationState, to);
};
isEnabled(tweaksId).then(async (tweaksEnabled) => {
if (!tweaksEnabled) return;
const tweaksDatabase = await modDatabase(tweaksId),
snappyTransitions = await tweaksDatabase.get("snappyTransitions");
if (snappyTransitions) transitionDuration = 0;
});
// dragging the resize handle horizontally will
// adjust the width of the panel correspondingly
const $resizeHandle = html`<div
class="absolute opacity-0 h-full w-[3px] left-[-2px]
active:cursor-text bg-[color:var(--theme--fg-border)] z-20
transition duration-300 hover:(cursor-col-resize opacity-100)
group-[&[data-peeked]]/panel:(w-[8px] left-[-1px] rounded-l-[7px])"
>
<div
class="ml-[2px] bg-[color:var(--theme--bg-primary)]
group-[&[data-peeked]]/panel:(my-px h-[calc(100%-2px)] rounded-l-[6px])"
></div>
</div>`,
startDrag = async (event) => {
dragStartX = event.clientX;
preDragWidth = await getWidth();
document.addEventListener("mousemove", onDrag);
document.addEventListener("mouseup", endDrag);
},
onDrag = (event) => {
event.preventDefault();
if (!isDragging()) return;
$panel.resize(preDragWidth + (dragStartX - event.clientX));
},
endDrag = (event) => {
document.removeEventListener("mousemove", onDrag);
document.removeEventListener("mouseup", endDrag);
if (!isDragging()) return;
$panel.resize(preDragWidth + (dragStartX - event.clientX));
// toggle panel if not resized
if (dragStartX - event.clientX === 0) $panel.toggle();
preDragWidth = dragStartX = undefined;
};
$resizeHandle.addEventListener("mousedown", startDrag);
$panel.lastElementChild.prepend($resizeHandle);
// add tooltips to panel pin/unpin toggles
const $resizeTooltipClick = html`<span></span>`,
$resizeTooltip = html`<${Tooltip}
onbeforeshow=${() => {
$resizeTooltipClick.innerText = isPinned() ? "close" : "lock open";
}}
><b>Drag</b> to resize<br />
<b>Click</b> to ${$resizeTooltipClick}
<//>`,
$toggleTooltipClick = html`<b></b>`,
$toggleTooltip = html`<${Tooltip}
onbeforeshow=${() => {
$toggleTooltipClick.innerText = isPinned()
? "Close sidebar"
: "Lock sidebar open";
}}
>${$toggleTooltipClick}<br />
${hotkey}
<//>`;
$resizeTooltip.attach($resizeHandle, "left");
$toggleTooltip.attach($topbarToggle, "bottom");
$toggleTooltip.attach($panelToggle, "bottom");
// hovering over the peek trigger will temporarily
// pop out an interactive preview of the panel
let _peekDebounce, _peekPanelOnHover;
const $peekTrigger = html`<div
class="absolute z-10 right-0 h-[calc(100vh-120px)] bottom-[60px] w-[96px]
group-[&[data-peeked]]/panel:(w-[calc(var(--panel--width,0)+8px)])
group-[&[data-pinned]]/panel:(w-[calc(var(--panel--width,0)+8px)])"
></div>`;
modDatabase(coreId).then(async (db) => {
_peekPanelOnHover = await db.get("peekPanelOnHover");
if (_peekPanelOnHover) $panel.prepend($peekTrigger);
});
$panel.addEventListener("mouseout", () => {
if (isDragging() || isAnimated() || isPinned()) return;
if (!$panel.matches(":hover")) $panel.close();
});
$panel.addEventListener("mouseover", () => {
_peekDebounce ??= setTimeout(() => {
if (isClosed() && $panel.matches(":hover")) $panel.peek();
_peekDebounce = undefined;
}, 100);
});
// moves ai/q&a button out of the way of open panel.
// normally would place outside of an island, but in
// this case is necessary for syncing up animations
const notionAi = ".notion-help-button, .notion-ai-button",
floatingButtons = ".notion-enhancer--floating-buttons",
repositionCorner = async (offset) => {
const $help = document.querySelector(notionAi),
$floating = document.querySelector(floatingButtons);
offset ??= await getWidth();
if (isNaN(offset)) offset = minWidth;
if (!isPinned()) offset = 0;
// offset help from panel edge
offset += 26;
for (const $btn of [$help, $floating]) {
if (!$btn) continue;
const computedStyles = getComputedStyle($btn),
visible = computedStyles.getPropertyValue("display") !== "none";
if (!visible) continue;
const width = computedStyles.getPropertyValue("width"),
from = computedStyles.getPropertyValue("right"),
to = offset + "px";
// offset floating buttons from help
offset += 12 + parseInt(width);
if (from === to) continue;
$btn.style.setProperty("right", to);
animate($btn, [({ right: from }, { right: to })]);
}
if ($help || $floating) removeMutationListener(repositionCorner);
};
const corner = `${notionAi}, ${floatingButtons}`;
addMutationListener(corner, repositionCorner, { subtree: false });
MODS_LOADED.then(() => repositionCorner());
$panel.pin = () => {
if (isPinned() || !panelViews.length) return;
if (isClosed()) Object.assign(animationState, pinAnimation);
animatePanel({ ...openWidth, ...pinAnimation });
animate($panel, [closedWidth, openWidth]);
$panel.removeAttribute("data-peeked");
$panel.dataset.pinned = true;
$topbarToggle.setAttribute("data-active", true);
setInteractive(true);
_setOpen(true);
$panel.resize();
};
$panel.peek = () => {
if (!_peekPanelOnHover) return;
if (isPeeked() || !panelViews.length) return;
if (isClosed()) Object.assign(animationState, peekAnimation);
animatePanel({ ...openWidth, ...peekAnimation });
// closing on mouseout is disabled mid-animation,
// queue close in case mouse is no longer peeking
// after the initial animation is complete
setTimeout(() => {
if (!isDragging() && !$panel.matches(":hover")) $panel.close();
}, transitionDuration);
$panel.removeAttribute("data-pinned");
$panel.dataset.peeked = true;
setInteractive(true);
$panel.resize();
};
$panel.close = async () => {
if (isClosed()) return;
if (panelViews.length) _setOpen(false);
$topbarToggle.removeAttribute("data-active");
const width = (animationState.width = `${await getWidth()}px`);
// only animate container close if it is actually taking up space,
// otherwise will unnaturally grow + retrigger peek on peek mouseout
if (isPinned()) animate($panel, [{ width }, closedWidth]);
if (!$panel.matches(":hover") || !_peekPanelOnHover) {
$panel.removeAttribute("data-pinned");
$panel.removeAttribute("data-peeked");
animatePanel(closedWidth);
setInteractive(false);
$panel.resize();
} else $panel.peek();
};
$panel.toggle = () => {
if (isPinned()) $panel.close();
else $panel.pin();
};
// resizing handles visual resizes (inc. setting width to 0
// if closed) and actual resizes on drag (inc. saving to db)
$panel.resize = async (width) => {
$resizeTooltip.hide();
width = await getWidth(width);
_setWidth?.(width);
// works in conjunction with animations, acts as fallback
// plus updates dependent styles e.g. page skeleton padding
if (isClosed()) width = 0;
const $parent = $panel.parentElement || $panel;
$parent.style.setProperty("--panel--width", `${width}px`);
if ($parent !== $panel) $panel.style.removeProperty("--panel--width");
repositionCorner(width);
};
useState(["panelViews"], async ([panelViews = []]) => {
$topbarToggle.style.display = panelViews.length ? "" : "none";
if (panelViews.length && (await _getOpen())) $panel.pin();
else $panel.close();
});
if (!hotkey) return $panel;
addKeyListener(hotkey, (event) => {
event.preventDefault();
event.stopPropagation();
$panel.toggle();
});
return $panel;
}
Object.assign((globalThis.__enhancerApi ??= {}), {
addPanelView,
removePanelView,
});
export { Panel };

View File

@ -1,94 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Tooltip(props, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
role: "dialog",
class: `absolute group/tooltip z-[999] text-center pointer-events-none`,
});
const notionApp = ".notion-app-inner",
$tooltip = html`<div ...${props}>
<div
class="bg-[color:var(--theme--bg-secondary)]
text-([color:var(--theme--fg-secondary)] [12px] nowrap)
leading-[1.4] font-medium py-[4px] px-[8px] rounded-[4px]
drop-shadow-md transition duration-100 opacity-0
group-open/tooltip:(pointer-events-auto opacity-100)
[&>b]:text-[color:var(--theme--fg-primary)]"
>
${children}
</div>
</div>`;
// can pass each coord as a number or a function
$tooltip.show = (x, y) => {
const $notionApp = document.querySelector(notionApp);
if (!document.contains($tooltip)) $notionApp?.append($tooltip);
if ($tooltip.hasAttribute("open")) return;
$tooltip.onbeforeshow?.();
const edgePadding = 12,
{ clientHeight, clientWidth } = document.documentElement;
requestAnimationFrame(() => {
if (typeof x === "function") x = x();
if (typeof y === "function") y = y();
if (x < edgePadding) x = $tooltip.clientWidth + edgePadding;
if (x + $tooltip.clientWidth > clientWidth - edgePadding)
x = clientWidth - $tooltip.clientWidth - edgePadding;
if (y < edgePadding) y = $tooltip.clientHeight + edgePadding;
if (y + $tooltip.clientHeight > clientHeight - edgePadding)
y = clientHeight - $tooltip.clientHeight - edgePadding;
$tooltip.style.left = `${x}px`;
$tooltip.style.top = `${y}px`;
$tooltip.setAttribute("open", true);
$tooltip.onshow?.();
});
};
$tooltip.hide = () => {
$tooltip.onbeforehide?.();
$tooltip.removeAttribute("open");
setTimeout(() => {
$tooltip.onhide?.();
}, 200);
};
$tooltip.attach = ($target, alignment = "") => {
$target.addEventListener("mouseover", (event) => {
setTimeout(() => {
if (!$target.matches(":hover")) return;
const x = () => {
const rect = $target.getBoundingClientRect();
if (["top", "bottom"].includes(alignment)) {
return rect.left + rect.width / 2 - $tooltip.clientWidth / 2;
} else if (alignment === "left") {
return rect.left - $tooltip.clientWidth - 6;
} else if (alignment === "right") {
return rect.right + 6;
} else return event.clientX;
},
y = () => {
const rect = $target.getBoundingClientRect();
if (["left", "right"].includes(alignment)) {
// match mouse alignment if hovering over large
// target e.g. panel resize handle, otherwise centre
return rect.height > $tooltip.clientHeight * 2
? event.clientY - $tooltip.clientHeight / 2
: rect.top + rect.height / 2 - $tooltip.clientHeight / 2;
} else if (alignment === "top") {
return rect.top - $tooltip.clientHeight - 6;
} else if (alignment === "bottom") {
return rect.bottom + 6;
} else return event.clientY;
};
$tooltip.show(x, y);
}, 200);
});
$target.addEventListener("mouseout", $tooltip.hide);
};
return $tooltip;
}
export { Tooltip };

View File

@ -1,29 +0,0 @@
/**
* notion-enhancer
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function TopbarButton({ icon, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
tabindex: 0,
class: `notion-enhancer--topbar-button
text-[color:var(--theme--fg-primary)] mr-[2px]
select-none h-[28px] w-[33px] duration-[20ms]
transition inline-flex items-center justify-center
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]
has-[span]:w-auto [&>span]:(text-[14px] leading-[1.2] px-[8px])
[&[data-active]]:bg-[color:var(--theme--bg-hover)]
[&>i]:size-[20px]`,
});
// [role="button"] == `-webkit-app-region: no-drag`
return html`<div role="button" ...${props}>
${props.innerHTML || children.length
? children
: html`<i class="i-${icon}" />`}
</div>`;
}
export { TopbarButton };

View File

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base target="_blank" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>notion-enhancer menu</title>
<link rel="stylesheet" href="./menu.css" />
<link rel="stylesheet" href="../../vendor/coloris.min.css" />
<script src="../../vendor/coloris.min.js" type="module"></script>
<script src="./menu.mjs" type="module" defer></script>
</head>
<body>
<div id="skeleton">
<div class="row row-group">
<div class="shimmer" style="width: 110px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 60px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 72px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 78px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 96px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 79px"></div>
</div>
<div class="row row-group">
<div class="shimmer" style="width: 53px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 30px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 48px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 65px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 75px"></div>
</div>
</div>
</body>
</html>

View File

@ -1,192 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Popup } from "./Popup.mjs";
import { Button } from "./Button.mjs";
import { Description } from "./Description.mjs";
const updateGuide =
"https://notion-enhancer.github.io/getting-started/updating/",
tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/";
const rectToStyle = (rect) =>
["width", "height", "top", "bottom", "left", "right"]
.filter((prop) => rect[prop])
.map((prop) => `${prop}: ${rect[prop]};`)
.join("");
function Star({ from, ...rect }) {
const { html } = globalThis.__enhancerApi;
return html`<svg
viewBox="0 0 24 24"
class="absolute fill-none skew-y-2${from
? ` hidden ${from}:inline-block`
: ""}"
xmlns="http://www.w3.org/2000/svg"
style=${rectToStyle(rect)}
>
<path
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
fill="#FDCC80"
/>
<path
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
fill="url(#paint0_linear_3_70)"
/>
<path
d="M11.3255 22.5826C11.3255 22.8897 11.5745 23.1387 11.8816 23.1387C12.1887 23.1387 12.4377 22.8897 12.4377 22.5826C12.4377 19.3351 13.2489 16.7277 14.8868 14.848C16.5218 12.9717 19.044 11.7477 22.6145 11.3906C22.9201 11.3601 23.1431 11.0875 23.1125 10.7819C23.082 10.4763 22.8094 10.2533 22.5038 10.2839C18.9253 10.6417 16.4423 9.91532 14.8501 8.40653C13.2524 6.89252 12.4377 4.48364 12.4377 1.22746C12.4377 0.920325 12.1887 0.67134 11.8816 0.67134C11.5745 0.67134 11.3255 0.920325 11.3255 1.22746C11.3255 4.47517 10.516 7.08239 8.87909 8.96186C7.24516 10.8379 4.72305 12.062 1.1487 12.4194C0.843091 12.45 0.620117 12.7225 0.650678 13.0281C0.681239 13.3337 0.953763 13.5567 1.25938 13.5261C4.84181 13.1679 7.32467 13.8944 8.91581 15.4031C10.5125 16.9171 11.3255 19.3261 11.3255 22.5826Z"
stroke="#FDCC80"
stroke-width="1.11225"
stroke-linejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_3_70"
x1="11.8816"
y1="1.22746"
x2="11.8816"
y2="22.5826"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FFE171" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
</svg>`;
}
function Circle(rect) {
const { html } = globalThis.__enhancerApi;
return html`<div
class="absolute rounded-full
border-(~ purple-500) bg-purple-400"
style=${rectToStyle(rect)}
></div>`;
}
function Banner({ updateAvailable, isDevelopmentBuild }) {
const { html, useState } = globalThis.__enhancerApi,
{ version, initDatabase } = globalThis.__enhancerApi,
$version = html`<button
class="text-[12px] py-[2px] px-[6px] mt-[2px]
font-medium leading-tight tracking-wide rounded-[3px]
relative bg-purple-500 from-white/[0.18] to-white/[0.16]
bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
>
<div
class="notion-enhancer--menu-update-indicator
absolute size-[12px] right-[-6px] top-[-6px]
${updateAvailable ? "" : "hidden"}"
>
<span
class="block rounded-full h-full w-full
absolute bg-purple-500/75 animate-ping"
></span>
<span
class="block rounded-full h-full w-full
relative bg-purple-500"
></span>
</div>
<span class="relative">v${version}</span>
</button>`,
$popup = html`<${Popup} trigger=${$version}>
<p
class="typography py-[2px] px-[8px] text-[14px]"
innerHTML=${updateAvailable
? `<b>v${updateAvailable}</b> is available! <a href="${updateGuide}">Update now.</a>`
: isDevelopmentBuild
? "This is a development build of the notion-enhancer. It may be unstable."
: "You're up to date!"}
/>
<//>`;
$version.append($popup);
if (updateAvailable) {
useState(["focus", "view"], ([, view = "welcome"]) => {
if (view !== "welcome") return;
// delayed appearance = movement attracts eye
setTimeout(() => $popup.open(), 400);
});
}
const $welcome = html`<div
class="relative flex overflow-hidden h-[192px] rounded-t-[4px]
border-(~ purple-400) bg-purple-500 from-white/20 to-transparent
text-white bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
>
<${Circle} width="128px" height="128px" bottom="-64px" left="-64px" />
<${Circle} width="144px" height="144px" top="-108px" left="80px" />
<${Circle} width="208px" height="208px" bottom="-64px" right="-16px" />
<${Circle} width="144px" height="144px" bottom="-72px" right="144px" />
<${Star} width="36px" height="36px" top="136px" left="190px" />
<${Star} width="48px" height="48px" top="32px" left="336px" />
<${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
<h1
class="z-10 px-[32px] md:px-[48px] lg:px-[64px]
font-bold leading-tight tracking-tight my-auto"
>
<a href="https://notion-enhancer.github.io/">
<span class="text-[26px]">Welcome to</span><br />
<span class="text-[28px]">the notion-enhancer</span>
</a>
</h1>
<div
class="absolute bottom-0 right-0 py-[24px]
px-[32px] md:px-[48px] lg:px-[64px]"
>
<div class="relative flex-(~ col)">
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
${$version}
</div>
</div>
</div>`,
$sponsorship = html`<div
class="py-[18px] px-[16px] rounded-b-[4px]
border-(~ [color:var(--theme--fg-border)]) bg-[color:var(--theme--bg-secondary)]"
>
<div class="flex items-center gap-[16px]">
<p class="text-[14px] font-semibold">
Enjoying the notion-enhancer?<br />
Support future development:
</p>
<${Button}
icon="coffee"
variant="brand"
class="grow justify-center"
href="https://www.buymeacoffee.com/dragonwocky"
>Buy me a coffee
<//>
<${Button}
icon="calendar-heart"
variant="brand"
class="grow justify-center"
href="https://github.com/sponsors/dragonwocky"
>Sponsor me
<//>
</div>
<!-- Disclaimer: draft of potential perks, to be confirmed before full release. -->
<${Description} class="mt-[6px]">
<!-- Sponsors help make open-source development sustainable and receive
access to priority support channels, private developer previews, and
role cosmetics on Discord. A one-time donation is equivalent to 1 month
of sponsor perks. To learn more about perks, read the
<a href=${tsAndCs} class="ml-[3px]">Terms & Conditions</a>. -->
<//>
</div>`;
initDatabase()
.get("agreedToTerms")
.then((agreedToTerms) => {
// only show sponsorship if already agree to terms
// and opening menu after having reloaded since agreeing
$welcome.style.borderRadius = agreedToTerms === version ? "" : "4px";
$sponsorship.style.display = agreedToTerms === version ? "" : "none";
});
return html`<section class="notion-enhancer--menu-banner">
${$welcome}${$sponsorship}
</section>`;
}
export { Banner };

View File

@ -1,45 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Button({ icon, variant, tagName, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `notion-enhancer--menu-button shrink-0
flex gap-[8px] items-center px-[12px] rounded-[4px]
h-[${variant === "sm" ? "28" : "32"}px] select-none
transition duration-[20ms] ${
variant === "primary"
? `text-[color:var(--theme--accent-primary\\_contrast)]
font-medium bg-[color:var(--theme--accent-primary)]
hover:bg-[color:var(--theme--accent-primary\\_hover)]`
: variant === "secondary"
? `text-[color:var(--theme--accent-secondary)]
border-(~ [color:var(--theme--accent-secondary)])
hover:bg-[color:var(--theme--accent-secondary\\_hover)]`
: variant === "brand"
? `text-white border-(~ purple-400)
bg-purple-500 hover:(from-white/20 to-transparent
bg-[linear-gradient(225deg,var(--tw-gradient-stops))])`
: `border-(~ [color:var(--theme--fg-border)])
not-disabled:hover:bg-[color:var(--theme--bg-hover)]
disabled:text-[color:var(--theme--fg-secondary)]`
}`,
});
tagName ??= props["href"] ? "a" : "button";
return html`<${tagName} ...${props}>
${icon
? html`<i
class="i-${icon}
text-[${variant === "sm" && children.length ? "13" : "17"}px]"
></i>`
: ""}
<span class="text-[${variant === "sm" ? "13" : "14"}px] empty:hidden">
${children}
</span>
<//>`;
}
export { Button };

View File

@ -1,47 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Checkbox({ _get, _set, _requireReload = true, ...props }) {
let _initialValue;
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
$input = html`<input
type="checkbox"
class="hidden [&:checked+div]:(px-px bg-[color:var(--theme--accent-primary)])
[&:not(:checked)+div>i]:text-transparent [&:not(:checked)+div]:(border-(~
[color:var(--theme--fg-primary)]) hover:bg-[color:var(--theme--bg-hover)])"
...${props}
/>`;
extendProps($input, { onchange: () => _set?.($input.checked) });
useState(["rerender"], async () => {
const checked = (await _get?.()) ?? $input.checked;
$input.checked = checked;
if (_requireReload) {
_initialValue ??= checked;
if (checked !== _initialValue) setState({ databaseUpdated: true });
}
});
return html`<label
tabindex="0"
class="notion-enhancer--menu-checkbox cursor-pointer"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
>
${$input}
<div class="flex items-center h-[16px] transition duration-200">
<i
class="i-check size-[14px]
text-[color:var(--theme--accent-primary\\_contrast)]"
></i>
</div>
</label>`;
}
export { Checkbox };

View File

@ -1,16 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Description(props, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `notion-enhancer--menu-description typography
leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
});
return html`<p ...${props}>${children}</p>`;
}
export { Description };

View File

@ -1,59 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Button } from "./Button.mjs";
function Footer({ categories, transitionDuration = 150 }) {
const { html, setState, useState, reloadApp } = globalThis.__enhancerApi,
$reload = html`<${Button}
class="ml-auto"
variant="primary"
icon="refresh-cw"
onclick=${reloadApp}
style="display: none"
>Reload & Apply Changes
<//>`,
$categories = categories.map(({ id, title, mods }) => {
return [
mods.map((mod) => mod.id),
html`<${Button}
icon="chevron-left"
onclick=${() => setState({ transition: "slide-to-left", view: id })}
>${title}
<//>`,
];
});
useState(["view"], ([view]) => {
let [footerOpen] = useState(["databaseUpdated"]);
footerOpen ||= $categories.some(([ids]) => ids.some((id) => id === view));
setState({ footerOpen });
});
useState(["databaseUpdated"], ([databaseUpdated]) => {
$reload.style.display = databaseUpdated ? "" : "none";
if (databaseUpdated) setState({ footerOpen: true });
});
useState(["footerOpen"], ([footerOpen]) => {
// only toggle buttons if footer is open,
// otherwise leave as is during transition
if (!footerOpen) return;
const [view] = useState(["view"]);
for (const [ids, $btn] of $categories) {
const viewInCategory = ids.some((id) => id === view);
$btn.style.display = viewInCategory ? "" : "none";
}
});
return html`<footer
class="notion-enhancer--menu-footer px-[60px] py-[16px]
flex w-full bg-[color:var(--theme--bg-primary)] h-[64px]
border-t-(~ [color:var(--theme--fg-border)])"
>
${$categories.map(([, $btn]) => $btn)}${$reload}
</footer>`;
}
export { Footer };

View File

@ -1,17 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Heading(props, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `notion-enhancer--menu-heading text-[16px]
font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
border-b-(~ [color:var(--theme--fg-border)])`,
});
return html`<h4 ...${props}>${children}</h4>`;
}
export { Heading };

View File

@ -1,200 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
const updateHotkey = (event) => {
const keys = [];
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
if (!event[modifier]) continue;
const alias = modifier[0].toUpperCase() + modifier.slice(1, -3);
keys.push(alias);
}
// retain keyboard navigation of menu
if (["Tab", "Escape"].includes(event.key) && !keys.length) {
return;
} else event.preventDefault();
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
event.target.value = "";
} else if (event.key) {
let key = event.key;
if (key === " ") key = "Space";
if (["+", "="].includes(key)) key = "Plus";
if (key === "-") key = "Minus";
if (key === "|") key = "\\";
if (event.code === "Comma") key = ",";
if (event.code === "Period") key = ".";
if (key === "Control") key = "Ctrl";
// avoid e.g. Shift+Shift, force inclusion of non-modifier
if (keys.includes(key)) return;
keys.push(key.length === 1 ? key.toUpperCase() : key);
event.target.value = keys.join("+");
}
event.target.dispatchEvent(new Event("input"));
event.target.dispatchEvent(new Event("change"));
},
updateContrast = ($input, $icon) => {
$input.style.background = $input.value;
const [r, g, b, a = 1] = $input.value
.replace(/^rgba?\(/, "")
.replace(/\)$/, "")
.split(",")
.map((n) => parseFloat(n));
if (a > 0.5) {
// pick a contrasting foreground for an rgb background
// using the percieved brightness constants from http://alienryderflex.com/hsp.html
const brightness = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b);
$input.style.color = Math.sqrt(brightness) > 165.75 ? "#000" : "#fff";
} else $input.style.color = "#000";
$icon.style.color = $input.style.color;
$icon.style.opacity = "0.7";
},
readUpload = async (event) => {
const file = event.target.files[0],
reader = new FileReader();
return new Promise((res) => {
reader.onload = async (progress) => {
const content = progress.currentTarget.result,
upload = { filename: file.name, content };
res(upload);
};
reader.readAsText(file);
});
};
function Input({
type,
icon,
variant,
extensions,
class: className,
_get,
_set,
_requireReload = true,
...props
}) {
let $filename, $clear;
const { html, extendProps, setState, useState } = globalThis.__enhancerApi;
Coloris({ format: "rgb" });
type ??= "text";
if (type === "text") icon ??= "text-cursor";
if (type === "number") icon ??= "hash";
if (type === "hotkey") icon ??= "command";
if (type === "color") icon ??= "pipette";
if (type === "file") {
icon ??= "file-up";
$filename = html`<span class="ml-[6px]">Upload a file</span>`;
$clear = html`<button
style="display: none"
class="h-[14px] transition duration-[20ms]
flex text-[color:var(--theme--fg-secondary)]
hover:text-[color:var(--theme--fg-primary)]"
onclick=${() => _set?.({ filename: "", content: "" })}
>
<i class="i-x size-[14px]"></i>
</button>`;
props.accept = extensions
?.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
.join(",");
}
const $input = html`<input
type=${["hotkey", "color"].includes(type) ? "text" : type}
class="h-full w-full pb-px text-[14px] leading-[1.2]
${variant === "lg" ? "pl-[12px] pr-[40px]" : "pl-[8px] pr-[32px]"}
appearance-none bg-transparent ${type === "file" ? "hidden" : ""}
${type === "hotkey" ? "text-[color:var(--theme--fg-secondary)]" : ""}
${type === "color"
? "font-medium"
: "border-(~ [color:var(--theme--fg-border)])"}"
data-coloris=${type === "color"}
...${props}
/>`,
$icon = html`<span
class="${variant === "lg" ? "pr-[12px]" : "pr-[8px]"}
absolute flex items-center h-full pointer-events-none
text-[color:var(--theme--fg-secondary)] right-0 top-0"
><i class="i-${icon} size-[16px]"></i>
</span>`;
let _initialValue;
extendProps($input, {
onclick: () => {
// change text to "uploading..." until file has uploaded
// to reassure users experiencing latency while file is processed
if (type === "file") $filename.innerText = "Uploading...";
},
onchange: (event) => {
if (_set && type === "file") {
readUpload(event)
.then(_set)
// refocus iframe after file has uploaded,
// sometimes switching back after opening the
// native file upload menu causes a loss of focus
.then(() => window.focus());
} else _set?.($input.value);
},
onrerender: async () => {
const value = (await _get?.()) ?? $input.value ?? "";
if (type === "file") {
$filename.innerText = value?.filename || "Upload a file";
$clear.style.display = value?.filename ? "" : "none";
if (_requireReload) {
_initialValue ??= value?.content || "";
if ((value?.content || "") !== _initialValue) {
setState({ databaseUpdated: true });
}
}
} else {
if ($input.value !== value) $input.value = value;
if (_requireReload) {
_initialValue ??= value;
if (value !== _initialValue) setState({ databaseUpdated: true });
}
if (type === "color") updateContrast($input, $icon);
}
},
onkeydown: type === "hotkey" ? updateHotkey : undefined,
oninput: type === "color" ? () => _set?.($input.value) : undefined,
});
useState(["rerender"], () => $input.onrerender?.());
return type === "file"
? html`<div
class="notion-enhancer--menu-file-input shrink-0
flex items-center gap-[8px] ${className ?? ""}"
>
<label
tabindex="0"
class="flex items-center cursor-pointer select-none
px-[8px] bg-[color:var(--theme--bg-secondary)]
h-[28px] rounded-[4px] transition duration-[20ms]
text-([14px] [color:var(--theme--fg-secondary)])
border-(~ [color:var(--theme--fg-border)])
hover:bg-[color:var(--theme--bg-hover)]"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
>${$input}${$icon.children[0]}${$filename}
</label>
${$clear}
</div>`
: html`<label
class="notion-enhancer--menu-input
${variant === "lg" ? "h-[32px]" : "h-[28px]"}
relative overflow-hidden rounded-[4px] w-full inline-block
focus-within:ring-(~ [color:var(--theme--accent-primary)])
${className ?? ""} ${type === "color"
? "bg-([image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)] [position:0_0,4px_4px] [size:8px_8px])"
: "bg-[color:var(--theme--bg-hover)]"}"
>${$input}${$icon}
</label>`;
}
export { Input };

View File

@ -1,72 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Description } from "./Description.mjs";
import { Input } from "./Input.mjs";
import { Mod } from "./Mod.mjs";
function Search({ items, itemType }) {
const { html, addKeyListener } = globalThis.__enhancerApi,
$search = html`<${Input}
type="text"
icon="search"
variant="lg"
placeholder="Search ${items.length} ${items.length === 1
? itemType.replace(/s$/, "")
: itemType} (Press '/' to focus)"
oninput=${(event) => {
const query = event.target.value.toLowerCase();
for (const $item of items) {
const matches = $item.innerText.toLowerCase().includes(query);
$item.style.display = matches ? "" : "none";
}
}}
/>`;
addKeyListener("/", (event) => {
if (document.activeElement?.nodeName === "INPUT") return;
// offsetParent == null if parent has "display: none;"
if ($search.offsetParent) {
event.preventDefault();
$search.focus();
}
});
return $search;
}
function List({ id, mods, description }) {
const { html, setState } = globalThis.__enhancerApi,
{ isEnabled, setEnabled } = globalThis.__enhancerApi,
$mods = mods.map((mod) => {
const _get = () => isEnabled(mod.id),
_set = async (enabled) => {
await setEnabled(mod.id, enabled);
// only one theme may be enabled per
// mode at a time => auto-disable other
// enabled themes of matching mode
if (enabled && id === "themes") {
const isDark = mod.tags.includes("dark"),
isLight = mod.tags.includes("light");
for (const other of mods) {
if (other.id === mod.id) continue;
const otherDark = other.tags.includes("dark"),
otherLight = other.tags.includes("light");
if ((isDark && otherDark) || (isLight && otherLight)) {
await setEnabled(other.id, false);
}
}
}
setState({ rerender: true });
};
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
});
return html`<div class="flex-(~ col) gap-y-[14px]">
<${Search} items=${$mods} itemType=${id} />
<${Description} innerHTML=${description} />
${$mods}
</div>`;
}
export { List };

View File

@ -1,84 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Description } from "./Description.mjs";
import { Toggle } from "./Toggle.mjs";
function Mod({
id,
name,
version,
description,
thumbnail,
tags = [],
authors,
options = [],
_get,
_set,
_src,
}) {
const { html, setState, enhancerUrl } = globalThis.__enhancerApi,
toggleId = Math.random().toString(36).slice(2, 5);
return html`<label
for=${toggleId}
class="notion-enhancer--menu-mod flex items-stretch rounded-[4px]
bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px]
border border-[color:var(--theme--fg-border)] cursor-pointer
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
transition [&+.notion-enhancer--menu-option]:mt-[24px]"
>
${thumbnail
? html`<img
src="${enhancerUrl(`${_src}/${thumbnail}`)}"
class="rounded-[4px] mr-[12px] h-[74px] aspect-video my-auto"
/>`
: ""}
<div class="flex-(~ col) w-full">
<div class="flex flex-wrap items-center gap-[8px] text-[14px] mb-[5px]">
<h3 class="my-0">${name}</h3>
${[`v${version}`, ...tags].map((tag) => {
return html`<span
class="text-([12px] [color:var(--theme--fg-secondary)]
nowrap) leading-tight tracking-wide py-[2px] px-[6px]
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
>${tag}
</span>`;
})}
</div>
<${Description} class="mb-[6px] max-w-[80%]" innerHTML=${description} />
<div class="mt-auto flex gap-x-[8px] text-[12px] leading-[16px]">
${authors.map((author) => {
return html`<a href=${author.homepage} class="flex items-center">
<img src=${author.avatar} alt="" class="h-[12px] rounded-full" />
<span class="ml-[6px]">${author.name}</span>
</a>`;
})}
</div>
</div>
<div class="flex ml-auto">
${options.length
? html`<button
class="flex items-center p-[4px] rounded-[4px] transition
text-[color:var(--theme--fg-secondary)] my-auto mr-[8px]
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
active:text-[color:var(--theme--fg-primary)]"
onclick=${() => {
setState({ transition: "slide-to-right", view: id });
}}
>
<i class="i-settings size-[18px]"></i>
</button>`
: ""}
<div class="my-auto scale-[1.15]">
<${Toggle} id=${toggleId} ...${{ _get, _set }} />
</div>
</div>
</label>`;
}
export { Mod };

View File

@ -1,108 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Heading } from "./Heading.mjs";
import { Description } from "./Description.mjs";
import { Checkbox } from "./Checkbox.mjs";
import { Button } from "./Button.mjs";
import { Tile } from "./Tile.mjs";
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/",
tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/";
function Onboarding() {
const { html, setState, useState } = globalThis.__enhancerApi,
{ version, initDatabase } = globalThis.__enhancerApi,
$submitAgreement = html`<${Button}
icon="arrow-right"
class="ml-auto"
disabled
>Continue
<//>`,
$agreeToTerms = html`<div class="mt-[32px]">
<${Heading} class="mb-[8px]">
Thanks for installing the notion-enhancer!
<//>
<${Description}>
In order for the notion-enhancer to function, it may access, collect,
process and/or store data on your device (including workspace content,
device metadata, and notion-enhancer configuration) as described in its
privacy policy. Unless otherwise stated, the notion-enhancer will never
transmit personally identifiable information from your device.
<br /><br />
The notion-enhancer is free and open-source software distributed under
the <a href="${tsAndCs}#license">MIT License</a> without warranty of any
kind. In no event shall the authors be liable for any consequences of
the software's use. Before continuing, you must read and agree to the
notion-enhancer's privacy policy and terms & conditions.
<//>
<div class="flex items-center my-[14px] gap-[8px]">
<!-- _requireReload=${false} prevents the footer from
suggesting a reload of the app when the box is checked -->
<${Checkbox}
_set=${(checked) => ($submitAgreement.disabled = !checked)}
_requireReload=${false}
/>
<p class="typography text-[14px] mr-[16px]">
I have read and agree to the
<a class="mx-[4px]" href=${privacyPolicy}>Privacy Policy</a>
and <a href=${tsAndCs}>Terms & Conditions</a>.
</p>
${$submitAgreement}
</div>
</div>`;
$submitAgreement.onclick = async () => {
if ($submitAgreement.disabled) return;
await initDatabase().set("agreedToTerms", version);
setState({ rerender: true });
};
const $regularGreeting = html`<div
class="mt-[16px] grid-(~ cols-3) gap-[16px]"
>
<${Tile}
href="https://notion-enhancer.github.io/getting-started/basic-usage/"
icon="graduation-cap"
title="Stuck?"
>Check out the usage guide.
<//>
<${Tile}
href="https://notion-enhancer.github.io/documentation/mods/"
icon="package-plus"
title="Something missing?"
>Build your own extension.
<//>
<${Tile}
href="https://github.com/notion-enhancer/notion-enhancer/issues"
icon="bug"
title="Something broken?"
>Report a bug.
<//>
</div>`,
$featuredSponsors = html`
<div class="mt-[32px]">
<${Heading} class="mb-[8px]">Featured Sponsors<//>
<${Description}>
A few awesome companies out there have teamed up with me to provide
you with the notion-enhancer, free forever. Check them out!
<//>
<div class="mt-[16px] grid-(~ cols-1) gap-[16px]"></div>
<${Description} class="mt-[12px]">
<a href="mailto:thedragonring.bod@gmail.com">Join this list.</a>
<//>
</div>
`;
useState(["rerender"], async () => {
const agreedToTerms = await initDatabase().get("agreedToTerms");
$agreeToTerms.style.display = agreedToTerms === version ? "none" : "";
$regularGreeting.style.display = agreedToTerms === version ? "" : "none";
$featuredSponsors.style.display = agreedToTerms === version ? "" : "none";
});
return html`${$agreeToTerms}${$regularGreeting}`;
}
export { Onboarding };

View File

@ -1,100 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Heading } from "./Heading.mjs";
import { Description } from "./Description.mjs";
import { Input } from "./Input.mjs";
import { Select } from "./Select.mjs";
import { Toggle } from "./Toggle.mjs";
const camelToSentenceCase = (string) =>
string[0].toUpperCase() +
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1),
filterOptionsForRender = (options) => {
const { platform } = globalThis.__enhancerApi;
options = options.reduce((options, opt) => {
// option must have key, headings may use label
if (!opt.key && (opt.type !== "heading" || !opt.label)) return options;
// ignore platform-specific options
if (opt.platforms && !opt.platforms.includes(platform)) return options;
// replace consective headings
opt._autoremoveIfSectionEmpty ??= true;
const prev = options[options.length - 1],
canReplacePrev =
prev?._autoremoveIfSectionEmpty && prev?.type === opt.type;
if (opt.type === "heading" && canReplacePrev) {
options[options.length - 1] = opt;
} else options.push(opt);
return options;
}, []);
// remove trailing heading
return options.at(-1)?.type === "heading" &&
options.at(-1)?._autoremoveIfSectionEmpty
? options.slice(0, -1)
: options;
};
function Option({ _get, _set, ...opt }) {
const { html } = globalThis.__enhancerApi;
return html`<${opt.type === "toggle" ? "label" : "div"}
class="notion-enhancer--menu-option flex items-center justify-between
mb-[18px] ${opt.type === "toggle" ? "cursor-pointer" : ""}"
>
<div class="flex-(~ col) ${opt.type === "text" ? "w-full" : "mr-[10%]"}">
<h5 class="text-[14px] mb-[2px] mt-0">${opt.label}</h5>
${opt.type === "text"
? html`<${Input}
type="text"
class="mt-[4px] mb-[8px]"
...${{ _get, _set }}
/>`
: ""}
${["string", "undefined"].includes(typeof opt.description)
? html`<${Description} innerHTML=${opt.description} />`
: html`<${Description}>${opt.description}<//>`}
</div>
${["number", "hotkey", "color"].includes(opt.type)
? html`<${Input}
type=${opt.type}
class="shrink-0 !w-[192px]"
...${{ _get, _set }}
/>`
: opt.type === "file"
? html`<${Input}
type="file"
extensions=${opt.extensions}
...${{ _get, _set }}
/>`
: opt.type === "select"
? html`<${Select} values=${opt.values} ...${{ _get, _set }} />`
: opt.type === "toggle"
? html`<${Toggle} ...${{ _get, _set }} />`
: ""}
<//>`;
}
function Options({ mod }) {
const { html, modDatabase, setState } = globalThis.__enhancerApi;
return filterOptionsForRender(mod.options).map((opt) => {
opt.label ??= camelToSentenceCase(opt.key);
if (opt.type === "heading") {
return typeof opt.description === "string"
? html`<div class="mb-[18px]">
<${Heading}>${opt.label}<//>
<${Description} innerHTML=${opt.description} />
</div>`
: html`<${Heading}>${opt.label}<//>`;
}
const _get = async () => (await modDatabase(mod.id)).get(opt.key),
_set = async (value) => {
await (await modDatabase(mod.id)).set(opt.key, value);
setState({ rerender: true });
};
return html`<${Option} ...${{ _get, _set, ...opt }} />`;
});
}
export { Options, Option };

View File

@ -1,82 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Popup(
{ trigger, mode = "left", width = 250, maxWidth, ...props },
...children
) {
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
// known values for mode:
// dropdown => panel switcher
isDropdown = mode === "dropdown",
// left => menu option select
isLeft = mode === "left";
extendProps(props, {
class: `notion-enhancer--menu-popup group/popup
absolute top-0 left-0 z-20 text-left font-normal
flex-(~ col) justify-center pointer-events-none
items-end w-full ${isDropdown ? "" : "h-full"}`,
});
const $popup = html`<div ...${props}>
<div
class="relative ${isDropdown ? "w-full" : ""}
${isLeft ? "right-[calc(100%+8px)]" : ""}"
>
<div
class="bg-[color:var(--theme--bg-secondary)]
rounded-[4px] overflow-y-auto drop-shadow-xl max-h-[70vh]
${isDropdown ? "w-full" : "w-[250px] max-w-[calc(100vw-24px)]"}
transition duration-200 opacity-0 scale-95 py-[6px] px-[4px]
group-open/popup:( pointer-events-auto opacity-100 scale-100)"
>
${children}
</div>
</div>
</div>`;
$popup.open = () => {
$popup.setAttribute("open", true);
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0));
setState({ popupOpen: true });
$popup.onopen?.();
};
$popup.close = () => {
$popup.onbeforeclose?.();
$popup.removeAttribute("open");
$popup.style.pointerEvents = "auto";
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
setTimeout(() => {
$popup.style.pointerEvents = "";
setState({ popupOpen: false });
$popup.onclose?.();
}, 200);
};
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
document.addEventListener("click", (event) => {
if (!$popup.hasAttribute("open")) return;
if ($popup.contains(event.target) || $popup === event.target) return;
if (trigger?.contains(event.target) || trigger === event.target) return;
$popup.close();
});
useState(["rerender"], () => {
if ($popup.hasAttribute("open")) $popup.close();
});
if (!trigger) return $popup;
extendProps(trigger, {
onclick: $popup.open,
onkeydown(event) {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$popup.open();
}
},
});
return $popup;
}
export { Popup };

View File

@ -1,237 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Heading } from "./Heading.mjs";
import { Description } from "./Description.mjs";
import { Checkbox } from "./Checkbox.mjs";
import { Button } from "./Button.mjs";
import { Input } from "./Input.mjs";
import { Popup } from "./Popup.mjs";
function Profile({ id }) {
const { html, setState } = globalThis.__enhancerApi,
{ getProfile, initDatabase } = globalThis.__enhancerApi,
profile = initDatabase([id]),
db = initDatabase();
const getName = async () => {
let profileName = await profile.get("profileName");
if (id === "default") profileName ??= "default";
return profileName ?? "";
},
setName = async (name) => {
// name only has effect in menu
// doesn't need to trigger reload
await profile.set("profileName", name);
},
isActive = async () => {
return id === (await getProfile());
},
setActive = async () => {
if (await isActive()) return;
await db.set("activeProfile", id);
setState({ rerender: true });
};
const $successName = html`<span
class="py-[2px] px-[4px] rounded-[3px]
bg-[color:var(--theme--bg-hover)]"
></span>`,
$uploadSuccess = html`<${Popup}
onopen=${async () => ($successName.innerText = await getName())}
>
<p class="py-[2px] px-[8px] text-[14px]">
The profile ${$successName} has been updated successfully.
</p>
<//>`,
$uploadError = html`<${Popup}>
<p
class="py-[2px] px-[8px] text-[14px]
text-[color:var(--theme--accent-secondary)]"
>
An error was encountered attempting to parse the uploaded file.
</p>
<//>`,
uploadProfile = (event) => {
const file = event.target.files[0],
reader = new FileReader();
reader.onload = async (progress) => {
try {
let res = progress.currentTarget.result;
res = JSON.parse(res);
delete res["profileName"];
await profile.import(res);
setState({ rerender: true });
$uploadSuccess.open();
setTimeout(() => $uploadSuccess.close(), 2000);
} catch (err) {
$uploadError.open();
setTimeout(() => $uploadError.close(), 2000);
}
// clear input value to allow repeat uploads
event.target.value = "";
};
reader.readAsText(file);
},
downloadProfile = async () => {
const now = new Date(),
year = now.getFullYear().toString(),
month = (now.getMonth() + 1).toString().padStart(2, "0"),
day = now.getDate().toString().padStart(2, "0"),
hour = now.getHours().toString().padStart(2, "0"),
min = now.getMinutes().toString().padStart(2, "0"),
sec = now.getSeconds().toString().padStart(2, "0"),
date = year + month + day + hour + min + sec;
const $a = html`<a
class="hidden"
download="notion-enhancer_${await getName()}_${date}.json"
href="data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(await profile.export())
)}"
/>`;
document.body.append($a);
$a.click();
$a.remove();
},
$uploadInput = html`<input
type="file"
class="hidden"
accept=".json"
onchange=${uploadProfile}
/>`;
const deleteProfile = async () => {
let profileIds = await db.get("profileIds");
if (!profileIds?.length) profileIds = ["default"];
// clear profile data
const keys = Object.keys(await profile.export());
await profile.remove(keys);
// remove profile from list
const index = profileIds.indexOf(id);
if (index > -1) profileIds.splice(index, 1);
await db.set("profileIds", profileIds);
if (await isActive()) await db.remove("activeProfile");
setState({ rerender: true });
},
$delete = html`<button
class="h-[14px] transition duration-[20ms]
text-[color:var(--theme--fg-secondary)]
hover:text-[color:var(--theme--fg-primary)]"
>
<i class="i-x size-[14px]"></i>
</button>`,
$confirmName = $successName.cloneNode(true),
$confirm = html`<${Popup}
trigger=${$delete}
onopen=${async () => ($confirmName.innerText = await getName())}
>
<p class="text-[14px] py-[2px] px-[8px]">
Are you sure you want to delete the profile ${$confirmName} permanently?
</p>
<div class="flex-(~ col) gap-[8px] py-[6px] px-[8px]">
<${Button}
tabindex="0"
icon="trash"
class="justify-center"
variant="secondary"
onclick=${deleteProfile}
>
Delete
<//>
<${Button}
tabindex="0"
class="justify-center"
onclick=${() => $confirm.close()}
>
Cancel
<//>
</div>
<//>`;
return html`<li class="flex items-center my-[14px] gap-[8px]" id=${id}>
<${Checkbox}
...${{ _get: isActive, _set: setActive, _requireReload: false }}
onchange=${(event) => (event.target.checked = true)}
/>
<${Input}
icon="file-cog"
...${{ _get: getName, _set: setName, _requireReload: false }}
/>
<${Button}
icon="import"
variant="sm"
tagName="label"
class="relative"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$uploadInput.click();
}
}}
>${$uploadInput} Import ${$uploadSuccess}${$uploadError}
<//>
<${Button} variant="sm" icon="upload" onclick=${downloadProfile}>Export<//>
<div class="relative flex">${$delete}${$confirm}</div>
</li>`;
}
function Profiles() {
const { html, setState, useState, initDatabase } = globalThis.__enhancerApi,
$input = html`<${Input} icon="file-cog" />`,
$list = html`<ul></ul>`;
const db = initDatabase(),
refreshProfiles = async () => {
let profileIds = await db.get("profileIds");
if (!profileIds?.length) profileIds = ["default"];
const $profiles = profileIds.map((id) => {
return document.getElementById(id) || html`<${Profile} id=${id} />`;
});
// replace rows one-by-one to avoid layout shift
for (let i = 0; i < $profiles.length || i < $list.children.length; i++) {
if ($profiles[i] === $list.children[i]) continue;
if ($list.children[i]) {
if ($profiles[i]) {
$list.children[i].replaceWith($profiles[i]);
} else $list.children[i].remove();
} else $list.append($profiles[i]);
}
},
addProfile = async () => {
if (!$input.children[0].value) return;
const name = $input.children[0].value,
id = crypto.randomUUID();
let profileIds = await db.get("profileIds");
if (!profileIds?.length) profileIds = ["default"];
await db.set("profileIds", [...profileIds, id]);
await initDatabase([id]).set("profileName", name);
$input.children[0].value = "";
setState({ rerender: true });
};
useState(["rerender"], () => refreshProfiles());
$input.onkeydown = (event) => {
if (event.key === "Enter") addProfile();
};
return html`
<${Heading}>Profiles<//>
<${Description}>
Profiles can be used to preserve and switch between notion-enhancer
configurations.
<//>
<div>
${$list}
<div class="flex items-center my-[14px] gap-[8px]">
${$input}
<${Button} variant="sm" icon="plus" onclick=${addProfile}>
Add Profile
<//>
</div>
</div>
`;
}
export { Profiles };

View File

@ -1,146 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Popup } from "./Popup.mjs";
function Option({ $icon = "", value = "", _get, _set }) {
const { html, useState } = globalThis.__enhancerApi;
return html`<div
tabindex="0"
role="option"
class="select-none cursor-pointer rounded-[3px]
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
transition duration-[20ms] focus:bg-[color:var(--theme--bg-hover)]"
onmouseover=${(event) => event.target.focus()}
onclick=${() => _set?.(value)}
onkeydown=${(event) => {
if (["Enter", " "].includes(event.key)) _set?.(value);
}}
>
<div
class="mr-[6px] inline-flex items-center gap-[6px]
text-[14px] text-ellipsis overflow-hidden"
>
${$icon}<span>${value}</span>
</div>
</div>`;
}
function Select({
_get,
_set,
_requireReload = true,
values = [],
popupMode = "left",
maxWidth = 256,
minWidth = 48,
...props
}) {
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
$selected = html`<i class="ml-auto i-check size-[16px]"></i>`,
// dir="rtl" overflows to the left during transition
$select = html`<div
dir="rtl"
role="button"
tabindex="0"
class="appearance-none bg-transparent rounded-[4px]
h-[28px] max-w-[${maxWidth}px] min-w-[${minWidth}px]
cursor-pointer text-[14px] overflow-hidden pr-[28px]
transition duration-[20ms] leading-[28px] pl-[8px]
hover:bg-[color:var(--theme--bg-hover)]"
></div>`,
$popup = html`<div></div>`,
onKeydown = (event) => {
const intercept = () => {
event.preventDefault();
event.stopPropagation();
};
if (event.key === "Escape") {
intercept(setState({ rerender: true }));
} else if (!options.length) return;
// prettier-ignore
const $next = options.find(({ $option }) => $option === event.target)
?.$option.nextElementSibling ?? options.at(0).$option,
$prev = options.find(({ $option }) => $option === event.target)
?.$option.previousElementSibling ?? options.at(-1).$option;
// overflow to opposite end of list from dir of travel
if (event.key === "ArrowUp") intercept($prev.focus());
if (event.key === "ArrowDown") intercept($next.focus());
// re-enable natural tab behaviour in notion interface
if (event.key === "Tab") event.stopPropagation();
};
let options = [];
const valueToOption = (opt) => {
if (["string", "number"].includes(typeof opt)) opt = { value: opt };
if (!(opt?.$icon instanceof Element)) {
if (typeof opt?.$icon === "string") {
opt.$icon = html`<i class="i-${opt.$icon} size-[16px]" />`;
} else delete opt.$icon;
}
const $icon = opt.$icon?.cloneNode(true);
return {
...opt,
$option: html`<${Option} ...${{ ...opt, _get, _set }} />`,
$value: html`<div class="inline-flex text-nowrap items-center gap-[6px]">
<!-- swap icon/value order for correct display when dir="rtl" -->
<span>${opt.label || opt.value}</span>${$icon ?? ""}
</div>`,
};
};
$select.setValues = (values) => {
options = values.map(valueToOption);
$popup.innerHTML = "";
$popup.append(...options.map(({ $option }) => $option));
};
$select.setValues(values);
let _initialValue;
const getSelected = async () => {
const value = (await _get?.()) ?? $select.innerText,
option = options.find((opt) => opt.value === value);
if (!option) _set?.(options[0].value);
return option || options[0];
};
useState(["rerender"], async () => {
if (!options.length) return;
const { value, $value, $option } = await getSelected();
$select.innerHTML = "";
$select.append($value);
$option.append($selected);
if (_requireReload) {
_initialValue ??= value;
if (value !== _initialValue) setState({ databaseUpdated: true });
}
});
extendProps(props, { class: "notion-enhancer--menu-select relative" });
return html`<div ...${props} setValues=${$select.setValues}>
${$select}<${Popup}
tabindex="0"
trigger=${$select}
mode=${popupMode}
onopen=${() => document.addEventListener("keydown", onKeydown, true)}
onbeforeclose=${() => {
document.removeEventListener("keydown", onKeydown, true);
$select.style.width = `${$select.offsetWidth}px`;
$select.style.background = "transparent";
}}
onclose=${() => {
$select.style.width = "";
$select.style.background = "";
}}
>${$popup}
<//>
<i
class="i-chevron-down pointer-events-none
absolute right-[6px] top-[6px] size-[16px]
text-[color:var(--theme--fg-secondary)]"
></i>
</div>`;
}
export { Select };

View File

@ -1,110 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Description } from "./Description.mjs";
function SidebarHeading({}, ...children) {
const { html } = globalThis.__enhancerApi;
return html`<h2
class="flex items-center font-semibold leading-none
text-([12px] [color:var(--theme--fg-secondary)])
h-[24px] px-[12px] mb-px mt-[18px] first:mt-[10px]"
>
${children}
</h2>`;
}
function SidebarButton({ id, icon, ...props }, ...children) {
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
$btn = html`<${props["href"] ? "a" : "button"}
class="flex items-center select-none cursor-pointer text-[14px]
transition hover:bg-[color:var(--theme--bg-hover)] disabled:hidden
min-h-[27px] w-full my-px last:mb-[12px] px-[12px] rounded-[4px]"
...${props}
>
${icon
? html`<i
class="i-${icon} ${icon.startsWith("notion-enhancer")
? "size-[17px] ml-[1.5px] mr-[9.5px]"
: "size-[18px] ml-px mr-[9px]"}"
></i>`
: ""}
<span class="leading-[20px]">${children}</span>
<//>`;
if (!props["href"]) {
extendProps($btn, {
onclick: () => setState({ transition: "fade", view: id }),
});
useState(["view"], ([view = "welcome"]) => {
const active = view.toLowerCase() === id.toLowerCase();
$btn.style.background = active ? "var(--theme--bg-hover)" : "";
$btn.style.fontWeight = active ? "600" : "";
});
}
return $btn;
}
function Sidebar({ items, categories }) {
const { html, useState } = globalThis.__enhancerApi,
{ version, initDatabase, isEnabled } = globalThis.__enhancerApi,
$agreeToUnlock = html`<span
class="pt-[2px] pb-[5px] px-[15px] text-[12px]
inline-block text-[color:var(--theme--fg-red)]"
>To unlock the notion-enhancer's full functionality, agree to the privacy
policy and terms & conditions on the welcome page.
</span>`,
$sidebar = html`<aside
class="notion-enhancer--menu-sidebar h-full
px-[4px] overflow-y-auto flex-(~ col) row-span-1
bg-[color:var(--theme--bg-secondary)]"
>
${items.map((item) => {
if (Array.isArray(item)) {
const [title, desc] = Array.isArray(item) ? item : [item];
return html`
<${SidebarHeading}>${title}<//>
<${Description}>${desc}<//>
`;
} else if (typeof item === "object") {
const { title, ...props } = item;
return html`<${SidebarButton} ...${props}>${title}<//>`;
} else return html`<${SidebarHeading}>${item}<//>`;
})}${$agreeToUnlock}
</aside>`;
useState(["rerender"], async () => {
const agreedToTerms = await initDatabase().get("agreedToTerms");
$agreeToUnlock.style.display = agreedToTerms === version ? "none" : "";
[...$sidebar.children].forEach(($btn) => {
if (!$btn.disableUntilAgreedToTerms) return;
$btn.disabled = agreedToTerms !== version;
});
});
for (const { title, mods } of categories) {
const $title = html`<${SidebarHeading}>${title}<//>`,
$mods = mods
.filter((mod) => mod.options?.length)
.map((mod) => [
mod.id,
html`<${SidebarButton} id=${mod.id}>${mod.name}<//>`,
]);
$sidebar.append($title, ...$mods.map(([, $btn]) => $btn));
useState(["rerender"], async () => {
let sectionVisible = false;
for (const [id, $btn] of $mods) {
$btn.disabled = !(await isEnabled(id));
sectionVisible ||= !$btn.disabled;
}
$title.style.display = sectionVisible ? "" : "none";
});
}
return $sidebar;
}
export { Sidebar };

View File

@ -1,53 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
// telemetry endpoint not ready, disabled for current release
import { collectTelemetryData } from "../../sendTelemetry.mjs";
import { Option } from "./Options.mjs";
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/";
function Telemetry() {
const { html, setState, useState, initDatabase } = globalThis.__enhancerApi,
_get = async () => {
// defaults to true, must be explicitly set to false to disable
return (await initDatabase().get("telemetryEnabled")) ?? true;
},
_set = async (value) => {
await initDatabase().set("telemetryEnabled", value);
setState({ rerender: true });
};
const $ = {
platform: html`<code></code>`,
version: html`<code></code>`,
timezone: html`<code></code>`,
enabled_mods: html`<code></code>`,
};
useState(["rerender"], async () => {
const telemetryData = await collectTelemetryData();
for (const key in telemetryData) {
$[key].innerText = JSON.stringify(telemetryData[key]);
}
});
return html`<${Option}
type="toggle"
label="Telemetry"
description=${html`If telemetry is enabled, usage data will be collected at
a regular interval from your device in order to better understand how and
where the notion-enhancer is used. This data is anonymous and includes
only your platform (${$.platform}), notion-enhancer version
(${$.version}), timezone (${$.timezone}), and enabled mods
(${$.enabled_mods}). You can opt in or out of telemetry at any time. This
setting syncs across configuration profiles. For more information, read
the notion-enhancer's
<a href=${privacyPolicy} class="ml-[3px]">privacy policy</a>.`}
...${{ _get, _set }}
/>`;
}
export { Telemetry };

View File

@ -1,27 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Tile({ icon, title, tagName, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `flex items-center gap-[12px] rounded-[4px]
border-(~ [color:var(--theme--fg-border)]) px-[16px]
bg-[color:var(--theme--bg-secondary)] py-[12px]
hover:bg-[color:var(--theme--bg-hover)]`,
});
tagName ??= props["href"] ? "a" : "button";
return html`<${tagName} ...${props}>
<i class="i-${icon} text-[28px]"></i>
<div>
<h3 class="text-[14px] font-semibold">${title}</h3>
<div class="text-([12px] [color:var(--theme--fg-secondary)])">
${children}
</div>
</div>
<//>`;
}
export { Tile };

View File

@ -1,53 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Toggle({ _get, _set, _requireReload = true, ...props }) {
let _initialValue;
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
$input = html`<input
type="checkbox"
class="hidden [&:checked+div>div]:(
bg-[color:var(--theme--accent-primary)]
after:translate-x-[12px])"
...${props}
/>`;
extendProps($input, { onchange: () => _set?.($input.checked) });
useState(["rerender"], async () => {
const checked = (await _get?.()) ?? $input.checked;
$input.checked = checked;
if (_requireReload) {
_initialValue ??= checked;
if (checked !== _initialValue) setState({ databaseUpdated: true });
}
});
return html`<div class="notion-enhancer--menu-toggle shrink-0">
${$input}
<div
tabindex="0"
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer
transition duration-200 bg-[color:var(--theme--bg-hover)]"
onkeydown=${(event) => {
if ([" ", "Enter"].includes(event.key)) {
event.preventDefault();
$input.click();
}
}}
>
<div
class="w-full h-full rounded-[44px] text-[12px]
p-[2px] hover:bg-[color:var(--theme--bg-hover)]
transition duration-200 after:(
inline-block size-[14px] rounded-[44px]
bg-[color:var(--theme--accent-primary\\_contrast)]
transition duration-200 content-empty
)"
></div>
</div>
</div>`;
}
export { Toggle };

View File

@ -1,92 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function View({ id }, ...children) {
const { html, setState, useState } = globalThis.__enhancerApi,
// set padding on last child to maintain pad on overflow
$view = html`<article
id=${id}
class="notion-enhancer--menu-view min-h-full w-full
absolute px-[60px] py-[36px] min-w-[580px]"
>
${children}
</article>`;
useState(["view"], ([view = "welcome"]) => {
const [transition] = useState(["transition"]),
isVisible = $view.style.display !== "none",
nowActive = view.toLowerCase() === id.toLowerCase();
switch (transition) {
case "fade": {
const duration = 100,
cssTransition = `opacity ${duration}ms`;
if (isVisible && !nowActive) {
$view.parentElement.style.overflow = "hidden";
requestAnimationFrame(() => {
$view.style.transition = cssTransition;
$view.style.opacity = "0";
setTimeout(() => ($view.style.display = "none"), duration);
});
} else if (!isVisible && nowActive) {
setTimeout(() => {
$view.style.opacity = "0";
$view.style.display = "";
requestAnimationFrame(() => {
$view.style.transition = cssTransition;
$view.style.opacity = "1";
$view.parentElement.style.overflow = "";
});
}, duration);
}
break;
}
case "slide-to-left":
case "slide-to-right": {
const duration = 200,
cssTransition = `opacity ${duration}ms, transform ${duration}ms`;
if (isVisible && !nowActive) {
$view.parentElement.style.overflow = "hidden";
requestAnimationFrame(() => {
$view.style.transition = cssTransition;
$view.style.transform = `translateX(${
transition === "slide-to-right" ? "-100%" : "100%"
})`;
$view.style.opacity = "0";
setTimeout(() => {
$view.style.display = "none";
$view.style.transform = "";
}, duration);
});
} else if (!isVisible && nowActive) {
$view.style.transform = `translateX(${
transition === "slide-to-right" ? "100%" : "-100%"
})`;
$view.style.opacity = "0";
$view.style.display = "";
requestAnimationFrame(() => {
$view.style.transition = cssTransition;
$view.style.transform = "";
$view.style.opacity = "1";
setTimeout(() => {
$view.parentElement.style.overflow = "";
}, duration);
});
}
break;
}
default:
$view.style.transition = "";
$view.style.opacity = nowActive ? "1" : "0";
$view.style.display = nowActive ? "" : "none";
}
});
return $view;
}
export { View };

View File

@ -1,140 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
@keyframes skeleton-shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
::selection {
background: var(--theme--accent-primary_transparent);
}
*:focus-visible {
outline: 3px solid var(--theme--accent-primary);
}
*:focus-visible[role="option"] {
outline: none;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
background: transparent;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
background: var(--theme--scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--theme--scrollbar-thumb);
}
::-webkit-scrollbar-thumb:hover {
background: var(--theme--scrollbar-thumb_hover);
}
body {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 240px auto;
width: 100vw;
height: 100vh;
margin: 0;
color: var(--theme--fg-primary);
font-family: var(--font--sans);
overflow: hidden;
}
body > #skeleton {
background: rgba(86, 86, 86, 0.1);
}
body > #skeleton .row {
display: flex;
align-items: center;
padding: 0 15px;
margin: 2px 0;
height: 27px;
}
body > #skeleton .shimmer {
height: 14px;
overflow: hidden;
position: relative;
border-radius: 4px;
background: rgba(86, 86, 86, 0.1);
}
body > #skeleton .shimmer.icon {
margin-left: 1px;
margin-right: 9px;
height: 18px;
width: 18px;
}
body > #skeleton .shimmer::before {
content: "";
position: absolute;
height: 100%;
width: 100%;
z-index: 1;
animation: 1s linear infinite skeleton-shimmer;
background: linear-gradient(
90deg,
transparent 0,
rgba(86, 86, 86, 0.1) 50%,
transparent 100%
);
}
body > #skeleton .row-group {
height: 24px;
margin-top: 18px;
}
body > #skeleton .row-group:first-child {
margin-top: 10px;
}
body > #skeleton .row-group .shimmer {
height: 11px;
}
.typography mark {
padding: 0 4px;
border-radius: 3px;
background-color: var(--theme--bg-hover);
color: inherit;
}
.typography code {
padding: 0 4px;
border-radius: 3px;
background-color: var(--theme--code-inline_bg);
color: var(--theme--code-inline_fg);
}
.typography kbd {
padding: 2px 4px;
border-radius: 6px;
border: solid 1px var(--theme--fg-border);
box-shadow: inset 0 -1px 0 var(--theme--fg-border);
}
.typography a {
text-decoration: underline;
transition: 100ms ease-in;
}
.typography a:hover {
color: var(--theme--accent-secondary);
}
/* https://coloris.js.org/ */
.clr-picker {
background-color: var(--theme--bg-secondary) !important;
}
.clr-color {
background-color: var(--theme--bg-hover) !important;
border-color: var(--theme--fg-border) !important;
color: var(--theme--fg-primary) !important;
}
.clr-preview:after,
.clr-preview:before {
border-color: var(--theme--fg-border) !important;
}

View File

@ -1,277 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { checkForUpdate, isDevelopmentBuild } from "../updateCheck.mjs";
import { Sidebar } from "./islands/Sidebar.mjs";
import { Footer } from "./islands/Footer.mjs";
import { Banner } from "./islands/Banner.mjs";
import { Onboarding } from "./islands/Onboarding.mjs";
import { View } from "./islands/View.mjs";
import { List } from "./islands/List.mjs";
import { Mod } from "./islands/Mod.mjs";
import { Options } from "./islands/Options.mjs";
import { Profiles } from "./islands/Profiles.mjs";
import { Description } from "./islands/Description.mjs";
let _apiImport, //
_renderStarted,
_stateHookedInto,
_hotkeyRegistered;
const categories = [
{
icon: "palette",
id: "themes",
title: "Themes",
description: `Themes override Notion's colour schemes. Dark themes require
Notion to be in dark mode and light themes require Notion to be in light
mode. To switch between dark mode and light mode, go to <mark>Settings &
members My notifications & settings My settings Appearance</mark>.`,
},
{
icon: "zap",
id: "extensions",
title: "Extensions",
description: `Extensions add to the functionality and layout of the Notion
client, interacting with and modifying existing interfaces.`,
},
// {
// icon: "plug",
// id: "integrations",
// title: "Integrations",
// description: `<span class="text-[color:var(--theme--fg-red)]">
// Integrations access and modify Notion content. They interact directly with
// <mark>https://www.notion.so/api/v3</mark>. Use at your own risk.</span>`,
// },
],
sidebar = [
"notion-enhancer",
{
id: "welcome",
title: "Welcome",
icon: "notion-enhancer",
},
{
icon: "message-circle",
title: "Community",
href: "https://discord.gg/sFWPXtA",
},
{
icon: "clock",
title: "Changelog",
href: "https://notion-enhancer.github.io/about/changelog/",
},
{
icon: "book",
title: "Documentation",
href: "https://notion-enhancer.github.io/",
},
{
icon: "github",
title: "Source Code",
href: "https://github.com/notion-enhancer",
},
"Settings",
{
id: "core",
title: "Core",
icon: "sliders-horizontal",
disableUntilAgreedToTerms: true,
},
...categories.map((c) => ({
id: c.id,
title: c.title,
icon: c.icon,
disableUntilAgreedToTerms: true,
})),
];
const renderMenu = async () => {
const { html, setState, useState } = globalThis.__enhancerApi,
{ getMods, isEnabled, setEnabled } = globalThis.__enhancerApi,
[theme, icon] = useState(["theme", "icon"]);
if (!theme || !icon || _renderStarted) return;
if (icon === "Monochrome") sidebar[1].icon += "?mask";
_renderStarted = true;
const mods = await getMods();
for (let i = 0; i < categories.length; i++) {
const { id } = categories[i];
categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`));
categories[i].view = html`<${View} id=${id}>
<${List} ...${categories[i]} />
<//>`;
}
for (let i = 0; i < mods.length; i++) {
const options = mods[i].options?.filter((opt) => opt.type !== "heading");
if (mods[i]._src === "core" || !options?.length) continue;
const _get = () => isEnabled(mods[i].id),
_set = async (enabled) => {
await setEnabled(mods[i].id, enabled);
setState({ rerender: true });
};
mods[i].view = html`<${View} id=${mods[i].id}>
<!-- passing an empty options array hides the settings button -->
<${Mod} ...${{ ...mods[i], options: [], _get, _set }} />
<${Options} mod=${mods[i]} />
<//>`;
}
const $sidebar = html`<${Sidebar}
items=${sidebar}
categories=${categories}
/>`,
$main = html`
<main
class="flex-(~ col) overflow-hidden transition-[height]"
style="height: calc(100% + 65px)"
>
<!-- wrappers necessary for transitions and breakpoints -->
<div class="grow overflow-auto">
<div class="relative h-full w-full">
<${View} id="welcome">
<${Banner}
updateAvailable=${await checkForUpdate()}
isDevelopmentBuild=${await isDevelopmentBuild()}
/>
<${Onboarding} />
<div
class="p-6 rounded-[4px] mt-[16px] text-[14px]
border border-[color:var(--theme--fg-red)]
bg-[color:var(--theme--dim-red)] typography"
>
Hi there! Before you go any further, <b>please note that this update is
not feature complete.</b> As part of an internal overhaul and the Chrome
extension's upgrade to manifest v3, all themes and extensions must be
ported manually across to the new version.
<br />
<br />
The following extensions have not been updated yet but will be
soon:
<ul class="list-disc pl-6">
<li>indentation lines</li>
<li>view scale</li>
<li>emoji sets</li>
<li>simpler databases</li>
<li>icon sets</li>
<li>quick note</li>
</ul>
<br />
The theming system is incomplete and only mostly recolours the
app's interface. The following themes have not been updated
yet but will be soon:
<ul class="list-disc pl-6">
<li>dark+</li>
<li>light+</li>
<li>nord</li>
<li>dracula</li>
<li>neutral</li>
<li>cherry cola</li>
<li>gruvbox dark</li>
<li>gruvbox light</li>
<li>pastel dark</li>
<li>pinky boom</li>
<li>playful purple</li>
</ul>
<br />
In the meantime, the styling for these themes can be
found <a href="https://github.com/notion-enhancer/repo"
>here</a> and
copy/pasted into your custom styles alongside the <a
href="https://github.com/notion-enhancer/repo/blob/dev/theming/theme.css"
>old theming system</a>, if you wish.
<br />
<br />
The following extensions have been deprecated as their feature
offerings are now available within Notion by default. Some
features that belonged to these extensions have been merged
into the notion-enhancer's core or into the tweaks extension:
<ul class="list-disc pl-6">
<li>integrated titlebar</li>
<li>collapsible properties</li>
<li>collapsible headers</li>
<li>tray</li>
<li>tabs</li>
<li>weekly view</li>
<li>truncated titles</li>
<li>global block links</li>
</ul>
<br />
A full changelog and updated documentation will be made
available on the website as soon as possible. This release is
being made available early in order to comply with Chrome's
deprecation of manifest v2.
</div>
<//>
<${View} id="core">
<${Options} mod=${mods.find(({ _src }) => _src === "core")} />
<${Profiles} />
<//>
${[...categories, ...mods]
.filter(({ view }) => view)
.map(({ view }) => view)}
</div>
</div>
<${Footer} categories=${categories} />
</main>
`;
useState(["footerOpen"], ([footerOpen]) => {
$main.style.height = footerOpen ? "100%" : "calc(100% + 65px)";
});
const $skeleton = document.querySelector("#skeleton");
$skeleton.replaceWith($sidebar, $main);
},
registerHotkey = ([hotkey]) => {
const { addKeyListener, setState, useState } = globalThis.__enhancerApi;
if (!hotkey || _hotkeyRegistered) return;
_hotkeyRegistered = true;
addKeyListener(hotkey, (event) => {
event.preventDefault();
const msg = { channel: "notion-enhancer", action: "open-menu" };
parent?.postMessage(msg, "*");
});
addKeyListener("Escape", () => {
const [popupOpen] = useState(["popupOpen"]);
if (document.activeElement?.tagName === "INPUT") {
document.activeElement.blur();
} else if (!popupOpen) {
const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*");
} else setState({ rerender: true });
});
},
updateTheme = ([theme]) => {
if (theme === "dark") document.body.classList.add("dark");
if (theme === "light") document.body.classList.remove("dark");
};
const importApi = () => {
return (_apiImport ??= (async () => {
const api = globalThis.__enhancerApi;
if (typeof api === "undefined") await import("../../api/system.js");
await import("../../load.mjs").then((i) => i.default);
})());
},
hookIntoState = () => {
if (_stateHookedInto) return;
_stateHookedInto = true;
const { useState } = globalThis.__enhancerApi;
useState(["theme"], updateTheme);
useState(["hotkey"], registerHotkey);
useState(["rerender"], renderMenu);
};
addEventListener("message", async (event) => {
if (event.data?.channel !== "notion-enhancer") return;
await importApi().then(hookIntoState);
const { setState, useState } = globalThis.__enhancerApi;
setState({
rerender: true,
hotkey: event.data?.hotkey ?? useState(["hotkey"])[0],
theme: event.data?.theme ?? useState(["theme"])[0],
icon: event.data?.icon ?? useState(["icon"])[0],
});
});

View File

@ -1,88 +0,0 @@
{
"name": "notion-enhancer",
"version": "0.11.1",
"id": "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
"description": "Customise the all-in-one productivity workspace Notion.",
"tags": ["core"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
}
],
"options": [
{ "type": "heading", "label": "Hotkeys" },
{
"type": "hotkey",
"key": "openMenuHotkey",
"description": "Opens the notion-enhancer menu from within Notion.",
"value": "Ctrl+Shift+,"
},
{
"type": "hotkey",
"key": "togglePanelHotkey",
"description": "Toggles the side panel used by some notion-enhancer extensions to display additional information and interfaces within the Notion app.",
"value": "Ctrl+Shift+\\"
},
{
"type": "hotkey",
"key": "toggleWindowHotkey",
"description": "Toggles focus of the Notion window anywhere, even when your Notion app isn't active.",
"value": "Ctrl+Shift+A"
},
{ "type": "heading", "label": "Appearance" },
{
"type": "file",
"key": "customStyles",
"description": "Adds the styles from an uploaded .css file to Notion. Use this if you would like to customise the current theme or <a href=\"https://notion-enhancer.github.io/advanced/tweaks\">otherwise tweak Notion's appearance</a>.",
"extensions": ["css"]
},
{
"type": "select",
"key": "loadThemeOverrides",
"description": "Loads the styling required for a theme to customise Notion's interface. Disabling this may increase client performance, but will also disable all themes.",
"values": ["Auto", "Enabled", "Disabled"]
},
{
"type": "text",
"key": "menuButtonLabel",
"description": "Sets the text to label the notion-enhancer button added to Notion's sidebar with.",
"value": "notion-enhancer"
},
{
"type": "select",
"key": "menuButtonIconStyle",
"description": "Sets whether the icon beside the notion-enhancer button added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.",
"values": ["Colour", "Monochrome"]
},
{
"type": "toggle",
"key": "peekPanelOnHover",
"description": "Pops the side panel out to preview its content when hovering near the right edge of the window, in the same way that Notion's left-hand sidebar will slide out on hover. Disable this if you prefer to view the panel only by pinning it.",
"value": true
},
{
"type": "heading",
"label": "Advanced",
"_autoremoveIfSectionEmpty": false
},
{
"type": "file",
"label": "Custom JavaScript",
"key": "customScript",
"description": "Executes the uploaded userscript within Notion. Requires <a href='https://developer.chrome.com/docs/extensions/reference/api/userScripts#developer_mode_for_extension_users'>developer mode</a> to be enabled in your browser's extension settings to run in Chromium-based browsers.",
"extensions": ["js"]
},
{
"type": "toggle",
"key": "developerMode",
"description": "Activates built-in debugging tools accessible through the application menu.",
"platforms": ["linux", "win32", "darwin"],
"value": false
}
],
"clientStyles": ["variables.css", "../vendor/@unocss-preflight-tailwind.css"],
"clientScripts": ["client.mjs"],
"electronScripts": [[".webpack/main/index.js", "electron.cjs"]]
}

View File

@ -1,47 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
// telemetry endpoint not ready, disabled for current release
const pingEndpoint = "https://notion-enhancer.deno.dev/api/ping",
collectTelemetryData = async () => {
const { platform, version } = globalThis.__enhancerApi,
{ getMods, isEnabled } = globalThis.__enhancerApi,
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
// prettier-ignore
enabled_mods = (await getMods(async (mod) => {
if (mod._src === "core") return false;
return await isEnabled(mod.id);
})).map(mod => mod.id);
return { platform, version, timezone, enabled_mods };
},
sendTelemetryPing = async () => {
// const db = __enhancerApi.initDatabase(),
// { version } = globalThis.__enhancerApi,
// agreedToTerms = await db.get("agreedToTerms"),
// telemetryEnabled = (await db.get("telemetryEnabled")) ?? true;
// if (!telemetryEnabled || agreedToTerms !== version) return;
// const lastTelemetryPing = await db.get("lastTelemetryPing");
// if (lastTelemetryPing) {
// const msSincePing = Date.now() - new Date(lastTelemetryPing);
// // send ping only once a week
// if (msSincePing / 8.64e7 < 7) return;
// }
// try {
// const telemetryData = await collectTelemetryData(),
// pingTimestamp = await fetch(pingEndpoint, {
// method: "POST",
// body: JSON.stringify(telemetryData),
// }).then((res) => res.text());
// await db.set("lastTelemetryPing", pingTimestamp);
// } catch (err) {
// console.error(err);
// }
};
export { collectTelemetryData, sendTelemetryPing };

File diff suppressed because one or more lines are too long

View File

@ -1,49 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
let _release;
const repo = "notion-enhancer/notion-enhancer",
endpoint = `https://api.github.com/repos/${repo}/releases/latest`,
getRelease = async () => {
const { version, readJson } = globalThis.__enhancerApi;
try {
_release ??= (await readJson(endpoint))?.tag_name.replace(/^v/, "");
} catch {}
_release ??= version;
return _release;
};
const parseVersion = (semver) => {
while (semver.split("-")[0].split(".").length < 3) semver = `0.${semver}`;
let [major, minor, patch, build] = semver.split("."),
prerelease = patch.split("-")[1]?.split(".")[0];
patch = patch.split("-")[0];
return [major, minor, patch, prerelease, build]
.map((v) => v ?? "")
.map((v) => (/^\d+$/.test(v) ? parseInt(v) : v));
},
// is a < b
greaterThan = (a, b) => {
if (a && !b) return true;
if (!a && b) return false;
a = parseVersion(a);
b = parseVersion(b);
for (let i = 0; i < a.length; i++) {
if (a[i] > b[i]) return true;
else if (a[i] < b[i]) return false;
}
};
const checkForUpdate = async () => {
const { version } = globalThis.__enhancerApi;
return greaterThan(await getRelease(), version) ? _release : false;
},
isDevelopmentBuild = async () => {
const { version } = globalThis.__enhancerApi;
return !(await checkForUpdate()) && version !== _release;
};
export { checkForUpdate, isDevelopmentBuild, greaterThan };

View File

@ -1,181 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
body.dark {
--theme--fg-primary: rgba(255, 255, 255, 0.81);
--theme--fg-secondary: rgb(155, 155, 155);
--theme--fg-border: rgb(47, 47, 47);
--theme--fg-gray: rgba(155, 155, 155, 1);
--theme--fg-brown: rgba(186, 133, 111, 1);
--theme--fg-orange: rgba(199, 125, 72, 1);
--theme--fg-yellow: rgba(202, 152, 73, 1);
--theme--fg-green: rgba(82, 158, 114, 1);
--theme--fg-blue: rgba(94, 135, 201, 1);
--theme--fg-purple: rgba(157, 104, 211, 1);
--theme--fg-pink: rgba(209, 87, 150, 1);
--theme--fg-red: rgba(223, 84, 82, 1);
--theme--bg-primary: rgb(25, 25, 25);
--theme--bg-secondary: rgb(32, 32, 32);
--theme--bg-hover: rgba(255, 255, 255, 0.055);
--theme--bg-overlay: rgba(15, 15, 15, 0.8);
--theme--bg-light_gray: rgb(55, 55, 55);
--theme--bg-gray: rgb(90, 90, 90);
--theme--bg-brown: rgb(96, 59, 44);
--theme--bg-orange: rgb(133, 76, 29);
--theme--bg-yellow: rgb(137, 99, 42);
--theme--bg-green: rgb(43, 89, 63);
--theme--bg-blue: rgb(40, 69, 108);
--theme--bg-purple: rgb(73, 47, 100);
--theme--bg-pink: rgb(105, 49, 76);
--theme--bg-red: rgb(110, 54, 48);
--theme--dim-light_gray: rgb(28, 28, 28);
--theme--dim-gray: rgb(32, 32, 32);
--theme--dim-brown: rgb(35, 30, 28);
--theme--dim-orange: rgb(37, 31, 27);
--theme--dim-yellow: rgb(35, 31, 26);
--theme--dim-green: rgb(29, 34, 32);
--theme--dim-blue: rgb(27, 31, 34);
--theme--dim-purple: rgb(31, 29, 33);
--theme--dim-pink: rgb(35, 28, 31);
--theme--dim-red: rgb(36, 30, 29);
--theme--accent-primary: rgb(35, 131, 226);
--theme--accent-primary_hover: rgb(0, 117, 211);
--theme--accent-primary_contrast: rgb(255, 255, 255);
--theme--accent-primary_transparent: rgba(35, 131, 226, 0.14);
--theme--accent-secondary: rgb(235, 87, 87);
--theme--accent-secondary_hover: rgba(235, 87, 87, 0.1);
--theme--accent-secondary_contrast: white;
--theme--scrollbar-track: rgba(202, 204, 206, 0.04);
--theme--scrollbar-thumb: #474c50;
--theme--scrollbar-thumb_hover: rgba(202, 204, 206, 0.3);
--theme--code-inline_fg: #eb5757;
--theme--code-inline_bg: rgba(135, 131, 120, 0.15);
--theme--code-block_fg: rgba(255, 255, 255, 0.81);
--theme--code-block_bg: rgba(255, 255, 255, 0.03);
--theme--code-keyword: rgb(209, 148, 158);
--theme--code-builtin: rgb(189, 224, 82);
--theme--code-class_name: rgba(255, 255, 255, 0.81);
--theme--code-function: var(--theme--code-class_name);
--theme--code-boolean: var(--theme--code-keyword);
--theme--code-number: var(--theme--code-keyword);
--theme--code-string: var(--theme--code-builtin);
--theme--code-char: var(--theme--code-builtin);
--theme--code-symbol: var(--theme--code-keyword);
--theme--code-regex: rgb(238, 153, 0);
--theme--code-url: rgb(245, 184, 61);
--theme--code-operator: var(--theme--code-url);
--theme--code-variable: var(--theme--code-url);
--theme--code-constant: var(--theme--code-keyword);
--theme--code-property: var(--theme--code-keyword);
--theme--code-punctuation: var(--theme--code-class_name);
--theme--code-important: var(--theme--code-regex);
--theme--code-comment: rgb(153, 128, 102);
--theme--code-tag: var(--theme--code-keyword);
--theme--code-attr_name: var(--theme--code-builtin);
--theme--code-attr_value: var(--theme--code-keyword);
--theme--code-namespace: var(--theme--code-class_name);
--theme--code-prolog: var(--theme--code-comment);
--theme--code-doctype: var(--theme--code-comment);
--theme--code-cdata: var(--theme--code-comment);
--theme--code-entity: var(--theme--code-url);
--theme--code-atrule: var(--theme--code-keyword);
--theme--code-selector: var(--theme--code-builtin);
--theme--code-inserted: var(--theme--code-builtin);
--theme--code-deleted: rgb(255, 0, 0);
}
body:not(.dark) {
--theme--fg-primary: rgb(55, 53, 47);
--theme--fg-secondary: rgba(25, 23, 17, 0.6);
--theme--fg-border: rgb(233, 233, 231);
--theme--fg-gray: rgba(120, 119, 116, 1);
--theme--fg-brown: rgba(159, 107, 83, 1);
--theme--fg-orange: rgba(217, 115, 13, 1);
--theme--fg-yellow: rgba(203, 145, 47, 1);
--theme--fg-green: rgba(68, 131, 97, 1);
--theme--fg-blue: rgba(51, 126, 169, 1);
--theme--fg-purple: rgba(144, 101, 176, 1);
--theme--fg-pink: rgba(193, 76, 138, 1);
--theme--fg-red: rgba(212, 76, 71, 1);
--theme--bg-primary: white;
--theme--bg-secondary: rgb(251, 251, 250);
--theme--bg-hover: rgba(55, 53, 47, 0.08);
--theme--bg-overlay: rgba(15, 15, 15, 0.6);
--theme--bg-light_gray: rgba(227, 226, 224, 0.5);
--theme--bg-gray: rgb(227, 226, 224);
--theme--bg-brown: rgb(238, 224, 218);
--theme--bg-orange: rgb(250, 222, 201);
--theme--bg-yellow: rgb(253, 236, 200);
--theme--bg-green: rgb(219, 237, 219);
--theme--bg-blue: rgb(211, 229, 239);
--theme--bg-purple: rgb(232, 222, 238);
--theme--bg-pink: rgb(245, 224, 233);
--theme--bg-red: rgb(255, 226, 221);
--theme--dim-light_gray: rgba(249, 249, 245, 0.5);
--theme--dim-gray: rgba(247, 247, 245, 0.7);
--theme--dim-brown: rgba(250, 246, 245, 0.7);
--theme--dim-orange: rgba(252, 245, 242, 0.7);
--theme--dim-yellow: rgba(250, 247, 237, 0.7);
--theme--dim-green: rgba(244, 248, 243, 0.7);
--theme--dim-blue: rgba(241, 248, 251, 0.7);
--theme--dim-purple: rgba(249, 246, 252, 0.7);
--theme--dim-pink: rgba(251, 245, 251, 0.7);
--theme--dim-red: rgba(253, 245, 243, 0.7);
--theme--accent-primary: rgb(35, 131, 226);
--theme--accent-primary_hover: rgb(0, 117, 211);
--theme--accent-primary_contrast: rgb(255, 255, 255);
--theme--accent-primary_transparent: rgba(35, 131, 226, 0.14);
--theme--accent-secondary: rgb(235, 87, 87);
--theme--accent-secondary_hover: rgba(235, 87, 87, 0.1);
--theme--accent-secondary_contrast: white;
--theme--scrollbar-track: #edece9;
--theme--scrollbar-thumb: #d3d1cb;
--theme--scrollbar-thumb_hover: #aeaca6;
--theme--code-inline_fg: #eb5757;
--theme--code-inline_bg: rgba(135, 131, 120, 0.15);
--theme--code-block_fg: rgb(55, 53, 47);
--theme--code-block_bg: rgb(247, 246, 243);
--theme--code-keyword: rgb(0, 119, 170);
--theme--code-builtin: rgb(102, 153, 0);
--theme--code-class_name: rgb(221, 74, 104);
--theme--code-function: var(--theme--code-class_name);
--theme--code-boolean: rgb(153, 0, 85);
--theme--code-number: var(--theme--code-boolean);
--theme--code-string: var(--theme--code-builtin);
--theme--code-char: var(--theme--code-builtin);
--theme--code-symbol: var(--theme--code-boolean);
--theme--code-regex: rgb(238, 153, 0);
--theme--code-url: rgb(154, 110, 58);
--theme--code-operator: var(--theme--code-url);
--theme--code-variable: var(--theme--code-regex);
--theme--code-constant: var(--theme--code-boolean);
--theme--code-property: var(--theme--code-boolean);
--theme--code-punctuation: rgb(153, 153, 153);
--theme--code-important: var(--theme--code-regex);
--theme--code-comment: rgb(112, 128, 144);
--theme--code-tag: var(--theme--code-boolean);
--theme--code-attr_name: var(--theme--code-builtin);
--theme--code-attr_value: var(--theme--code-keyword);
--theme--code-namespace: rgb(55, 53, 47);
--theme--code-prolog: var(--theme--code-comment);
--theme--code-doctype: var(--theme--code-comment);
--theme--code-cdata: var(--theme--code-comment);
--theme--code-entity: var(--theme--code-url);
--theme--code-atrule: var(--theme--code-keyword);
--theme--code-selector: var(--theme--code-builtin);
--theme--code-inserted: var(--theme--code-builtin);
--theme--code-deleted: var(--theme--code-boolean);
}

View File

@ -1,10 +0,0 @@
/**
* notion-enhancer: emoji sets
* (c) 2021 Arecsu
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
[aria-label][role='image'][style*='Apple Color Emoji']:not([data-emoji-sets-unsupported]) {
margin-left: 2.5px !important;
}

View File

@ -1,71 +0,0 @@
/**
* notion-enhancer: emoji sets
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async function ({ web, env }, db) {
const style = await db.get(['style']),
// real emojis are used on macos instead of the twitter set
nativeEmojiSelector = `[aria-label][role="image"][style*="Apple Color Emoji"]:not([data-emoji-sets-unsupported])`,
imgEmojiSelector = '.notion-emoji:not([data-emoji-sets-unsupported])',
imgEmojiOverlaySelector = `${imgEmojiSelector} + [src*="notion-emojis"]`;
await Promise.any([web.whenReady([nativeEmojiSelector]), web.whenReady([imgEmojiSelector])]);
const nativeEmojis = document.querySelectorAll(nativeEmojiSelector).length,
imgEmojis = document.querySelectorAll(imgEmojiSelector).length;
const unsupportedEmojis = [],
emojiReqs = new Map(),
getEmoji = async (emoji) => {
emoji = encodeURIComponent(emoji);
if (unsupportedEmojis.includes(emoji)) return undefined;
try {
if (!emojiReqs.get(emoji)) {
emojiReqs.set(emoji, fetch(`https://emojicdn.elk.sh/${emoji}?style=${style}`));
}
const res = await emojiReqs.get(emoji);
if (!res.ok) throw new Error();
return `url("https://emojicdn.elk.sh/${emoji}?style=${style}") 100% 100% / 100%`;
} catch {
unsupportedEmojis.push(emoji);
return undefined;
}
};
if (nativeEmojis) {
const updateEmojis = async () => {
const $emojis = document.querySelectorAll(nativeEmojiSelector);
for (const $emoji of $emojis) {
const emojiSrc = await getEmoji($emoji.ariaLabel);
if (emojiSrc) {
$emoji.style.background = emojiSrc;
$emoji.style.width = '1em';
$emoji.style.height = '1em';
$emoji.style.display = 'inline-block';
$emoji.innerText = '';
} else $emoji.dataset.emojiSetsUnsupported = true;
}
};
web.addDocumentObserver(updateEmojis, [nativeEmojiSelector]);
}
if (style !== 'twitter' && imgEmojis) {
const updateEmojis = async () => {
const $emojis = document.querySelectorAll(imgEmojiSelector);
for (const $emoji of $emojis) {
const emojiSrc = await getEmoji($emoji.ariaLabel);
if (emojiSrc) {
$emoji.style.background = emojiSrc;
$emoji.style.opacity = 1;
if ($emoji.nextElementSibling?.matches?.(imgEmojiOverlaySelector)) {
$emoji.nextElementSibling.style.opacity = 0;
}
} else $emoji.dataset.emojiSetsUnsupported = true;
}
};
updateEmojis();
web.addDocumentObserver(updateEmojis, [imgEmojiSelector, imgEmojiOverlaySelector]);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,46 +0,0 @@
{
"name": "emoji sets",
"id": "a2401ee1-93ba-4b8c-9781-7f570bf5d71e",
"version": "0.4.0",
"description": "pick from a variety of emoji styles to use.",
"preview": "emoji-sets.jpg",
"tags": ["extension", "customisation"],
"authors": [
{
"name": "dragonwocky",
"email": "thedragonring.bod@gmail.com",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
}
],
"js": {
"client": ["client.mjs"]
},
"css": {
"client": ["client.css"]
},
"options": [
{
"type": "select",
"key": "style",
"label": "emoji style",
"tooltip": "**initial use may involve some lag and load-time for emojis until they have all been cached**",
"values": [
"twitter",
"apple",
"google",
"microsoft",
"samsung",
"whatsapp",
"facebook",
"messenger",
"joypixels",
"openmoji",
"emojidex",
"lg",
"htc",
"mozilla"
]
}
]
}

View File

@ -1,25 +0,0 @@
/**
* notion-enhancer: focus
* (c) 2020 Arecsu
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
/* hide topbar and ai */
.notion-sidebar-container[aria-hidden] ~ div {
:is(.notion-topbar, .notion-help-button, .notion-ai-button) {
opacity: 0 !important;
transition: opacity 200ms ease-in-out !important;
}
.notion-topbar:hover {
opacity: 1 !important;
}
}
/* hide tabs */
body > #root.sidebar-collapsed {
transition: opacity 200ms ease-in-out;
&:not(:hover) {
opacity: 0;
}
}

View File

@ -1,23 +0,0 @@
/**
* notion-enhancer: focus
* (c) 2020 Arecsu
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async (api, db) => {
// tabs can only be hidden in the desktop app
const { platform, sendMessage, addMutationListener } = api;
if (!["linux", "win32", "darwin"].includes(platform)) return;
let _state;
const sidebar = ".notion-sidebar-container",
onUpdate = () => {
const $sidebar = document.querySelector(sidebar),
state = $sidebar.hasAttribute("aria-hidden") ? "collapsed" : "pinned";
if (state === _state) return;
sendMessage("notion-enhancer:focus", "sidebar-" + (_state = state));
};
addMutationListener(sidebar, onUpdate, { childList: false, subtree: false });
onUpdate();
};

View File

@ -1,17 +0,0 @@
/**
* notion-enhancer: focus
* (c) 2020 Arecsu
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
module.exports = async (api, db) => {
const { ipcMain, BrowserWindow } = require("electron"),
channel = "notion-enhancer:focus";
ipcMain.on(channel, ({ sender }, message) => {
const views = BrowserWindow.fromWebContents(sender).getBrowserViews();
for (const view of views) view.webContents.send(channel, message);
});
};

View File

@ -1,25 +0,0 @@
{
"name": "Focus",
"version": "0.4.0",
"id": "5a08598d-bfac-4167-9ae8-2bd0e2ef141e",
"description": "Enter focus mode when the left sidebar is closed, hiding Notion's extraneous interface elements (e.g. the topbar) until they are hovered over.",
"tags": ["productivity", "focus-mode"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "Arecsu",
"homepage": "https://github.com/Arecsu",
"avatar": "https://avatars.githubusercontent.com/u/12679098"
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"],
"electronScripts": [
[".webpack/main/index.js", "electron.cjs"],
[".webpack/renderer/tabs/preload.js", "tabs.cjs"]
]
}

View File

@ -1,18 +0,0 @@
/**
* notion-enhancer: focus
* (c) 2020 Arecsu
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
module.exports = async (api, db) => {
const { onMessage } = api,
focusClass = "sidebar-collapsed";
onMessage("notion-enhancer:focus", (message) => {
const $root = document.querySelector("#root");
if (message === "sidebar-pinned") $root?.classList.remove(focusClass);
if (message === "sidebar-collapsed") $root?.classList.add(focusClass);
});
};

View File

@ -1,52 +0,0 @@
/**
* notion-enhancer: fonts
* (c) 2021 TorchAtlas (https://github.com/torchatlas/)
* (c) 2021 admiraldus (https://github.com/admiraldus)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
:root {
--font--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont,
"Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif,
"Segoe UI Emoji", "Segoe UI Symbol";
--font--serif: Lyon-Text, Georgia, ui-serif, serif;
--font--mono: iawriter-mono, Nitti, Menlo, Courier, monospace;
--font--code: "SFMono-Regular", Menlo, Consolas, "PT Mono",
"Liberation Mono", Courier, monospace;
--font--math: KaTeX_Main, Times New Roman, serif;
--font--quotes: inherit;
--font--headings: inherit;
}
[style*="Segoe UI"] {
font-family: var(--font--sans) !important;
}
[style*="Georgia"] {
font-family: var(--font--serif) !important;
}
[style*="iawriter-mono"] {
font-family: var(--font--mono) !important;
}
[style*=SFMono-Regular] {
font-family: var(--font--code) !important;
}
[placeholder='Untitled'],
[placeholder='Heading 1'],
[placeholder='Heading 2'],
[placeholder='Heading 3'] {
font-family: var(--font--headings) !important;
}
.notion-quote-block {
font-family: var(--font--quotes) !important;
}
.katex,
.katex * {
font-family: var(--font--math) !important;
}

View File

@ -1,23 +0,0 @@
/**
* notion-enhancer: fonts
* (c) 2021 TorchAtlas (https://github.com/torchatlas/)
* (c) 2021 admiraldus (https://github.com/admiraldus
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async (api, db) => {
const $root = document.documentElement;
for (const style of [
"sans",
"serif",
"mono",
"code",
"math",
"quotes",
"headings",
]) {
const font = await db.get(style);
if (font) $root.style.setProperty(`--font--${style}`, font);
}
};

View File

@ -1,66 +0,0 @@
{
"name": "Fonts",
"version": "0.5.0",
"id": "e0d8d148-45e7-4d79-8313-e7b2ad8abe16",
"description": "Replace Notion's default fonts with any font installed on your system.",
"tags": ["customisation", "font-chooser"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "TorchAtlas",
"homepage": "https://github.com/torchatlas/",
"avatar": "https://avatars.githubusercontent.com/u/12666855"
}
],
"options": [
{
"type": "text",
"key": "sans",
"label": "Sans serif",
"description": "Sets the font used across Notion's interface and as the default page font. Leave this blank to use Notion's default sans serif font.",
"value": "ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, \"Apple Color Emoji\", Arial, sans-serif, \"Segoe UI Emoji\", \"Segoe UI Symbol\""
},
{
"type": "text",
"key": "serif",
"description": "Sets the font used on serif-styled pages (configurable via the <i class='i-ellipsis -mb-px'></i> <i>Style, export and more...</i> menu). Leave this blank to use Notion's default serif font.",
"value": "Lyon-Text, Georgia, ui-serif, serif"
},
{
"type": "text",
"key": "mono",
"description": "Sets the font used on mono-styled pages (configurable via the <i class='i-ellipsis -mb-px'></i> <i>Style, export and more...</i> menu). Leave this blank to use Notion's default monospaced font.",
"value": "iawriter-mono, Nitti, Menlo, Courier, monospace"
},
{
"type": "text",
"key": "code",
"description": "Sets the font used for code blocks and inline code. Leave this blank to use Notion's default code font.",
"value": "\"SFMono-Regular\", Menlo, Consolas, \"PT Mono\", \"Liberation Mono\", Courier, monospace"
},
{
"type": "text",
"key": "math",
"description": "Sets the font used for math equations. Leave this blank to use Notion's default math font.",
"value": "KaTeX_Main, Times New Roman, serif"
},
{
"type": "text",
"key": "quotes",
"description": "Sets the font used for quote blocks. Leave this blank to inherit the page's font style.",
"value": ""
},
{
"type": "text",
"key": "headings",
"description": "Sets the font used for page headings. Leave this blank to inherit the page's font style.",
"value": ""
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"]
}

View File

@ -1,84 +0,0 @@
/**
* notion-enhancer: indent guides
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
body {
--guide--style: solid;
--guide--color: var(--theme--fg-border);
--guide--opacity: 0;
}
/* add indent guides to nested blocks */
.notion-header-block,
.notion-sub_header-block,
.notion-sub_sub_header-block,
.notion-toggle-block,
.notion-to_do-block,
.notion-bulleted_list-block,
.notion-numbered_list-block {
--guide--offset: 32px;
--guide--indent: 14px;
position: relative;
&:before {
content: "";
position: absolute;
height: calc(100% - var(--guide--offset));
top: var(--guide--offset);
margin-inline-start: var(--guide--indent);
border-left: 1px var(--guide--style) var(--guide--color);
opacity: var(--guide--opacity);
}
}
.notion-header-block {
--guide--offset: 47px;
}
.notion-sub_header-block {
--guide--offset: 40px;
}
.notion-header-block,
.notion-sub_header-block,
.notion-sub_sub_header-block,
.notion-toggle-block {
--guide--indent: 13.4px;
}
/* add indent guides to toc blocks & the outliner */
.notion-table_of_contents-block
[contenteditable="false"]
a
> div:not([style*="margin-left: 0"]),
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
position: relative;
--guide--indent: -16px;
&:before {
content: "";
top: 0;
position: absolute;
height: 100%;
margin-inline-start: var(--guide--indent);
border-left: 1px var(--guide--style) var(--guide--color);
opacity: var(--guide--opacity);
}
}
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
--guide--indent: -12px;
}
/* add solid background to drag handles,
otherwise guides show through underneath */
[role="button"]:is([aria-label="Drag"], [aria-label^="Click to add below"]) {
position: relative;
&:before {
content: "";
z-index: -1;
position: absolute;
width: 100%;
height: 100%;
background: var(--theme--bg-primary);
}
}

View File

@ -1,66 +0,0 @@
/**
* notion-enhancer: indent guides
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async function (api, db) {
const { html } = api,
guideStyle = await db.get("guideStyle"),
rainbowMode = await db.get("rainbowMode");
document.body.style.setProperty("--guide--style", guideStyle.toLowerCase());
const nestedTargets = [],
outlineTargets = [];
for (const [listType, selectors] of [
["to-doList", [".notion-to_do-block"]],
["bulletedList", [".notion-bulleted_list-block"]],
["numberedList", [".notion-numbered_list-block"]],
["toggleList", [".notion-toggle-block"]],
[
"toggleHeadings",
[
".notion-header-block",
".notion-sub_header-block",
".notion-sub_sub_header-block",
],
],
]) {
if (await db.get(listType)) nestedTargets.push(...selectors);
}
if (await db.get("tableOfContents"))
outlineTargets.push(".notion-table_of_contents-block");
if (await db.get("outliner"))
outlineTargets.push(".notion-enhancer--outliner-heading");
let css = `${[...nestedTargets, ...outlineTargets].join(",")} {
--guide--opacity: 1;
}`;
if (rainbowMode) {
const opacity = `--guide--opacity: 0.5;`,
selector = `:is(${nestedTargets.join(",")})`,
colours = ["green", "blue", "purple", "pink", "red", "orange", "yellow"];
colours.push(...colours, ...colours, ...colours, "gray");
for (let i = 0; i < colours.length; i++) {
css += `${(selector + " ").repeat(i + 1)} {
--guide--color: var(--theme--fg-${colours[i]});
${opacity}
}`;
}
css += `
.notion-table_of_contents-block [contenteditable="false"] a
> div[style*="margin-left: 24px"],
.notion-enhancer--outliner-heading.pl-\\[36px\\] {
--guide--color: var(--theme--fg-${colours[0]});
${opacity}
}
.notion-table_of_contents-block [contenteditable="false"] a
> div[style*="margin-left: 48px"],
.notion-enhancer--outliner-heading.pl-\\[54px\\] {
--guide--color: var(--theme--fg-${colours[1]});
${opacity}
}`;
}
document.head.append(html`<style innerHTML=${css}></style>`);
}

View File

@ -1,79 +0,0 @@
{
"name": "Indent Guides",
"id": "35815b3b-3916-4dc6-8769-c9c2448f8b57",
"version": "0.3.0",
"description": "Marks list indentation with vertical lines to make it easy to follow.",
"tags": ["extension", "usability", "indentation-lines"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "runargs",
"email": "alnbaldon@gmail.com",
"homepage": "http://github.com/runargs",
"avatar": "https://avatars.githubusercontent.com/u/39810066"
}
],
"options": [
{
"type": "select",
"key": "guideStyle",
"description": "The type of line to use for indent guides.",
"values": ["Solid", "Dashed", "Dotted"]
},
{
"type": "toggle",
"key": "rainbowMode",
"description": "By default, indent guides are coloured based on the current theme. Rainbow mode uses alternating colours at each indent level to better connect corresponding blocks in longer lists.",
"value": false
},
{ "type": "heading", "label": "List Types" },
{
"type": "toggle",
"key": "to-doList",
"description": "Shows indent guides for Notion's to-do list blocks.",
"value": true
},
{
"type": "toggle",
"key": "bulletedList",
"description": "Shows indent guides for Notion's bulleted list blocks.",
"value": true
},
{
"type": "toggle",
"key": "numberedList",
"description": "Shows indent guides for Notion's numbered list blocks.",
"value": true
},
{
"type": "toggle",
"key": "toggleList",
"description": "Shows indent guides for Notion's toggle list blocks.",
"value": true
},
{
"type": "toggle",
"key": "toggleHeadings",
"description": "Shows indent guides for Notion's toggle heading blocks.",
"value": true
},
{
"type": "toggle",
"key": "tableOfContents",
"description": "Shows indent guides for Notion's table of contents blocks.",
"value": true
},
{
"type": "toggle",
"key": "outliner",
"description": "Shows indent guides for the Outliner's table of contents in the side panel.",
"value": true
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"]
}

View File

@ -1,105 +0,0 @@
/**
* notion-enhancer: line numbers
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function LineNumbers({ decorationStyle = "None" }) {
const { html } = globalThis.__enhancerApi,
decorations = {
Border: `pr-[16px] border-r-([2px]
[color:var(--theme--bg-hover)])`,
Background: `pr-[4px] before:(absolute block
h-full w-[calc(100%-24px)] rounded-[4px] right-0
content-empty bg-[var(--theme--bg-hover)] z-[-1])`,
};
return html`<div
class="notion-enhancer--line-numbers mt-[34px]
text-([85%] [var(--theme--fg-secondary)] right)
font-[var(--font--code)] overflow-hidden select-none
relative flex-grow ${decorations[decorationStyle] || ""}"
></div>`;
}
export default async (api, db) => {
const { html, addMutationListener } = api,
decorationStyle = await db.get("decorationStyle"),
numberSingleLines = await db.get("numberSingleLines"),
codeBlockSelector = ".notion-code-block.line-numbers > .notranslate";
// get character width in pixels
const getCharWidth = ($elem) => {
const $char = html`<span style="width:1ch"> </span>`;
$elem.append($char);
const charWidth = getComputedStyle($char).getPropertyValue("width");
$char.remove();
return parseFloat(charWidth);
},
// get line width in pixels
getLineWidth = ($elem) =>
parseFloat(getComputedStyle($elem).getPropertyValue("width")) -
parseFloat(getComputedStyle($elem).getPropertyValue("padding-left")) -
parseFloat(getComputedStyle($elem).getPropertyValue("padding-right")),
// get line height in pixels
getLineHeight = ($elem) =>
parseFloat(getComputedStyle($elem).getPropertyValue("line-height")),
// update inline styles without unnecessary dom updates
applyStyles = ($elem, styles) => {
for (const property in styles) {
if ($elem.style[property] === styles[property]) continue;
$elem.style[property] = styles[property];
}
};
const numberLines = () => {
for (const $code of document.querySelectorAll(codeBlockSelector)) {
const wrap = $code.style.wordBreak === "break-all",
lines = $code.innerText.split("\n"),
numLines = Math.max(lines.length - 1, 1),
numChars = lines.map((line) => line.length).join(","),
numDigits = (Math.log(numLines) * Math.LOG10E + 1) | 0;
if ($code.dataset.lines === wrap + "," + numChars) continue;
$code.dataset.lines = wrap + "," + numChars;
// do not add to single-line blocks if disabled
const visible = numberSingleLines || numLines > 1,
width = visible
? decorationStyle === "Border"
? `calc(100% - 50px - ${numDigits}ch)`
: `calc(100% - 32px - ${numDigits}ch)`
: "",
paddingLeft = visible && decorationStyle === "Border" ? "16px" : "32px";
// shrink block to allow space for numbers
applyStyles($code.parentElement, { justifyContent: "flex-end" });
applyStyles($code, { minWidth: width, maxWidth: width, paddingLeft });
// calculate heights of wrapped lines and render line nums
let totalHeight = 0;
const lineHeight = getLineHeight($code),
charsPerLine = Math.floor(getLineWidth($code) / getCharWidth($code));
$code._$lineNumbers ||= html`<${LineNumbers}...${{ decorationStyle }} />`;
for (let i = 1; i <= numLines; i++) {
const $n = $code._$lineNumbers.children[i - 1] || html`<p>${i}</p>`;
if (!$code._$lineNumbers.contains($n)) $code._$lineNumbers.append($n);
const wrappedHeight =
wrap && lines[i - 1].length > charsPerLine
? Math.ceil(lines[i - 1].length / charsPerLine) * lineHeight
: lineHeight;
applyStyles($n, { height: `${wrappedHeight}px` });
totalHeight += wrappedHeight;
}
applyStyles($code._$lineNumbers, {
display: visible ? "" : "none",
height: `${totalHeight}px`,
});
if (visible && !document.contains($code._$lineNumbers)) {
$code.before($code._$lineNumbers);
} else if (!visible) $code._$lineNumbers.style.display = "none";
}
};
addMutationListener(codeBlockSelector, numberLines);
};

View File

@ -1,34 +0,0 @@
{
"name": "Line Numbers",
"id": "d61dc8a7-b195-465b-935f-53eea9efe74e",
"version": "0.5.0",
"description": "Adds line numbers to code blocks.",
"tags": ["code-line-numbers"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "CloudHill",
"homepage": "https://github.com/CloudHill",
"avatar": "https://avatars.githubusercontent.com/u/54142180"
}
],
"options": [
{
"type": "toggle",
"key": "numberSingleLines",
"description": "Adds line numbers to code blocks with only one line.",
"value": true
},
{
"type": "select",
"key": "decorationStyle",
"description": "Decorates line numbers with additional styling to distinguish them from code block content.",
"values": ["Border", "Background", "None"]
}
],
"clientScripts": ["client.mjs"]
}

View File

@ -1,9 +0,0 @@
/**
* notion-enhancer: no peeking
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
.notion-peek-renderer {
display: none;
}

View File

@ -1,26 +0,0 @@
/**
* notion-enhancer: no peeking
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async (api) => {
const { addMutationListener } = api,
peekRenderer = ".notion-peek-renderer",
openInFullPage = `[aria-label="Open in full page"]`,
pageId = () => location.pathname.split(/-|\//g).at(-1),
peekId = () => new URLSearchParams(location.search).get("p");
let _pageId = pageId();
const skipPeek = () => {
const $openInFullPage = document.querySelector(openInFullPage);
if (peekId() === _pageId) {
_pageId = pageId();
history.back();
} else if (peekId() && $openInFullPage) {
_pageId = peekId();
$openInFullPage.click();
} else _pageId = pageId();
};
addMutationListener(peekRenderer, skipPeek);
};

View File

@ -1,16 +0,0 @@
{
"name": "No Peeking",
"id": "cb6fd684-f113-4a7a-9423-8f0f0cff069f",
"version": "0.3.0",
"description": "Globally force pages opening in side peek or center peek to open as full pages instead.",
"tags": ["automation"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"]
}

View File

@ -1,135 +0,0 @@
/**
* notion-enhancer: outliner
* (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { Heading } from "./islands/Heading.mjs";
import { PanelDescription } from "./islands/PanelDescription.mjs";
export default async (api, db) => {
const { html, debounce, addMutationListener, addPanelView } = api,
behavior = (await db.get("smoothScrolling")) ? "smooth" : "auto",
scroller = ".notion-frame .notion-scroller",
equation = ".notion-text-equation-token",
annotation = (await db.get("equationRendering"))
? ".katex-html"
: ".katex-mathml annotation",
page = ".notion-page-content",
headings = [
".notion-header-block",
".notion-sub_header-block",
".notion-sub_sub_header-block",
],
$toc = html`<div></div>`;
addPanelView({
title: "Outliner",
// prettier-ignore
$icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<circle cx="5" cy="7" r="2.8"/>
<circle cx="5" cy="17" r="2.79"/>
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
</svg>`,
$view: html`<section>
<${PanelDescription}>Click on a heading to jump to it.<//>
${$toc}
</section>`,
});
const replaceFloatingOutline = await db.get("replaceFloatingOutline");
if (replaceFloatingOutline) {
document.head.append(html`<style>
.hide-scrollbar.ignore-scrolling-container:has(
div:empty[style*="width"]
) {
display: none !important;
}
</style>`);
}
let $page, $scroller;
const getHeadings = () => {
if (!$page) return [];
return [...$page.querySelectorAll(headings.join(", "))];
},
getHeadingLevel = ($heading) => {
for (let i = 0; i < headings.length; i++)
if ($heading.matches(headings[i])) return i + 1;
},
getHeadingTitle = ($heading) => {
if (!$heading.innerText) return "Untitled";
let title = "";
for (const node of $heading.querySelector("h2, h3, h4").childNodes) {
if (node.nodeType === 3) title += node.textContent;
else if (node.matches(equation)) {
// https://github.com/notion-enhancer/repo/issues/39
const $katex = node.querySelector(annotation);
title += $katex.textContent;
} else title += node.innerText;
}
return title;
},
getBlockOffset = ($block) => {
let offset = 0;
while (!$block?.matches("[data-content-editable-root]")) {
offset += $block.offsetTop;
$block = $block.offsetParent;
}
return offset;
},
updateHeadings = debounce(() => {
$toc.innerHTML = "";
if (!$page) return;
let indent = 0,
prev_level = 0;
const $frag = document.createDocumentFragment();
for (const $heading of getHeadings()) {
const level = getHeadingLevel($heading);
if (level === 1) indent = 1;
else if (level > prev_level) indent = Math.min(indent + 1, level);
else if (level < prev_level) indent = Math.max(indent - 1, level);
prev_level = level;
$heading._$outline = html`<${Heading}
...${{ indent }}
onclick=${() => {
if (!$scroller) return;
const top = getBlockOffset($heading) - 24;
$scroller.scrollTo({ top, behavior });
}}
>${getHeadingTitle($heading)}
<//>`;
$frag.append($heading._$outline);
}
$toc.append($frag);
onScroll();
});
const $progressMarker = html`<span
class="absolute block left-[6px] top-[calc(50%-1px)]
size-[6px] rounded-full bg-[color:var(--theme--fg-secondary)]"
></span>`,
onScroll = () => {
if (!$scroller) return;
const $h = getHeadings().find(($h) => {
return $scroller.scrollTop < getBlockOffset($h) - 16;
})?._$outline;
if ($h) $h.prepend($progressMarker);
},
setup = () => {
if (document.contains($page)) return;
$page = document.querySelector(page);
$scroller = document.querySelector(scroller);
$scroller?.removeEventListener("scroll", onScroll);
$scroller?.addEventListener("scroll", onScroll);
updateHeadings();
};
const semanticHeadings = '[class$="header-block"] :is(h2, h3, h4)';
addMutationListener(`${page} ${semanticHeadings}`, updateHeadings);
addMutationListener(`${page}, ${scroller}`, setup, { subtree: false });
setup();
};

View File

@ -1,23 +0,0 @@
/**
* notion-enhancer: outliner
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function Heading({ indent, ...props }, ...children) {
const { html } = globalThis.__enhancerApi;
return html`<div
role="button"
class="notion-enhancer--outliner-heading block
relative cursor-pointer select-none text-[14px]
decoration-(2 [color:var(--theme--fg-border)])
hover:bg-[color:var(--theme--bg-hover)]
py-[6px] pr-[2px] pl-[${indent * 18}px]
underline-(~ offset-4) last:mb-[24px]"
...${props}
>
${children}
</div>`;
}
export { Heading };

View File

@ -1,16 +0,0 @@
/**
* notion-enhancer: outliner
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function PanelDescription(props, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
class: `py-[12px] px-[18px] text-(
[13px] [color:var(--theme--fg-secondary)])`,
});
return html` <p ...${props}>${children}</p>`;
}
export { PanelDescription };

View File

@ -1,44 +0,0 @@
{
"name": "Outliner",
"version": "0.5.0",
"id": "87e077cc-5402-451c-ac70-27cc4ae65546",
"description": "Adds a table of contents to the side panel to overview and navigate the current page's headings and subheadings.",
"tags": [
"panel"
],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "CloudHill",
"homepage": "https://github.com/CloudHill",
"avatar": "https://avatars.githubusercontent.com/u/54142180"
}
],
"options": [
{
"type": "toggle",
"key": "smoothScrolling",
"description": "Animates scrolling to a heading smoothly. Disable this to jump to a heading instantly when clicking it in the Outliner's table of contents.",
"value": true
},
{
"type": "toggle",
"key": "equationRendering",
"description": "Attempts to render special symbols from inline equations in headings. Note that position- and size-based formatting will be lost when displaying equations in the Outliner's table of contents. Disable this to display the raw TeX equation instead.",
"value": true
},
{
"type": "toggle",
"key": "replaceFloatingOutline",
"description": "Disables Notion's builtin floating table of contents for a complete switch to the Outliner.",
"value": true
}
],
"clientScripts": [
"client.mjs"
]
}

View File

@ -1,15 +0,0 @@
/**
* notion-enhancer: right to left
* (c) 2021 obahareth <omar@omar.engineer> (https://omar.engineer)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
/* indent rtl toc header levels,
* https://github.com/notion-enhancer/notion-enhancer/issues/616 */
.notion-table_of_contents-block div[style*="margin-left: 24px"] {
margin-inline-start: 24px;
}
.notion-table_of_contents-block div[style*="margin-left: 48px"] {
margin-inline-start: 48px;
}

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