diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/assetPipeline/AssetPipeline.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/assetPipeline/AssetPipeline.java index 78c47bc36f5..40b5f0db06d 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/assetPipeline/AssetPipeline.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/assetPipeline/AssetPipeline.java @@ -105,6 +105,7 @@ public void apply(GeneratorContext generatorContext) { "grails-app/assets/images/spring-boot.svg", "grails-app/assets/javascripts/application.js", + "grails-app/assets/javascripts/theme.js", "grails-app/assets/javascripts/welcome.js", "grails-app/assets/stylesheets/application.css", diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/theme.js b/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/theme.js new file mode 100644 index 00000000000..dc9a8192866 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/theme.js @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Light / dark / auto color-mode switcher for Bootstrap 5.3's `data-bs-theme`. +// The preference is persisted in localStorage; "auto" follows the operating system. +// This file is loaded in the document so the theme is applied before the +// page paints, avoiding a flash of the wrong theme. +(() => { + 'use strict' + + const STORAGE_KEY = 'theme' + const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches + const storedTheme = () => localStorage.getItem(STORAGE_KEY) + const preferredTheme = () => storedTheme() || 'auto' + const icons = { light: 'bi-sun-fill', dark: 'bi-moon-stars-fill', auto: 'bi-circle-half' } + + const applyTheme = theme => { + const resolved = theme === 'auto' ? (prefersDark() ? 'dark' : 'light') : theme + document.documentElement.setAttribute('data-bs-theme', resolved) + } + + // Apply immediately (before DOMContentLoaded) to avoid a flash of the wrong theme. + applyTheme(preferredTheme()) + + const showActiveTheme = theme => { + document.querySelectorAll('[data-bs-theme-value]').forEach(button => { + const active = button.getAttribute('data-bs-theme-value') === theme + button.classList.toggle('active', active) + button.setAttribute('aria-pressed', String(active)) + const check = button.querySelector('.bi-check') + if (check) { + check.classList.toggle('d-none', !active) + } + }) + document.querySelectorAll('.theme-icon-active').forEach(icon => { + Object.values(icons).forEach(cls => icon.classList.remove(cls)) + icon.classList.add(icons[theme] || icons.auto) + }) + } + + // Re-resolve "auto" when the OS preference changes. + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (preferredTheme() === 'auto') { + applyTheme('auto') + } + }) + + window.addEventListener('DOMContentLoaded', () => { + showActiveTheme(preferredTheme()) + document.querySelectorAll('[data-bs-theme-value]').forEach(button => { + button.addEventListener('click', () => { + const theme = button.getAttribute('data-bs-theme-value') + localStorage.setItem(STORAGE_KEY, theme) + applyTheme(theme) + showActiveTheme(theme) + }) + }) + }) +})() diff --git a/grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp b/grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp index 3581e0e031f..3632764b42f 100644 --- a/grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp +++ b/grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp @@ -216,7 +216,7 @@
-
+
@@ -240,7 +240,7 @@ -
  • +
  • - + ') + } + @Unroll void "test grails-gsp gradle plugins and dependencies are present for #applicationType application"() { when: diff --git a/grails-profiles/web/skeleton/grails-app/assets/javascripts/theme.js b/grails-profiles/web/skeleton/grails-app/assets/javascripts/theme.js new file mode 100644 index 00000000000..dc9a8192866 --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/javascripts/theme.js @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Light / dark / auto color-mode switcher for Bootstrap 5.3's `data-bs-theme`. +// The preference is persisted in localStorage; "auto" follows the operating system. +// This file is loaded in the document so the theme is applied before the +// page paints, avoiding a flash of the wrong theme. +(() => { + 'use strict' + + const STORAGE_KEY = 'theme' + const prefersDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches + const storedTheme = () => localStorage.getItem(STORAGE_KEY) + const preferredTheme = () => storedTheme() || 'auto' + const icons = { light: 'bi-sun-fill', dark: 'bi-moon-stars-fill', auto: 'bi-circle-half' } + + const applyTheme = theme => { + const resolved = theme === 'auto' ? (prefersDark() ? 'dark' : 'light') : theme + document.documentElement.setAttribute('data-bs-theme', resolved) + } + + // Apply immediately (before DOMContentLoaded) to avoid a flash of the wrong theme. + applyTheme(preferredTheme()) + + const showActiveTheme = theme => { + document.querySelectorAll('[data-bs-theme-value]').forEach(button => { + const active = button.getAttribute('data-bs-theme-value') === theme + button.classList.toggle('active', active) + button.setAttribute('aria-pressed', String(active)) + const check = button.querySelector('.bi-check') + if (check) { + check.classList.toggle('d-none', !active) + } + }) + document.querySelectorAll('.theme-icon-active').forEach(icon => { + Object.values(icons).forEach(cls => icon.classList.remove(cls)) + icon.classList.add(icons[theme] || icons.auto) + }) + } + + // Re-resolve "auto" when the OS preference changes. + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (preferredTheme() === 'auto') { + applyTheme('auto') + } + }) + + window.addEventListener('DOMContentLoaded', () => { + showActiveTheme(preferredTheme()) + document.querySelectorAll('[data-bs-theme-value]').forEach(button => { + button.addEventListener('click', () => { + const theme = button.getAttribute('data-bs-theme-value') + localStorage.setItem(STORAGE_KEY, theme) + applyTheme(theme) + showActiveTheme(theme) + }) + }) + }) +})() diff --git a/grails-profiles/web/skeleton/grails-app/views/index.gsp b/grails-profiles/web/skeleton/grails-app/views/index.gsp index 3581e0e031f..3632764b42f 100644 --- a/grails-profiles/web/skeleton/grails-app/views/index.gsp +++ b/grails-profiles/web/skeleton/grails-app/views/index.gsp @@ -216,7 +216,7 @@
    -
    +
    @@ -240,7 +240,7 @@ -
  • +
  • - +