Skip to content

Commit

Permalink
Implement dark mode
Browse files Browse the repository at this point in the history
  • Loading branch information
LunarN0va committed Jun 12, 2024
1 parent b80c1df commit 205ca0e
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ architecture model:
| `generatr.site.externalTag` | Software systems containing this tag will be considered external | | |
| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable groups | `false` | `true` |
| `generatr.site.cdn` | Specifies the CDN base location for fetching NPM packages for browser runtime dependencies. Defaults to jsDelivr, but can be changed to e.g. an on-premise location. | `https://cdn.jsdelivr.net/npm` | `https://cdn.my-company/npm` |
| `generatr.site.darkMode` | Will show the dark mode button to switch between light and dark mode on the website. If turned off, the site will be shown in light mode and this button will not be shown. Defaults to true/on. | `true` | `false` |

See the included example for usage of some those properties in the
[C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163).
Expand Down
1 change: 1 addition & 0 deletions docs/example/workspace.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea
"generatr.site.externalTag" "External System"
"generatr.site.nestGroups" "false"
"generatr.site.cdn" "https://cdn.jsdelivr.net/npm"
"generatr.site.darkMode" "true"
}

systemlandscape "SystemLandscape" {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"lunr-languages": "1.14.0",
"mermaid": "10.9.1",
"svg-pan-zoom": "3.6.1",
"webfontloader": "1.6.28"
"webfontloader": "1.6.28",
"font-awesome": "4.7.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ fun copySiteWideAssets(exportDir: File) {
copySiteWideAsset(exportDir, "/css/treeview.css")
copySiteWideAsset(exportDir, "/js/treeview.js")
copySiteWideAsset(exportDir, "/js/katex-render.js")
copySiteWideAsset(exportDir, "/js/toggle-theme.js")
}

