diff --git a/README.md b/README.md index 89826bb8..2be91665 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/example/workspace.dsl b/docs/example/workspace.dsl index ce0c94d6..6ad8ea23 100644 --- a/docs/example/workspace.dsl +++ b/docs/example/workspace.dsl @@ -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" { diff --git a/package.json b/package.json index 8faf797b..72e7b0db 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt index 201ef461..c64660be 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt @@ -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) { @@ -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; diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt index 4626bf7e..c50b0f8b 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt @@ -12,6 +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 diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt index 9d79b8af..93564220 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt @@ -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 diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt index 089cca09..94c1f6b3 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt @@ -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('/') diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt index 5b63f6fc..5c2143c0 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt @@ -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) @@ -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)) { } @@ -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() } } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt index c362dfbd..648b162a 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt @@ -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") { } + } + } } } } diff --git a/src/main/resources/assets/css/style.css b/src/main/resources/assets/css/style.css index e50d09d7..a6cda061 100644 --- a/src/main/resources/assets/css/style.css +++ b/src/main/resources/assets/css/style.css @@ -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 +} diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt index 5dab8acb..0d92f65b 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt @@ -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() + } } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt index 4405dca0..c5a58e35 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt @@ -9,11 +9,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory class CDNTest { + val workspace = Workspace("workspace name", "") + val cdn = CDN(workspace) @TestFactory - fun `cdn locations`() { - val workspace = Workspace("workspace name", "") - val cdn = CDN(workspace) - + fun `cdn locations`() : List = listOf( cdn.bulmaCss() to "/css/bulma.min.css", cdn.katexJs() to "/dist/katex.min.js", @@ -23,7 +22,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 { @@ -33,7 +33,6 @@ class CDNTest { } } } - } @Test fun `usage of default cdn url`() {