Repository: manusa/electronim
Pull request #31 (2020-08-02 09:55:08)

🧹 Refactored chrome-tabs module
- Completely based on Preact
- chrome-tabs external dependency only used for styles
- Ditched draggabilly
- Probably fixed issue related with draggabilly on MacOS
- Fixed edge-case bugs when loading invalid settings

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='98.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [98.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='98.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [98.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='98.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [98.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


manusa: Please @visomar  could you verify nothing breaks on MacOS 🙏 

Hopefully, this will also fix the problem you reported when dragging tabs around on mac and `mouseup` event not being picked up.

Unlike [Draggabilly](https://draggabilly.desandro.com/), the current implementation is completely based on [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API).
visomar: Nothing seems to be broken on macOS, and the behavior of starting dragging without the mouseDown being held is gone, but now there is some strange blinking when the tab is cliked or at the beginning of being dragged. I'll include a gif to show it.
visomar: 
![screencast 2020-08-02 12-36-41](https://user-images.githubusercontent.com/7437402/89121224-5a78e000-d4bd-11ea-82e6-d475a5b68876.gif)

It's difficult to describe it, but sometimes, the tab bar is duplicated right below the original one... sometimes it disappears before rerendering again. Either way, there is this blinking which is disturbing, and not ideal for the UX.
manusa: Gosh I hate 🍏. I think I can fix partially the issue. But the flickering when tabs change is really strange and the fix will probably be ugly.
visomar: How about this? Since the scope of this refactor is achieved, you proceed with this PR, and we'll think of a way to solve that in the future... ok?
It's going to be difficult for you to fix it specially if this isn't happening to you (or you don't have a way to reproduce it).
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='97.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [97.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='97.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [97.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


manusa: I can't be completely sure but I think [this commit](https://github.com/manusa/electronim/pull/31/commits/2a435db0a9bac42d9731c73f726e61f9cfe45939) may fix the issue, or at least improve the experience.

Could you retry 🙏 ?

I really don't feel comfortable making the current experience worse for macOS users (even if I hate their system),
visomar: It worked! Now both the issue with the drag and drop, and the blinking are gone. Good job!
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='97.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [97.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=31&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='97.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list) [97.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=31&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/README.md b/README.md
index e8d886e..9dfc7a2 100644
--- a/README.md
+++ b/README.md
@@ -62,9 +62,8 @@ spell checking for free.
 
 - [Electron](https://electronjs.org/)
 - [Bulma](https://github.com/jgthms/bulma)
+- [Preact](https://github.com/preactjs/preact)
 - [Chrome tabs](https://github.com/adamschwartz/chrome-tabs#readme)
-- [Draggabilly](https://github.com/desandro/draggabilly)
 - [Font Awesome Free](https://github.com/FortAwesome/Font-Awesome)
 - [Nodehun](https://github.com/Wulf/nodehun/)
 - [Woorm's dictionary repo](https://github.com/wooorm/dictionaries)
-- [Preact](https://preactjs.com)
diff --git a/docs/Keyboard-shortcuts.md b/docs/Keyboard-shortcuts.md
index 8c8918c..8e37691 100644
--- a/docs/Keyboard-shortcuts.md
+++ b/docs/Keyboard-shortcuts.md
@@ -1,6 +1,8 @@
 # Keyboard Shortcuts
 
-- Ctrl+r / Cmd+r / F5: Reload current tab.
-- Ctrl+**+** / Cmd+**+** / Ctrl+ScrollUp / Cmd+ScrollUp: Zoom in.
-- Ctrl+**-** / Cmd+**-** / Ctrl+ScrollDown / Cmd+ScrollDown: Zoom out.
-- Ctrl+**0** / Cmd+**0**: Reset zoom.
\ No newline at end of file
+| Key combination                                                      | Description         |
+|----------------------------------------------------------------------|---------------------|
+| `Ctrl+r` <br/> `Cmd+r` <br/> `F5`                                    | Reload current tab. |
+| `Ctrl++` <br/> `Cmd++` <br/> `Ctrl+ScrollUp` <br/> `Cmd+ScrollUp`    | Zoom in.            |
+| `Ctrl+-` <br/> `Cmd+-`<br/> `Ctrl+ScrollDown` <br/> `Cmd+ScrollDown` | Zoom out.           |
+| `Ctrl+0` <br/> `Cmd+0`                                               | Reset zoom.         |
\ No newline at end of file
diff --git a/docs/Roadmap.md b/docs/Roadmap.md
index 5e20ae5..297b892 100644
--- a/docs/Roadmap.md
+++ b/docs/Roadmap.md
@@ -4,22 +4,24 @@
 
 ### Internationalization
 
-- String literals retrieved using translation library
+- [ ] String literals retrieved using translation library
 
 ### Notification handling
 
-- Notifications can be snoozed [#3](https://github.com/manusa/electronim/issues/3)
-- Private notifications (hidden content) [#4](https://github.com/manusa/electronim/issues/4)
+- [x] Notifications can be disabled [#28](https://github.com/manusa/electronim/pull/28)
+- [ ] Notifications can be snoozed [#3](https://github.com/manusa/electronim/issues/3)
+- [ ] Private notifications (hidden content) [#4](https://github.com/manusa/electronim/issues/4)
 
 ### Electron
 
-- Screen sharing [#12](https://github.com/manusa/electronim/issues/12)
-- Disable individual applications [#5](https://github.com/manusa/electronim/issues/5)
+- [ ] Screen sharing [#12](https://github.com/manusa/electronim/issues/12)
+- [x] Disable individual applications [#5](https://github.com/manusa/electronim/issues/5)
 
 ### NFR
 
-- Better support for Snap ([#20](https://github.com/manusa/electronim/issues/20)...)
-- Embed dictionaries in repository // Pre-download dictionaries from original sources (Chromium, LibreOffice, etc.) 
-- Replace current templates to use [preact](https://github.com/preactjs/preact) instead [#21](https://github.com/manusa/electronim/pull/21).
-- ~~Evaluate migration to use template engines such as [lit-html](https://github.com/Polymer/lit-html)
+- [ ] Better support for Snap ([#20](https://github.com/manusa/electronim/issues/20)...)
+- [ ] Embed dictionaries in repository // Pre-download dictionaries from original sources (Chromium, LibreOffice, etc.) 
+- [ ] Replace current templates to use [preact](https://github.com/preactjs/preact) instead
+      [#21](https://github.com/manusa/electronim/pull/21).
+- [ ] ~~Evaluate migration to use template engines such as [lit-html](https://github.com/Polymer/lit-html)
   or [hyperHTML](https://github.com/WebReflection/hyperHTML). Or maybe use [preact](https://preactjs.com/) instead.~~
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index fc596fa..56b85ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2411,15 +2411,6 @@
       "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
       "dev": true
     },
-    "draggabilly": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/draggabilly/-/draggabilly-2.3.0.tgz",
-      "integrity": "sha512-gSsHrIE3rYK9fhK2vQC83t2YsbKbAHQkLsxsXw9TI3LUziq8EZXjX8g7eaf/NlrtWTFqgz7FmP/kdIvLjt64wA==",
-      "requires": {
-        "get-size": "^2.0.2",
-        "unidragger": "^2.3.0"
-      }
-    },
     "duplexer3": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -2445,9 +2436,9 @@
       }
     },
     "electron": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-9.1.0.tgz",
-      "integrity": "sha512-VRAF8KX1m0py9I9sf0kw1kWfeC87mlscfFcbcRdLBsNJ44/GrJhi3+E8rKbpHUeZNQxsPaVA5Zu5Lxb6dV/scQ==",
+      "version": "9.1.2",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-9.1.2.tgz",
+      "integrity": "sha512-xEYadr3XqIqJ4ktBPo0lhzPdovv4jLCpiUUGc2M1frUhFhwqXokwhPaTUcE+zfu5+uf/ONDnQApwjzznBsRrgQ==",
       "requires": {
         "@electron/get": "^1.0.1",
         "@types/node": "^12.0.12",
diff --git a/package.json b/package.json
index 1d6c16f..80d5051 100644
--- a/package.json
+++ b/package.json
@@ -172,8 +172,7 @@
     "dictionary-sv": "^3.0.1",
     "dictionary-tr": "^1.3.3",
     "dictionary-uk": "^2.1.1",
-    "draggabilly": "^2.3.0",
-    "electron": "^9.1.0",
+    "electron": "^9.1.2",
     "htm": "^3.0.4",
     "nodehun": "^3.0.1",
     "preact": "^10.4.5"
diff --git a/src/chrome-tabs/browser-chrome-tabs.js b/src/chrome-tabs/browser-chrome-tabs.js
index 119473d..81e14eb 100644
--- a/src/chrome-tabs/browser-chrome-tabs.js
+++ b/src/chrome-tabs/browser-chrome-tabs.js
@@ -14,68 +14,229 @@
    limitations under the License.
  */
 /* eslint-disable no-undef */
-const chromeTabs = new ChromeTabs();
+const {h, render} = window.preact;
+const {useLayoutEffect, useReducer, useState} = window.preactHooks;
+const html = window.htm.bind(h);
+
+const getTabContainer = () => document.querySelector('.tab-container');
+const getChromeTabs = () => getTabContainer().querySelector('.chrome-tabs');
+
+const openSettings = () => ipcRenderer.send(APP_EVENTS.settingsOpenDialog);
+const sendTabsReady = () => ipcRenderer.send(APP_EVENTS.tabsReady, {});
+const sendActivateTab = id => ipcRenderer.send(APP_EVENTS.activateTab, {id});
+const sendReorderTabs = tabs =>
+  ipcRenderer.send(APP_EVENTS.tabReorder, {tabIds: tabs.map(({id}) => id)});
+
+const TRANSPARENT_GIF = new Image();
+TRANSPARENT_GIF.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
+const TAB_CONTENT_MARGIN = 9;
+const TAB_CONTENT_OVERLAP_DISTANCE = 1;
+const TAB_CONTENT_MAX_WIDTH = 240;
+
+const ACTIONS = {
+  ACTIVATE_TAB: 'ACTIVATE_TAB',
+  MOVE_TAB: 'MOVE_TAB',
+  SET_TAB_PROPERTY: 'SET_TAB_PROPERTY',
+  SET_TABS: 'SET_TABS'
+};
+
+const initialState = {
+  tabs: []
+};
 
-const getTab = tabId => chromeTabs.tabEls.find(tabEl => tabEl.dataset.tabId === tabId);
+const reducer = (state, action) => {
+  switch (action.type) {
+    case ACTIONS.ACTIVATE_TAB: {
+      return {...state,
+        tabs: state.tabs.map(tab => ({...tab, active: tab.id === action.payload}))
+      };
+    }
+    case ACTIONS.MOVE_TAB: {
+      const {id, idx, offsetX} = action.payload;
+      const fromIdx = state.tabs.findIndex(t => t.id === id);
+      const ret = {...state, tabs: [...state.tabs]};
+      ret.tabs.forEach(t => (t.offsetX = 0));
+      const tab = ret.tabs[fromIdx];
+      tab.offsetX = offsetX;
+      ret.tabs.splice(fromIdx, 1);
+      ret.tabs.splice(idx, 0, tab);
+      sendReorderTabs(ret.tabs);
+      return ret;
+    }
+    case ACTIONS.SET_TAB_PROPERTY: {
+      const {id, property, value} = action.payload;
+      return {...state, tabs: state.tabs.map(tab => {
+        if (tab.id === id) {
+          const ret = {...tab};
+          ret[property] = value;
+          return ret;
+        }
+        return tab;
+      })};
+    }
+    case ACTIONS.SET_TABS: {
+      return {...state, tabs: [...action.payload]};
+    }
+    default: return state;
+  }
+};
 
-const tabsReady = () => ipcRenderer.send(APP_EVENTS.tabsReady, {});
+const dispatchActivateTab = dispatch => id => dispatch({type: ACTIONS.ACTIVATE_TAB, payload: id});
 
-const addTabs = (event, tabs) => {
-  tabs.forEach(({id, title = id, favicon = false}) => chromeTabs.addTab({id, title, favicon}));
+const dispatchAddTabs = dispatch => (event, tabs) => {
+  dispatch({type: ACTIONS.SET_TABS, payload: tabs});
   const activeTabMeta = tabs.find(({active}) => active === true);
-  if (activeTabMeta) {
-    const activeTabId = activeTabMeta.id;
-    chromeTabs.setCurrentTab(getTab(activeTabId));
-    ipcRenderer.send(APP_EVENTS.activateTab, {id: activeTabId});
+  if (tabs.length > 0 && activeTabMeta) {
+    sendActivateTab(activeTabMeta.id);
   }
 };
 
-const doForTab = (tabId, func) => {
-  const tab = getTab(tabId);
-  if (tab) {
-    func(tab);
-  }
+const dispatchSetTabTitle = dispatch => (event, {id, title}) => {
+  dispatch({type: ACTIONS.SET_TAB_PROPERTY, payload: {
+    id, property: 'title', value: title
+  }});
 };
 
-const setActiveTab = (event, {tabId}) => {
-  chromeTabs.setCurrentTab(getTab(tabId));
+const dispatchSetTabFavicon = dispatch => (event, {id, favicon}) => {
+  dispatch({type: ACTIONS.SET_TAB_PROPERTY, payload: {
+    id, property: 'favicon', value: favicon
+  }});
 };
 
-const setTabTitle = (event, {id, title}) => doForTab(id, tab =>
-  tab.querySelectorAll('.chrome-tab-title').forEach(element => {
-    element.innerText = title;
-    element.title = title;
-  }));
-
-const setTabFavicon = (event, {id, favicon}) => doForTab(id, tab =>
-  tab.querySelectorAll('.chrome-tab-favicon').forEach(element => {
-    element.style.backgroundImage = `url('${favicon}')`;
-    element.removeAttribute('hidden', '');
-  }));
-
-const init = () => {
-  const $chromeTabs = document.querySelector('.chrome-tabs');
-  chromeTabs.init($chromeTabs);
-
-  const $settingsButton = document.querySelector('.settings__button');
-  $settingsButton.addEventListener('click', () => ipcRenderer.send(APP_EVENTS.settingsOpenDialog));
-
-  ipcRenderer.on(APP_EVENTS.activateTabInContainer, setActiveTab);
-  ipcRenderer.on(APP_EVENTS.addTabs, addTabs);
-  ipcRenderer.on(APP_EVENTS.setTabTitle, setTabTitle);
-  ipcRenderer.on(APP_EVENTS.setTabFavicon, setTabFavicon);
-  document.addEventListener('DOMContentLoaded', tabsReady);
-
-  $chromeTabs.addEventListener('activeTabChange', ({detail}) => {
-    ipcRenderer.send(APP_EVENTS.activateTab, {id: detail.tabEl.dataset.tabId});
-  });
 
-  $chromeTabs.addEventListener('tabReorder', () => {
-    ipcRenderer.send(APP_EVENTS.tabReorder, {tabIds: chromeTabs.tabEls.map(tabEl => tabEl.dataset.tabId)});
+const calculateTabWidth = numberOfTabs => {
+  const availableWidth = (getChromeTabs() || getTabContainer()).clientWidth;
+  const tabsCumulativeOverlappedWidth = (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE;
+  const targetWidth = (availableWidth - (2 * TAB_CONTENT_MARGIN) + tabsCumulativeOverlappedWidth) / numberOfTabs;
+  return Math.min(TAB_CONTENT_MAX_WIDTH, Math.floor(targetWidth));
+};
+
+const calculateNewIndex = (numberOfTabs, currentX, originX, width, currentIndex) => {
+  const offset = currentX - originX;
+  const offsetIdx = Math.round(offset / width);
+  return Math.min(numberOfTabs + 1, Math.max(0, currentIndex + offsetIdx));
+};
+
+const tabStyle = (width, idx, offsetX) =>
+  `width: ${width + (2 * TAB_CONTENT_MARGIN) + TAB_CONTENT_OVERLAP_DISTANCE}px;` +
+  `transform: translate3d(${(width) * idx}px, 0, 0);` +
+  `left: ${offsetX}px`;
+
+const isInVisibleArea = event =>
+  event.clientX > 0 && event.clientY > 0 &&
+  event.clientX <= window.innerWidth && event.clientY <= window.innerHeight;
+
+const Tab = ({dispatch, numberOfTabs, idx, id, active, favicon, offsetX = 0, title, url, width}) => {
+  const tabClick = () => {
+    if (active !== true) {
+      sendActivateTab(id);
+      dispatchActivateTab(dispatch)(id);
+    }
+  };
+  const [draggedId, setDraggedId] = useState(id);
+  const [originX, setOriginX] = useState(0);
+  const [currentIndex, setCurrentIndex] = useState(idx);
+  const tabDrag = event => {
+    const newIdx = calculateNewIndex(numberOfTabs, event.clientX, originX, width, currentIndex);
+    let currentOffsetX = 0;
+    if (isInVisibleArea(event) && event.type === 'drag') {
+      currentOffsetX = (event.clientX - originX) + (width * (idx - newIdx));
+    }
+    dispatch({type: ACTIONS.MOVE_TAB, payload: {
+      id: draggedId,
+      idx: isInVisibleArea(event) ? newIdx : idx,
+      offsetX: currentOffsetX
+    }});
+  };
+  const props = {
+    'data-tab-id': id,
+    draggable: true,
+    onDragStart: event => {
+      tabClick();
+      event.dataTransfer.setDragImage(TRANSPARENT_GIF, 0, 0);
+      setOriginX(event.clientX);
+      setDraggedId(id);
+      setCurrentIndex(idx);
+    },
+    onDrag: tabDrag,
+    onDragEnd: tabDrag,
+    style: tabStyle(width, idx, offsetX),
+    onclick: tabClick
+  };
+  if (active === true) {
+    props.active = '';
+  }
+  const faviconProps = {hidden: true};
+  if (favicon) {
+    faviconProps.style = `background-image: url("${favicon}");`;
+    delete faviconProps.hidden;
+  }
+  return html`
+    <div class="chrome-tab" ...${props}>
+      <div class="chrome-tab-dividers"></div>
+      <div class="chrome-tab-background">
+        <svg version="1.1" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="chrome-tab-geometry-left" viewBox="0 0 214 36"><path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"/></symbol><symbol id="chrome-tab-geometry-right" viewBox="0 0 214 36"><use xlink:href="#chrome-tab-geometry-left"/></symbol><clipPath id="crop"><rect class="mask" width="100%" height="100%" x="0"/></clipPath></defs><svg width="52%" height="100%"><use xlink:href="#chrome-tab-geometry-left" width="214" height="36" class="chrome-tab-geometry"/></svg><g transform="scale(-1, 1)"><svg width="52%" height="100%" x="-100%" y="0"><use xlink:href="#chrome-tab-geometry-right" width="214" height="36" class="chrome-tab-geometry"/></svg></g></svg>
+      </div>
+      <div class="chrome-tab-content">
+        <div class="chrome-tab-favicon" ...${faviconProps}></div>
+        <div class="chrome-tab-title" title=${url}>
+          ${title ? title : url}
+        </div>
+        <div class="chrome-tab-drag-handle"></div>
+        <div class="chrome-tab-close"></div>
+      </div>
+    </div>
+  `;
+};
+
+const ChromeTabs = ({dispatch, state: {tabs}}) => {
+  const [width, setWidth] = useState(calculateTabWidth(tabs.length));
+  const applyWidth = () => setWidth(calculateTabWidth(tabs.length));
+  useLayoutEffect(() => {
+    window.addEventListener('resize', applyWidth);
+    return () => window.removeEventListener('resize', applyWidth);
   });
+  return html`
+    <div
+      class="chrome-tabs"
+      style=${`--tab-content-margin: ${TAB_CONTENT_MARGIN}px`}
+      ondragover=${event => event.preventDefault()}
+    >
+      <div class="chrome-tabs-content">
+        ${tabs.map((tab, idx) => html`
+          <${Tab} ...${{...tab, dispatch, numberOfTabs: tabs.length, idx, width}}/>`)}
+      </div>
+    </div>
+  `;
 };
 
-// $chromeTabs.addEventListener('tabAdd', ({ detail }) => console.log('Tab added', detail.tabEl));
-// $chromeTabs.addEventListener('tabRemove', ({ detail }) => console.log('Tab removed', detail.tabEl));
-init();
+const Settings = () => html`
+ <div class="settings">
+     <button
+        class="settings__button button"
+        onclick=${openSettings}
+      >
+         <span class='icon'>
+           <em class='fas fa-cog'></em>
+         </span>
+     </button>
+ </div>
+`;
+
+const TabContainer = () => {
+  const [state, dispatch] = useReducer(reducer, initialState);
+  useLayoutEffect(() => {
+    ipcRenderer.on(APP_EVENTS.addTabs, dispatchAddTabs(dispatch));
+    ipcRenderer.on(APP_EVENTS.activateTabInContainer,
+      (event, {tabId}) => dispatchActivateTab(dispatch)(tabId));
+    ipcRenderer.on(APP_EVENTS.setTabTitle, dispatchSetTabTitle(dispatch));
+    ipcRenderer.on(APP_EVENTS.setTabFavicon, dispatchSetTabFavicon(dispatch));
+    sendTabsReady();
+  }, []);
+  return html`
+    <${ChromeTabs} state=${state} dispatch=${dispatch}/>
+    <${Settings}/>
+  `;
+};
 
+render(html`<${TabContainer} />`, getTabContainer());
diff --git a/src/chrome-tabs/index.html b/src/chrome-tabs/index.html
index 90b8740..9a27fa5 100644
--- a/src/chrome-tabs/index.html
+++ b/src/chrome-tabs/index.html
@@ -15,31 +15,19 @@ limitations under the License.
 -->
 <!DOCTYPE html>
 <html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Tabs</title>
-    <link rel="stylesheet" href="../../node_modules/chrome-tabs/css/chrome-tabs.css">
-    <link rel="stylesheet" href="../../node_modules/chrome-tabs/css/chrome-tabs-dark-theme.css">
-    <link rel="stylesheet" href="../../node_modules/bulma/css/bulma.min.css">
-    <link rel="stylesheet" href="../../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
-    <link rel="stylesheet" href="./browser-chrome-tabs.css">
-</head>
-<body>
-    <div class="tab-container container">
-        <div class="chrome-tabs" style="--tab-content-margin: 9px">
-            <div class="chrome-tabs-content">
-            </div>
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <title>ElectronIM tabs</title>
+        <link rel="stylesheet" href="../../node_modules/chrome-tabs/css/chrome-tabs.css">
+        <link rel="stylesheet" href="../../node_modules/chrome-tabs/css/chrome-tabs-dark-theme.css">
+        <link rel="stylesheet" href="../../node_modules/bulma/css/bulma.min.css">
+        <link rel="stylesheet" href="../../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
+        <link rel="stylesheet" href="./browser-chrome-tabs.css">
+    </head>
+    <body>
+        <div class="tab-container container">
         </div>
-        <div class="settings">
-            <button class="settings__button button">
-                <span class='icon'>
-                  <em class='fas fa-cog'></em>
-                </span>
-            </button>
-        </div>
-    </div>
-    <script src="../../node_modules/draggabilly/dist/draggabilly.pkgd.min.js"></script>
-    <script src="../../node_modules/chrome-tabs/js/chrome-tabs.js"></script>
-    <script src="./browser-chrome-tabs.js"></script>
-</body>
+        <script src="./browser-chrome-tabs.js"></script>
+    </body>
 </html>
diff --git a/src/chrome-tabs/index.js b/src/chrome-tabs/index.js
index 6c21584..5691a05 100644
--- a/src/chrome-tabs/index.js
+++ b/src/chrome-tabs/index.js
@@ -37,6 +37,7 @@ const handleContextMenu = (mainWindow, browserView) => (event, params) => {
 
 const initTabContainer = mainWindow => {
   const tabContainer = new BrowserView({webPreferences});
+  tabContainer.isTabContainer = true;
   mainWindow.addBrowserView(tabContainer);
   tabContainer.setAutoResize({width: true, horizontal: true});
   tabContainer.webContents.loadURL(`file://${__dirname}/index.html`,
diff --git a/src/chrome-tabs/preload.js b/src/chrome-tabs/preload.js
index 603e0ae..151b2e9 100644
--- a/src/chrome-tabs/preload.js
+++ b/src/chrome-tabs/preload.js
@@ -13,4 +13,7 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
+window.preact = require('preact');
+window.preactHooks = require('preact/hooks');
+window.htm = require('htm');
 require('../main/preload');
diff --git a/src/main/index.js b/src/main/index.js
index f134c6c..3924847 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -29,15 +29,25 @@ const webPreferences = {
 let mainWindow;
 let tabContainer;
 
+const isNotTabContainer = bv => bv.isTabContainer !== true;
+
+const resetMainWindow = () => {
+  const currentViews = mainWindow.getBrowserViews();
+  currentViews.filter(isNotTabContainer).forEach(bv => mainWindow.removeBrowserView(bv));
+  if (mainWindow.getBrowserViews().length === 0) {
+    mainWindow.setBrowserView(tabContainer);
+  }
+};
+
 const activateTab = tabId => {
   const activeTab = tabManager.getTab(tabId);
   if (activeTab) {
     const {width, height} = mainWindow.getContentBounds();
-    mainWindow.setBrowserView(tabContainer);
+    resetMainWindow();
     mainWindow.addBrowserView(activeTab);
-    tabManager.setActiveTab(tabId);
     tabContainer.setBounds({x: 0, y: 0, width, height: TABS_CONTAINER_HEIGHT});
     activeTab.setBounds({x: 0, y: TABS_CONTAINER_HEIGHT, width, height: height - TABS_CONTAINER_HEIGHT});
+    tabManager.setActiveTab(tabId);
     activeTab.webContents.focus();
   }
 };
@@ -78,7 +88,6 @@ const initTabListener = () => {
     if (tabs.length > 0) {
       const ipcSender = event.sender;
       tabManager.addTabs(ipcSender)(tabs);
-      ipcSender.send(APP_EVENTS.activateTab, {tabId: currentSettings.activeTab});
     } else {
       openSettingsDialog(mainWindow);
     }
diff --git a/src/settings/index.js b/src/settings/index.js
index 636b758..1d79fd9 100644
--- a/src/settings/index.js
+++ b/src/settings/index.js
@@ -29,13 +29,13 @@ const webPreferences = {
 const appDir = path.join(HOME_DIR, APP_DIR);
 const settingsPath = path.join(appDir, SETTINGS_FILE);
 
-const containsTabId = settings => tabId =>
-  settings.tabs.map(({id}) => id).includes(tabId);
+const containsTabId = tabs => tabId => tabs.map(({id}) => id).includes(tabId);
 
 const ensureActiveTab = settings => {
   let {activeTab} = settings;
-  if (settings.tabs.length > 0 && !containsTabId(settings)(activeTab)) {
-    activeTab = settings.tabs[0].id;
+  const enabledTabs = settings.tabs.filter(({disabled}) => !disabled);
+  if (enabledTabs.length > 0 && !containsTabId(enabledTabs)(activeTab)) {
+    activeTab = enabledTabs[0].id;
   }
   return {...settings, activeTab};
 };
@@ -48,7 +48,7 @@ const loadSettings = () => {
   if (fs.existsSync(settingsPath)) {
     loadedSettings = JSON.parse(fs.readFileSync(settingsPath));
   }
-  return Object.assign(DEFAULT_SETTINGS, loadedSettings);
+  return ensureActiveTab(Object.assign(DEFAULT_SETTINGS, loadedSettings));
 };
 
 const writeSettings = settings => {


Test patch:
diff --git a/src/chrome-tabs/__tests__/browser-chrome-tabs.test.js b/src/chrome-tabs/__tests__/browser-chrome-tabs.test.js
index 406e810..15f2dc0 100644
--- a/src/chrome-tabs/__tests__/browser-chrome-tabs.test.js
+++ b/src/chrome-tabs/__tests__/browser-chrome-tabs.test.js
@@ -13,127 +13,208 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
+const {createEvent, fireEvent, waitFor} = require('@testing-library/dom');
+
+const mockDOM = () => {
+  document.body.innerHTML = '';
+  const $root = document.createElement('div');
+  $root.innerHTML = '<div class="tab-container container"></div>';
+  document.body.append($root);
+};
+
 describe('ChromeTabs in Browser test suite', () => {
-  let mockChromeTabs;
   let mockIpcRenderer;
+  let $chromeTabs;
   beforeEach(() => {
-    mockChromeTabs = {
-      init: jest.fn(),
-      tabEls: []
-    };
     mockIpcRenderer = {
       events: {},
       on: jest.fn((key, event) => (mockIpcRenderer.events[key] = event)),
       send: jest.fn()
     };
-    window.ChromeTabs = jest.fn(() => mockChromeTabs);
+    window.preact = require('preact');
+    window.preactHooks = require('preact/hooks');
+    window.htm = require('htm');
     window.ipcRenderer = mockIpcRenderer;
     window.APP_EVENTS = {
+      activateTab: 'activateTab',
       activateTabInContainer: 'activateTabInContainer',
       addTabs: 'addTabs',
       setTabFavicon: 'setTabFavicon',
-      setTabTitle: 'setTabTitle'
+      setTabTitle: 'setTabTitle',
+      tabReorder: 'tabReorder',
+      tabsReady: 'tabsReady'
     };
-    ['chrome-tabs', 'settings__button'].forEach(className => {
-      const $domElement = document.createElement('div');
-      $domElement.innerHTML = `<div class="${className}"></div>`;
-      document.body.append($domElement);
-    });
+    mockDOM();
     jest.isolateModules(() => {
       require('../browser-chrome-tabs');
     });
+    $chromeTabs = document.querySelector('.chrome-tabs');
   });
-  test('settingsButton click, should dispatch APP_EVENTS.settingsOpenDialog', () => {
-    // Given
-    window.APP_EVENTS.settingsOpenDialog = 'open your settings please';
-    const $settingsButton = document.querySelector('.settings__button');
-    // When
-    $settingsButton.dispatchEvent(new Event('click'));
-    // Then
-    expect(mockIpcRenderer.send).toHaveBeenCalledWith('open your settings please');
+  test('APP_EVENTS.tabsReady should be fired on load', () => {
+    expect(mockIpcRenderer.send).toHaveBeenCalledWith('tabsReady', {});
   });
-  describe('$chromeTabs events', () => {
-    let $chromeTabs;
+  describe('External events (ipcRenderer.on)', () => {
+    let tabs;
     beforeEach(() => {
-      $chromeTabs = document.querySelector('.chrome-tabs');
+      Object.defineProperty($chromeTabs, 'clientWidth', {value: 100});
+      window.dispatchEvent(new CustomEvent('resize'));
+      tabs = [
+        {id: 1337, active: true, url: 'https://1337.com'},
+        {id: 313373, title: '313373', url: 'https://313373.com'},
+        {id: 13373, favicon: 'https://13373.png', url: 'https://13373.com'}
+      ];
+    });
+    test('addTabs, should set tabs', async () => {
+      // When
+      mockIpcRenderer.events.addTabs({}, tabs);
+      // Then
+      await waitFor(() =>
+        expect($chromeTabs.querySelectorAll('.chrome-tab').length).toBe(3));
+      const $addedTabs = $chromeTabs.querySelectorAll('.chrome-tab');
+      $addedTabs.forEach(tab => expect(tab.style.width).toBe('259px'));
+      expect($addedTabs[0].querySelector('.chrome-tab-title').innerHTML)
+        .toBe('https://1337.com');
+      expect($addedTabs[0].hasAttribute('active')).toBe(true);
+      expect($addedTabs[1].querySelector('.chrome-tab-title').innerHTML)
+        .toBe('313373');
+      expect($addedTabs[1].hasAttribute('active')).toBe(false);
+      expect($addedTabs[2].querySelector('.chrome-tab-favicon').style.backgroundImage)
+        .toBe('url(https://13373.png)');
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('activateTab', {id: 1337});
     });
-    test('activeTabChange', () => {
+    test('activateTabInContainer, should change active tab', async () => {
       // Given
-      window.APP_EVENTS.activateTab = 'activate this tab';
+      mockIpcRenderer.events.addTabs({}, tabs);
       // When
-      $chromeTabs.dispatchEvent(new CustomEvent('activeTabChange', {detail: {tabEl: {dataset: {tabId: 1337}}}}));
+      mockIpcRenderer.events.activateTabInContainer({}, {tabId: 313373});
       // Then
-      expect(mockIpcRenderer.send).toHaveBeenCalledWith('activate this tab', {id: 1337});
+      await waitFor(() =>
+        expect($chromeTabs.querySelector('.chrome-tab[data-tab-id="313373"]').hasAttribute('active')).toBe(true));
     });
-    test('tabReorder', () => {
+    test('setTabTitle, should change title of specified tab', async () => {
       // Given
-      window.APP_EVENTS.tabReorder = 'reorder this tab';
+      mockIpcRenderer.events.addTabs({}, tabs);
       // When
-      $chromeTabs.dispatchEvent(new Event('tabReorder'));
+      mockIpcRenderer.events.setTabTitle({}, {id: 313373, title: 'replaced'});
       // Then
-      expect(mockIpcRenderer.send).toHaveBeenCalledWith('reorder this tab', {tabIds: []});
+      await waitFor(() => expect(
+        $chromeTabs.querySelector('.chrome-tab[data-tab-id="313373"] .chrome-tab-title').innerHTML)
+        .toBe('replaced'));
+      expect(
+        $chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"] .chrome-tab-title').innerHTML)
+        .toBe('https://1337.com');
+    });
+    test('setTabFavicon, should change favicon of specified tab', async () => {
+      // Given
+      mockIpcRenderer.events.addTabs({}, tabs);
+      // When
+      mockIpcRenderer.events.setTabFavicon({}, {id: 313373, favicon: 'https://f/replaced.png'});
+      // Then
+      await waitFor(() => expect(
+        $chromeTabs.querySelector('.chrome-tab[data-tab-id="313373"] .chrome-tab-favicon').style.backgroundImage)
+        .toBe('url(https://f/replaced.png)'));
+      expect(
+        $chromeTabs.querySelector('.chrome-tab[data-tab-id="13373"] .chrome-tab-favicon').style.backgroundImage)
+        .toBe('url(https://13373.png)');
     });
   });
-  describe('ipcRenderer events', () => {
-    test('activateTabInContainer, should call setCurrentTab in ChromeTabs module', () => {
+  describe('Tab events', () => {
+    let tabs;
+    beforeEach(async () => {
+      tabs = [
+        {id: 1337, active: true, url: 'https://1337.com'},
+        {id: 313373, title: '313373', url: 'https://313373.com'},
+        {id: 13373, favicon: 'https://13373.png', url: 'https://13373.com'}
+      ];
+      Object.defineProperty($chromeTabs, 'clientWidth', {value: 100});
+      window.dispatchEvent(new CustomEvent('resize'));
+      mockIpcRenderer.events.addTabs({}, tabs);
+      await waitFor(() => {
+        if ($chromeTabs.querySelectorAll('.chrome-tab').length !== 3) {
+          throw Error('Tabs are not ready');
+        }
+      });
+    });
+    test('click, on inactive tab, should request tab activation', () => {
+      // When
+      fireEvent.click($chromeTabs.querySelector('.chrome-tab[data-tab-id="313373"]'));
+      // Then
+      expect(mockIpcRenderer.send).toHaveBeenNthCalledWith(3, 'activateTab', {id: 313373});
+    });
+    test('click, on active tab, should do nothing', () => {
+      // When
+      fireEvent.click($chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]'));
+      // Then
+      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(2);
+    });
+    test('dragOver, should invoke preventDefault to allow drop', () => {
       // Given
-      mockChromeTabs.tabEls = [{dataset: {tabId: 1337}}];
-      mockChromeTabs.setCurrentTab = jest.fn();
+      const event = createEvent.dragOver($chromeTabs);
+      jest.spyOn(event, 'preventDefault');
       // When
-      mockIpcRenderer.events.activateTabInContainer(new Event(''), {tabId: 1337});
+      fireEvent($chromeTabs, event);
       // Then
-      expect(mockChromeTabs.setCurrentTab).toHaveBeenCalledTimes(1);
-      expect(mockChromeTabs.setCurrentTab).toHaveBeenCalledWith({dataset: {tabId: 1337}});
+      expect(event.preventDefault).toHaveBeenCalledTimes(1);
     });
-    test('addTabs, should call setCurrentTab in ChromeTabs module', () => {
+    test('dragStart, should activate tab and set initial drag values', () => {
       // Given
-      mockChromeTabs.addTab = jest.fn();
-      mockChromeTabs.setCurrentTab = jest.fn();
-      window.APP_EVENTS.activateTab = 'ACTIVATE TAB!';
+      const $tab = $chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]');
+      const event = new MouseEvent('dragstart', {
+        clientX: 100, clientY: 0});
+      Object.defineProperty(event, 'dataTransfer', {value: {
+        setDragImage: jest.fn()
+      }});
       // When
-      mockIpcRenderer.events.addTabs(new Event(''), [
-        {id: 1337, active: true},
-        {id: 313373, title: 'Dr.', favicon: 'Andy Warhol'}
-      ]);
+      fireEvent($tab, event);
       // Then
-      expect(mockChromeTabs.addTab).toHaveBeenCalledTimes(2);
-      expect(mockChromeTabs.addTab).toHaveBeenCalledWith({id: 1337, title: 1337, favicon: false});
-      expect(mockChromeTabs.addTab).toHaveBeenCalledWith({id: 313373, title: 'Dr.', favicon: 'Andy Warhol'});
-      expect(mockChromeTabs.setCurrentTab).toHaveBeenCalledTimes(1);
-      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
-      expect(mockIpcRenderer.send).toHaveBeenCalledWith('ACTIVATE TAB!', {id: 1337});
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('activateTab', {id: 1337});
+      expect(event.dataTransfer.setDragImage).toHaveBeenCalledTimes(1);
     });
-    test('setTabTitle, should set DOM element text and title', () => {
+    test('drag, same position, should keep positions moving current tab left', async () => {
       // Given
-      const element = {};
-      const querySelectorAll = jest.fn(() => ([element]));
-      mockChromeTabs.tabEls = [{
-        dataset: {tabId: 1337},
-        querySelectorAll
-      }];
-      mockChromeTabs.setCurrentTab = jest.fn();
+      const $tab = $chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]');
+      const event = new MouseEvent('drag', {clientX: 100, clientY: 1});
       // When
-      mockIpcRenderer.events.setTabTitle(new Event(''), {id: 1337, title: 'Alex kid'});
+      fireEvent($tab, event);
       // Then
-      expect(querySelectorAll).toHaveBeenCalledTimes(1);
-      expect(element.innerText).toBe('Alex kid');
-      expect(element.title).toBe('Alex kid');
+      await waitFor(() =>
+        expect($chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]').style.left).toBe('100px'));
+      expect($chromeTabs.querySelectorAll('.chrome-tab')[0].dataset.tabId).toBe('1337');
     });
-    test('setTabFavicon, should set DOM element background url', () => {
+    test('drag, one position right, should switch positions in array', async () => {
+      // Given
+      const $tab = $chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]');
+      const event = new MouseEvent('drag', {clientX: 200, clientY: 1});
+      // When
+      fireEvent($tab, event);
+      // Then
+      await waitFor(() =>
+        expect($chromeTabs.querySelectorAll('.chrome-tab')[0].dataset.tabId).toBe('313373'));
+      expect($chromeTabs.querySelectorAll('.chrome-tab')[1].dataset.tabId).toBe('1337');
+      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(3);
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('tabReorder', {tabIds: [313373, 1337, 13373]});
+    });
+    test('drag, one position right out of window, should leave tabs as before drag started', async () => {
+      // Given
+      const $tab = $chromeTabs.querySelector('.chrome-tab[data-tab-id="1337"]');
+      const event = new MouseEvent('drag', {clientX: 200, clientY: -100});
+      // When
+      fireEvent($tab, event);
+      // Then
+      await waitFor(() =>
+        expect(mockIpcRenderer.send).toHaveBeenCalledWith('tabReorder', {tabIds: [1337, 313373, 13373]}));
+      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(3);
+      expect($chromeTabs.querySelectorAll('.chrome-tab')[0].dataset.tabId).toBe('1337');
+    });
+  });
+  describe('Main button events', () => {
+    test('settingsButton click, should dispatch APP_EVENTS.settingsOpenDialog', () => {
       // Given
-      const element = {style: {}, removeAttribute: jest.fn()};
-      const querySelectorAll = jest.fn(() => ([element]));
-      mockChromeTabs.tabEls = [{
-        dataset: {tabId: 1337},
-        querySelectorAll
-      }];
-      mockChromeTabs.setCurrentTab = jest.fn();
+      window.APP_EVENTS.settingsOpenDialog = 'open your settings please';
       // When
-      mockIpcRenderer.events.setTabFavicon(new Event(''), {id: 1337, favicon: 'Andy Warhol'});
+      fireEvent.click(document.querySelector('.settings__button'));
       // Then
-      expect(querySelectorAll).toHaveBeenCalledTimes(1);
-      expect(element.style.backgroundImage).toBe('url(\'Andy Warhol\')');
-      expect(element.removeAttribute).toHaveBeenCalledTimes(1);
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('open your settings please');
     });
   });
 });
diff --git a/src/main/__tests__/index.test.js b/src/main/__tests__/index.test.js
index d9e9fdf..b4f7c12 100644
--- a/src/main/__tests__/index.test.js
+++ b/src/main/__tests__/index.test.js
@@ -190,6 +190,7 @@ describe('Main module test suite', () => {
           webContents: {focus: jest.fn()}
         };
         mockTabContainer.setBounds = jest.fn();
+        mockBrowserWindow.getBrowserViews = jest.fn(() => ([]));
         mockBrowserWindow.setBrowserView = jest.fn();
         mockBrowserWindow.addBrowserView = jest.fn();
         tabManagerModule.getTab = jest.fn(id => (id === 'validId' ? activeTab : null));
diff --git a/src/settings/__tests__/index.test.js b/src/settings/__tests__/index.test.js
index 219de82..cf21a8d 100644
--- a/src/settings/__tests__/index.test.js
+++ b/src/settings/__tests__/index.test.js
@@ -62,7 +62,7 @@ describe('Settings module test suite', () => {
     test('settings exist, should load settings from file system and merge with defaults', () => {
       // Given
       fs.existsSync.mockImplementationOnce(() => true);
-      fs.readFileSync.mockImplementationOnce(() => '{"tabs": [{"id": "1"}], "otherSetting": 1337}');
+      fs.readFileSync.mockImplementationOnce(() => '{"tabs": [{"id": "1"}], "activeTab": "1", "otherSetting": 1337}');
       // When
       const result = settings.loadSettings();
       // Then
@@ -71,8 +71,19 @@ describe('Settings module test suite', () => {
       expect(fs.readFileSync).toHaveBeenCalledWith(path.join('$HOME', '.electronim', 'settings.json'));
       expect(result.tabs).toEqual([{id: '1'}]);
       expect(result.enabledDictionaries).toEqual(['en-US']);
+      expect(result.activeTab).toBe('1');
       expect(result.otherSetting).toBe(1337);
     });
+    test('settings exist disabled tab as active, should load settings from file and ensure active tab is enabled', () => {
+      // Given
+      fs.existsSync.mockImplementationOnce(() => true);
+      fs.readFileSync.mockImplementationOnce(() => '{"tabs": [{"id": "1", "disabled": true}, {"id": "2"}], "activeTab": "1"}');
+      // When
+      const result = settings.loadSettings();
+      // Then
+      expect(result.tabs).toEqual([{id: '1', disabled: true}, {id: '2'}]);
+      expect(result.activeTab).toBe('2');
+    });
   });
   describe('updateSettings', () => {
     test('empty object and NO saved settings, should write default settings', () => {