private fun copySiteWideAsset(exportDir: File, asset: String) {
Expand Down Expand Up @@ -116,6 +117,12 @@ private fun generateStyle(context: GeneratorContext, branchDir: File) {
color: dimgrey!important;
background-color: white!important;
}
body.dark-theme .input.has-site-branding {
background-color: #1b212c!important;
}
i .has-site-branding {
color: $secondary!important
}
.input.has-site-branding:focus {
border-color: $secondary!important;
box-shadow: 0 0 0 0.125em $secondary;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class HeaderBarViewModel(pageViewModel: PageViewModel, generatorContext: Generat
.map { BranchHomeLinkViewModel(pageViewModel, it) }
val currentBranch = generatorContext.currentBranch
val version = generatorContext.version

val showDarkModeButton = generatorContext.workspace.views.configuration.properties
.getOrDefault("generatr.site.darkMode", "true").toBoolean()
private fun logoPath(generatorContext: GeneratorContext) =
generatorContext.workspace.views.configuration.properties
.getOrDefault(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) {
val includedSoftwareSystems = generatorContext.workspace.includedSoftwareSystems
val configuration = generatorContext.workspace.views.configuration.properties
val includeTreeview = configuration.getOrDefault("generatr.site.nestGroups", "false").toBoolean()
val showDarkModeButton = configuration.getOrDefault("generatr.site.darkMode", "true").toBoolean()

abstract val url: String
abstract val pageSubTitle: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class CDN(val workspace: Workspace) {
"${it.baseUrl()}/webfontloader.js"
}

fun fontAwesomeCss() = dependencies.single { it.name == "font-awesome"}.let {
"${it.baseUrl()}/css/font-awesome.min.css"
}

fun getCdnBaseUrl() = workspace.views.configuration.properties
.getOrDefault("generatr.site.cdn", "https://cdn.jsdelivr.net/npm")
.trimEnd('/')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import nl.avisi.structurizr.site.generatr.site.model.PageViewModel

fun HTML.page(viewModel: PageViewModel, block: DIV.() -> Unit) {
attributes["lang"] = "en"
attributes["data-theme"] = "light"
classes = setOf("has-background-light")
if (!viewModel.showDarkModeButton) {
attributes["data-theme"] = "light"
classes = setOf("has-background-light")
}

headFragment(viewModel)
bodyFragment(viewModel, block)
Expand All @@ -19,6 +21,7 @@ private fun HTML.headFragment(viewModel: PageViewModel) {
meta(name = "viewport", content = "width=device-width, initial-scale=1")
title { +viewModel.pageTitle }
link(rel = "stylesheet", href = viewModel.cdn.bulmaCss())
link(rel = "stylesheet", href = viewModel.cdn.fontAwesomeCss())
link(rel = "stylesheet", href = "../" + "/style.css".asUrlToFile(viewModel.url))
link(rel = "stylesheet", href = "./" + "/style-branding.css".asUrlToFile(viewModel.url))
script(type = ScriptType.textJavaScript, src = "../" + "/modal.js".asUrlToFile(viewModel.url)) { }
Expand Down Expand Up @@ -59,7 +62,7 @@ private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {
div(classes = "site-layout") {
id = "site"
menu(viewModel.menu, viewModel.includeTreeview)
div(classes = "container is-fluid has-background-white") {
div(classes = "container is-fluid") {
block()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ fun BODY.pageHeader(viewModel: HeaderBarViewModel) {
}
}
}
if (viewModel.showDarkModeButton) {
div(classes = "navbar-item") {
button(classes = "btn-toggle") {
span(classes = "icon") {
i(classes = "fa fa-xl has-site-branding") {
id = "icon-toggle"
}
}
}
script(type = ScriptType.textJavaScript, src = "/toggle-theme.js") { }
}
}
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/resources/assets/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,19 @@ a.navbar-item:hover {
color: #485fc7;
border-bottom-color: #485fc7;
}

.fa-xl {
font-size: 24px
}

body.dark-theme .tabs li a {
border-bottom-color: #696969;
}

body.dark-theme .tabs li.is-active a {
color: #7585ff;
border-bottom-color: #7585ff;
}
body.dark-theme .text {
fill: #ebecf0
}
60 changes: 60 additions & 0 deletions src/main/resources/assets/js/toggle-theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
let darkMode = localStorage.getItem('darkMode');
const darkModeToggle = document.querySelector('.btn-toggle');

const enableDarkModeOnInit = () => {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.setAttribute('class', 'has-background-dark');
document.getElementById('icon-toggle').classList.add('fa-sun-o');
document.body.classList.add('dark-theme');
localStorage.setItem('darkMode', 'enabled');
}

const enableDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.setAttribute('class', document.documentElement.getAttribute('class').replace('has-background-light', 'has-background-dark'));
document.getElementById('icon-toggle').classList.replace('fa-moon-o','fa-sun-o');
document.body.classList.add('dark-theme');
localStorage.setItem('darkMode', 'enabled');
}

const disableDarkModeOnInit = () => {
document.documentElement.setAttribute('data-theme','light');
document.documentElement.setAttribute('class','has-background-light');
document.getElementById('icon-toggle').classList.add('fa-moon-o');
document.body.classList.remove('dark-theme');
localStorage.setItem('darkMode', 'disabled')
}

const disableDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.setAttribute('class', document.documentElement.getAttribute('class').replace('has-background-dark', 'has-background-light'));
document.getElementById('icon-toggle').classList.replace('fa-sun-o','fa-moon-o');
document.body.classList.remove('dark-theme');
localStorage.setItem('darkMode', 'disabled')
}

(() => {
//Check if the user hasn't visited the site before. If yes, check what the preferred color scheme is.
if (!darkMode) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
enableDarkModeOnInit();
} else {
disableDarkModeOnInit();
}
}

if (darkMode === 'enabled') {
enableDarkModeOnInit();
} else if (darkMode === 'disabled') {
disableDarkModeOnInit();
}

darkModeToggle.addEventListener('click', () => {
darkMode = localStorage.getItem('darkMode');
if (darkMode !== 'enabled') {
enableDarkMode();
} else {
disableDarkMode();
}
});
})();
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,22 @@ class HeaderBarViewModelTest : ViewModelTest() {

assertThat(viewModel.hasLogo).isFalse()
}

@Test
fun `dark mode`() {
val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)

assertThat(viewModel.showDarkModeButton).isTrue()
}

@Test
fun `no dark mode`() {
generatorContext.workspace.views.configuration.addProperty(
"generatr.site.darkMode",
"false"
)
val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)

assertThat(viewModel.showDarkModeButton).isFalse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class CDNTest {
cdn.lunrLanguagesJs("en") to "/min/lunr.en.min.js",
cdn.mermaidJs() to "/dist/mermaid.esm.min.mjs",
cdn.svgpanzoomJs() to "/dist/svg-pan-zoom.min.js",
cdn.webfontloaderJs() to "/webfontloader.js"
cdn.webfontloaderJs() to "/webfontloader.js",
cdn.fontAwesomeCss() to "/css/font-awesome.min.css"
).map { (url, suffix) ->
DynamicTest.dynamicTest(url) {
assertThat(url).all {
Expand Down

0 comments on commit 205ca0e

Please sign in to comment.