This is an automated email from the ASF dual-hosted git repository. matrei pushed a commit to branch app-welcome-page in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit e20c91bef6eb1956238cada650f1e18b5865dbaf Author: Mattias Reichel <[email protected]> AuthorDate: Thu Jan 15 12:43:40 2026 +0100 fix: improve new app welcome page --- .../resources/assets/images/advancedgrails.svg | 6 +- .../main/resources/assets/images/documentation.svg | 4 +- .../src/main/resources/assets/images/favicon.ico | Bin 5558 -> 27198 bytes .../src/main/resources/assets/images/groovy.svg | 1 + .../src/main/resources/assets/images/java.svg | 1 + .../main/resources/assets/images/spring-boot.svg | 20 ++ .../src/main/resources/assets/images/spring.svg | 1 + .../main/resources/assets/javascripts/welcome.js | 116 ++++++ .../main/resources/assets/stylesheets/welcome.css | 63 ++++ .../src/main/resources/gsp/index.gsp | 397 +++++++++++++++++---- .../src/main/resources/gsp/main.gsp | 113 +++--- .../grails-app/assets/images/advancedgrails.svg | 6 +- .../grails-app/assets/images/documentation.svg | 4 +- .../skeleton/grails-app/assets/images/favicon.ico | Bin 5558 -> 27198 bytes .../skeleton/grails-app/assets/images/groovy.svg | 1 + .../web/skeleton/grails-app/assets/images/java.svg | 1 + .../grails-app/assets/images/spring-boot.svg | 20 ++ .../skeleton/grails-app/assets/images/spring.svg | 1 + .../grails-app/assets/javascripts/welcome.js | 115 ++++++ .../grails-app/assets/stylesheets/welcome.css | 63 ++++ .../web/skeleton/grails-app/views/index.gsp | 397 +++++++++++++++++---- .../web/skeleton/grails-app/views/layouts/main.gsp | 113 +++--- 22 files changed, 1187 insertions(+), 256 deletions(-) diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/advancedgrails.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/advancedgrails.svg index 8b63ec8be5..9036e3cf5f 100644 --- a/grails-forge/grails-forge-core/src/main/resources/assets/images/advancedgrails.svg +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/advancedgrails.svg @@ -5,14 +5,14 @@ width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve"> <g> <g> - <circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.79" cy="46.789" r="45.374"/> + <circle fill="#FEB672" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.79" cy="46.789" r="45.374"/> </g> <g> - <path fill="#FEB672" d="M71.126,29.576c0,0.414-0.337,0.75-0.75,0.75h-3.25v3.25c0,0.415-0.337,0.751-0.751,0.751h-1.499 + <path fill="white" d="M71.126,29.576c0,0.414-0.337,0.75-0.75,0.75h-3.25v3.25c0,0.415-0.337,0.751-0.751,0.751h-1.499 c-0.415,0-0.75-0.336-0.75-0.751v-3.25h-3.251c-0.414,0-0.749-0.336-0.749-0.75v-1.498c0-0.416,0.335-0.752,0.749-0.752h3.251 v-3.249c0-0.414,0.335-0.75,0.75-0.75h1.499c0.414,0,0.751,0.336,0.751,0.75v3.249h3.25c0.413,0,0.75,0.336,0.75,0.752V29.576z"/> </g> - <path fill="#FEB672" d="M50.42,60.386c0.554,1.467,0.855,1.951,1.493,3.44c0.271,0.627,0.523,1.228,0.649,1.518 + <path fill="white" d="M50.42,60.386c0.554,1.467,0.855,1.951,1.493,3.44c0.271,0.627,0.523,1.228,0.649,1.518 c0.049,0.117,0.036,0.248-0.033,0.355c-0.172,0.259-0.552,0.747-1.181,1.086c-1.098,0.594-3.409,0.809-4.555,0.812h-0.006 c-1.146-0.004-3.457-0.219-4.558-0.812c-0.627-0.339-1.006-0.827-1.177-1.086c-0.07-0.107-0.083-0.238-0.032-0.355 c0.123-0.29,0.376-0.891,0.646-1.518c0.64-1.489,0.941-1.974,1.495-3.44c0.485-1.294,0.729-3.175,0.745-4.593 diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/documentation.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/documentation.svg index 29bc9d57d3..78ff4dc4b9 100644 --- a/grails-forge/grails-forge-core/src/main/resources/assets/images/documentation.svg +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/documentation.svg @@ -5,9 +5,9 @@ width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve"> <g> <g> - <circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.88" cy="46.792" r="45.374"/> + <circle fill="#FEB672" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.88" cy="46.792" r="45.374"/> </g> - <path fill="#FEB672" d="M64.379,40.958v24.062c0,1.208-0.979,2.188-2.188,2.188H31.567c-1.208,0-2.188-0.979-2.188-2.188V28.562 + <path fill="white" d="M64.379,40.958v24.062c0,1.208-0.979,2.188-2.188,2.188H31.567c-1.208,0-2.188-0.979-2.188-2.188V28.562 c0-1.208,0.98-2.188,2.188-2.188h18.229v12.396c0,1.208,0.979,2.188,2.188,2.188H64.379z M55.629,44.604 c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729v1.458c0,0.41,0.319,0.729,0.729,0.729H54.9 c0.41,0,0.729-0.319,0.729-0.729V44.604z M55.629,50.438c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729 diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/favicon.ico b/grails-forge/grails-forge-core/src/main/resources/assets/images/favicon.ico index 76e4b11fed..6e0e0ecff9 100644 Binary files a/grails-forge/grails-forge-core/src/main/resources/assets/images/favicon.ico and b/grails-forge/grails-forge-core/src/main/resources/assets/images/favicon.ico differ diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/groovy.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/groovy.svg new file mode 100644 index 0000000000..4bb39ed848 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/groovy.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><path d="M103.555 95.851L64 80.52 24.446 95.847l15.618-24.436L0 56.447l49.208.23L63.999 32.1l14.794 24.578L128 56.453l-40.065 14.96 15.62 24.438"/><path d="M98.204 91.48l-34.17-13.244-34.168 13.242 13.491-21.11-34.61-12.926 42.506.198L64.03 36.41l12.78 21.23 42.509-.194L84.71 70.37l13.493 21.11" fill="#619cbc"/><path d="M37.804 44.66c3.413 0 1.424 8.528.21 11.1-1.04 2.201-3.38 5.377-6.153 5.377-3.327 0-3.228-3.727-1.904-6.532. [...] \ No newline at end of file diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/java.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/java.svg new file mode 100644 index 0000000000..051bf254ad --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/java.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#0074BD" d="M47.617 98.12s-4.767 2.774 3.397 3.71c9.892 1.13 14.947.968 25.845-1.092 0 0 2.871 1.795 6.873 3.351-24.439 10.47-55.308-.607-36.115-5.969zm-2.988-13.665s-5.348 3.959 2.823 4.805c10.567 1.091 18.91 1.18 33.354-1.6 0 0 1.993 2.025 5.132 3.131-29.542 8.64-62.446.68-41.309-6.336z"/><path fill="#EA2D2E" d="M69.802 61.271c6.025 6.935-1.58 13.17-1.58 13.17s15.289-7.891 8.269-17.777c-6.559-9.215-11.587-13.792 [...] \ No newline at end of file diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/spring-boot.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/spring-boot.svg new file mode 100644 index 0000000000..d84ae1cf9f --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/spring-boot.svg @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 510 457.8" style="enable-background:new 0 0 510 457.8;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#6DB33F;} +</style> +<title>icon-spring-boot</title> +<g id="Layer_2_1_"> + <g id="Layer_1-2"> + <path class="st0" d="M503.5,201.4L403,27.5C394.3,12.4,372.9,0,355.4,0H154.6c-17.4,0-38.9,12.4-47.6,27.5L6.6,201.4 + c-8.7,15.1-8.7,39.8,0,54.9l100.4,174c8.7,15.1,30.1,27.5,47.6,27.5h200.9c17.4,0,38.8-12.4,47.6-27.5l100.4-174 + C512.2,241.2,512.2,216.5,503.5,201.4z M233.3,96.2c0-11.4,9.3-20.7,20.7-20.7c11.4,0,20.7,9.3,20.7,20.7v123.7 + c0,11.4-9.3,20.7-20.7,20.7c-11.4,0-20.7-9.3-20.7-20.7l0,0V96.2z M254,360.3c-77.4,0-140.4-63-140.4-140.4 + c0.1-44.4,21.1-86.1,56.7-112.7c8.2-6.1,19.7-4.4,25.8,3.8s4.4,19.7-3.8,25.8l0,0c-45.9,34.1-55.5,99-21.4,144.9 + s99,55.5,144.9,21.4c26.3-19.5,41.8-50.4,41.8-83.2c-0.1-32.9-15.7-63.8-42.2-83.4c-8.2-6-9.9-17.6-3.9-25.8s17.6-9.9,25.8-3.9 + c35.9,26.5,57,68.5,57.1,113.1C394.4,297.4,331.4,360.3,254,360.3z"/> + </g> +</g> +</svg> diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/images/spring.svg b/grails-forge/grails-forge-core/src/main/resources/assets/images/spring.svg new file mode 100644 index 0000000000..93d6bde281 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/images/spring.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 97.1 97"><defs><style>.cls-1{fill:#6db33f;}</style></defs><title>spring-icon</title><g id="Layer_2" data-name="Layer 2"><g id="logos"><path class="cls-1" d="M88.4,5.6a42.32,42.32,0,0,1-5.2,9.1A48.46,48.46,0,1,0,15.5,84l1.8,1.6A48.41,48.41,0,0,0,96.8,52C98.2,39.8,94.5,24.2,88.4,5.6ZM22.5,84.4a4.12,4.12,0,1,1-.6-5.8A4.21,4.21,0,0,1,22.5,84.4ZM88.1,69.9C76.2,85.8,50.6,80.4,34.3,81.2c0,0-2.9.2-5.8.6,0,0,1.1-.5,2.5-1,11.5-4,16.9-4.8,23.9-8. [...] \ No newline at end of file diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/welcome.js b/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/welcome.js new file mode 100644 index 0000000000..5b240fcb93 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/javascripts/welcome.js @@ -0,0 +1,116 @@ +(function () { + function compare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; + } + + function parseVersionParts(v) { + const cleaned = String(v || '').trim().replace(/[^0-9.]+/g, '.'); + if (!cleaned) return []; + return cleaned.split('.').filter(Boolean).map((s) => Number(s) || 0); + } + + function compareVersions(a, b) { + const pa = parseVersionParts(a); + const pb = parseVersionParts(b); + const n = Math.max(pa.length, pb.length); + for (let i = 0; i < n; i++) { + const da = pa[i] ?? 0; + const db = pb[i] ?? 0; + if (da !== db) return da < db ? -1 : 1; + } + const sa = String(a || '').toLowerCase(); + const sb = String(b || '').toLowerCase(); + return compare(sa, sb); + } + + function getValue(tr, key) { + if (key === 'name') return (tr.dataset.name || '').toLowerCase(); + if (key === 'version') return (tr.dataset.version || ''); + if (key === 'order') return Number(tr.dataset.order || '0'); + return ''; + } + + function sortTableBy(table, key, dir) { + const tbody = table.tBodies[0]; + if (!tbody) return; + + const rows = Array.from(tbody.querySelectorAll('tr')); + rows.sort((ra, rb) => { + if (key === 'version') { + return compareVersions(getValue(ra, key), getValue(rb, key)) * dir; + } + return compare(getValue(ra, key), getValue(rb, key)) * dir; + }); + + const frag = document.createDocumentFragment(); + rows.forEach((r) => frag.appendChild(r)); + tbody.appendChild(frag); + } + + function setSortIndicator(table, activeTh, ariaSortValue, dataSortDirValue) { + table.querySelectorAll('th.sortable, th[data-sort-key]').forEach((th) => { + if (th !== activeTh) th.removeAttribute('data-sort-dir'); + }); + + if (dataSortDirValue) { + activeTh.setAttribute('data-sort-dir', dataSortDirValue); + } else { + activeTh.removeAttribute('data-sort-dir'); + } + + table.querySelectorAll('th.sortable, th[data-sort-key]').forEach((th) => { + th.removeAttribute('aria-sort'); + }); + activeTh.setAttribute('aria-sort', ariaSortValue); + } + + function initSortableTable(table) { + const headers = Array.from( + table.querySelectorAll('th.sortable[data-sort-key], th[data-sort-key]') + ); + const state = { key: null, dir: 1 }; + + function activateSort(th) { + const key = th.getAttribute('data-sort-key'); + if (!key) return; + + state.dir = state.key === key ? state.dir * -1 : 1; + state.key = key; + + const ariaSort = state.dir === 1 ? 'ascending' : 'descending'; + const dataSortDir = state.dir === 1 ? 'asc' : 'desc'; + + setSortIndicator(table, th, ariaSort, dataSortDir); + sortTableBy(table, key, state.dir); + } + + headers.forEach((th) => { + th.style.cursor = 'pointer'; + + th.addEventListener('click', () => activateSort(th)); + + // Keyboard support: Enter / Space should sort like a click. + th.addEventListener('keydown', (e) => { + const k = e.key; + if (k === 'Enter' || k === ' ' || k === 'Spacebar') { + e.preventDefault(); + activateSort(th); + } + }); + }); + + // Initial load: show ascending indicator on Name (and sort to match). + const defaultKey = 'name'; + const defaultTh = headers.find((th) => (th.getAttribute('data-sort-key') || '') === defaultKey); + if (defaultTh) { + state.key = defaultKey; + state.dir = 1; + setSortIndicator(table, defaultTh, 'ascending', 'asc'); + sortTableBy(table, defaultKey, state.dir); + } + } + + document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('table[data-sortable]').forEach(initSortableTable); + }); +})(); \ No newline at end of file diff --git a/grails-forge/grails-forge-core/src/main/resources/assets/stylesheets/welcome.css b/grails-forge/grails-forge-core/src/main/resources/assets/stylesheets/welcome.css new file mode 100644 index 0000000000..ba0a393921 --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/resources/assets/stylesheets/welcome.css @@ -0,0 +1,63 @@ +.reload-indicator { + display: inline-flex; + align-items: center; + gap: .5rem; + padding: .35rem .6rem; + border: 1px solid var(--bs-border-color); + border-radius: 999px; + background: var(--bs-body-bg); + font-size: .875rem; + line-height: 1; + white-space: nowrap; +} + +.reload-dot { + position: relative; + width: .6rem; + height: .6rem; + border-radius: 999px; + background: currentColor; + flex: 0 0 auto; +} + +.reload-dot.ping::before { + content: ""; + position: absolute; + inset: 0; + border-radius: 999px; + background: currentColor; + opacity: .35; + animation: reload-ping 1.25s ease-out infinite; +} + +.sortable { + cursor: pointer; + user-select: none; +} + +.sort-hint { + display: inline-block; + width: .9em; +} + +.sort-hint::before { + content: ''; +} + +th.sortable[data-sort-dir="asc"] .sort-hint::before { + content: '↑'; +} + +th.sortable[data-sort-dir="desc"] .sort-hint::before { + content: '↓'; +} + +@keyframes reload-ping { + 0% { transform: scale(1); opacity: .35; } + 70% { transform: scale(2.25); opacity: 0; } + 100% { transform: scale(2.25); opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .reload-dot::before { animation: none; } +} 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 c7544050a7..3581e0e031 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 @@ -1,84 +1,343 @@ -<%@ page import="grails.util.Environment; org.springframework.core.SpringVersion; org.springframework.boot.SpringBootVersion" -%><!doctype html> +<%@ page import="grails.util.Environment"%> +<%@ page import="org.springframework.boot.SpringBootVersion"%> +<%@ page import="org.springframework.core.SpringVersion"%> +<g:set var="pluginManager" bean="pluginManager"/> +<g:set var="servletContext" bean="servletContext"/> +<g:set var="pluginsWithOrder" + value="${pluginManager.allPlugins.toList() + .withIndex() + .collect { p, i -> [plugin: p, order: i + 1] } + .sort { a, b -> a.plugin.name.toLowerCase() <=> b.plugin.name.toLowerCase() }}" +/> +<g:set var="numControllers" value="${grailsApplication.controllerClasses.size()}"/> +<!doctype html> <html> <head> - <meta name="layout" content="main"/> <title>Welcome to Grails</title> + <meta name="layout" content="main"/> + <asset:stylesheet src="welcome.css"/> </head> <body> -<content tag="nav"> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Application Status <span class="caret"></span></a> - <ul class="dropdown-menu"> - <li><a class="dropdown-item" href="#">Server: ${request.getServletContext().getServerInfo()}</a></li> - <li><a class="dropdown-item" href="#">Host: ${InetAddress.getLocalHost()}</a></li> - <li><a class="dropdown-item" href="#">Environment: ${Environment.current.name}</a></li> - <li><a class="dropdown-item" href="#">App version: - <g:meta name="info.app.version"/></a> - </li> - <li><a class="dropdown-item" href="#">App profile: ${grailsApplication.config.getProperty('grails.profile')}</a></li> - <li><hr class="dropdown-divider"></li> - <li><a class="dropdown-item" href="#">Grails version: - <g:meta name="info.app.grailsVersion"/></a> - </li> - <li><a class="dropdown-item" href="#">Groovy version: ${GroovySystem.getVersion()}</a></li> - <li><a class="dropdown-item" href="#">JVM version: ${System.getProperty('java.version')}</a></li> - <li><a class="dropdown-item" href="#">Spring Boot version: ${SpringBootVersion.getVersion()}</a></li> - <li><a class="dropdown-item" href="#">Spring version: ${SpringVersion.getVersion()}</a></li> - <li><hr class="dropdown-divider"></li> - <li><a class="dropdown-item" href="#">Reloading active: ${Environment.reloadingAgentEnabled}</a></li> - </ul> - </li> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Artefacts <span class="caret"></span></a> - <ul class="dropdown-menu"> - <li><a class="dropdown-item" href="#">Controllers: ${grailsApplication.controllerClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Domains: ${grailsApplication.domainClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Services: ${grailsApplication.serviceClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Tag Libraries: ${grailsApplication.tagLibClasses.size()}</a></li> - </ul> - </li> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Installed Plugins<span class="caret"></span></a> - <ul class="dropdown-menu dropdown-menu-right"> - <g:each var="plugin" in="${applicationContext.getBean('pluginManager').allPlugins}"> - <li><a class="dropdown-item" href="#">${plugin.name} - ${plugin.version}</a></li> - </g:each> - </ul> - </li> -</content> +<main id="content" role="main" class="pb-4 pb-md-5"> + <div class="container-lg py-2 py-md-3"> + <div class="row align-items-top g-4"> -<div class="svg" role="presentation"> - <div class="bg-dark-subtle text-center"> - <asset:image src="grails-cupsonly-logo-white.svg" class="w-50"/> + <%-- WELCOME MESSAGE --%> + <div class="col-12 col-md-7"> + <h1 class="display-6 fw-semibold mb-2">Welcome to Grails</h1> + <p class="lead text-body-secondary"> + Congratulations, you have successfully started a Grails application. + </p> + <p class="text-body-secondary"> + At the moment this is the default page, feel free to modify it to either + redirect to a controller or display whatever content you may choose. + </p> + </div> + + <%-- RUNTIME VERSIONS --%> + <div class="col-12 col-md-5"> + <div class="card border-1 shadow-sm"> + <div class="card-body"> + <h6 class="card-title mb-3 fw-semibold">Runtime versions</h6> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="grails.svg" alt="Grails" width="18" height="18" class="me-2"/> + Grails + </span> + <g:meta name="info.app.grailsVersion"/> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="spring-boot.svg" alt="Spring Boot" width="18" height="18" class="me-2"/> + Spring Boot + </span> + ${SpringBootVersion.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="spring.svg" alt="Spring" width="18" height="18" class="me-2"/> + Spring + </span> + ${SpringVersion.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="groovy.svg" alt="Groovy" width="18" height="18" class="me-2"/> + Groovy + </span> + ${GroovySystem.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="java.svg" alt="Java" width="18" height="18" class="me-2"/> + JVM (${System.getProperty('java.vendor')}) + </span> + ${System.getProperty('java.version')} + </li> + </ul> + </div> + </div> + </div> + </div> </div> -</div> + <div class="container-lg"> + <div class="row g-4 align-items-stretch"> -<div id="content" role="main"> - <div class="container"> - <section class="row colset-2-its"> - <h1>Welcome to Grails</h1> + <%-- APPLICATION INFO --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Application</h6> + <g:if test="${Environment.reloadingAgentEnabled}"> + <span class="reload-indicator text-success" role="status" aria-label="Reloading active"> + <span class="reload-dot ping" aria-hidden="true"></span> + <span class="text-body-secondary">Reloading active</span> + </span> + </g:if> + <g:else> + <span class="reload-indicator text-danger" role="status" aria-label="Reloading inactive"> + <span class="reload-dot" aria-hidden="true"></span> + <span class="text-body-secondary">Reloading inactive</span> + </span> + </g:else> + </div> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Name</span> + <span class="fw-medium text-truncate ms-3"><g:meta name="info.app.name"/></span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Version</span> + <span class="fw-medium" style="font-variant-numeric: tabular-nums;"> + <g:meta name="info.app.version"/> + </span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Profile</span> + <span class="fw-medium text-truncate ms-3"> + ${grailsApplication.config.getProperty('grails.profile')} + </span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Environment</span> + <span class="fw-medium">${Environment.current.name}</span> + </li> + </ul> + </div> + </div> + </div> + + <%-- SERVER INFO --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Server</h6> + </div> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Servlet Container</span> + <span class="fw-medium text-truncate ms-3">${servletContext.serverInfo}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Host</span> + <span class="fw-medium text-truncate ms-3">${InetAddress.localHost}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">OS</span> + <span class="fw-medium text-truncate ms-3"> + ${System.getProperty('os.name')} ${System.getProperty('os.version')} (${System.getProperty('os.arch')}) + </span> + </li> + </ul> + </div> + </div> + </div> - <p> - Congratulations, you have successfully started your first Grails application! At the moment - this is the default page, feel free to modify it to either redirect to a controller or display - whatever content you may choose. Below is a list of controllers that are currently deployed in - this application, click on each to execute its default action: - </p> + <%-- ARTEFACT COUNTS --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Artefact counts</h6> + </div> - <div id="controllers" role="navigation"> - <h2>Available Controllers:</h2> - <ul> - <g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }"> - <li class="controller"> - <g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link> - </li> - </g:each> - </ul> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Controllers</span> + <span class="fw-medium">${numControllers}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Domains</span> + <span class="fw-medium">${grailsApplication.domainClasses.size()}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Services</span> + <span class="fw-medium">${grailsApplication.serviceClasses.size()}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Tag Libraries</span> + <span class="fw-medium">${grailsApplication.tagLibClasses.size()}</span> + </li> + </ul> + </div> + </div> </div> - </section> + + </div> </div> -</div> + <%-- AVAILABLE CONTROLLERS --%> + <div class="container-lg mt-4"> + <div class="row g-4 align-items-start"> + <div class="col-12 col-lg-7"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body p-4 p-md-5"> + <div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2"> + <div> + <h2 class="h4 mb-1">Available Controllers</h2> + <p class="text-body-secondary mb-0"> + ${numControllers} controller${numControllers != 1 ? 's' : ''} detected. + </p> + </div> + <g:if test="${numControllers != 0}"> + <div class="small text-body-secondary"> + Click a controller to execute its default action. + </div> + </g:if> + </div> + <hr class="my-3 my-md-4 text-body-tertiary"/> + <g:set var="controllersByNamespace" + value="${grailsApplication.controllerClasses + .groupBy { cc -> ((cc.namespace ?: '').trim()) ?: 'default' } + .sort { a, b -> a.key.toString().toLowerCase() <=> b.key.toString().toLowerCase() }}"/> + + <g:each var="nsEntry" in="${controllersByNamespace}" status="nsIndex"> + <div class="${nsIndex > 0 ? 'mt-4' : ''}"> + <div class="px-0 py-2 bg-body-tertiary"> + <div class="d-flex align-items-center justify-content-between"> + <div class="small text-uppercase text-body-secondary fw-semibold" + style="letter-spacing: .04em;"> + <g:if test="${nsEntry.key != 'default'}"> + ${nsEntry.key} + </g:if> + <g:else> + Default namespace + </g:else> + </div> + </div> + </div> + + <ul class="list-group list-group-flush"> + <g:each var="c" in="${nsEntry.value.sort { it.fullName }}"> + <g:set var="simpleName" value="${(c.fullName ?: '') + .tokenize('.') + .last() + .replaceFirst(/Controller$/, '')}"/> + + <g:set var="controllerUrl" + value="${createLink(controller: c.logicalPropertyName, namespace: c.namespace)}"/> + + <li class="list-group-item px-0"> + <div class="d-flex align-items-center justify-content-between gap-3"> + <g:link controller="${c.logicalPropertyName}" + namespace="${c.namespace}" + class="d-flex align-items-center gap-3 text-decoration-none min-w-0 flex-grow-1"> + <div class="min-w-0"> + <div class="fw-semibold text-body text-truncate"> + ${simpleName} + </div> + </div> + </g:link> + + <a href="${controllerUrl}" + class="small link-primary link-offset-2 link-underline-opacity-0 link-underline-opacity-75-hover flex-shrink-0"> + ${controllerUrl} + </a> + </div> + </li> + </g:each> + </ul> + </div> + </g:each> + </div> + </div> + </div> + + <%-- PLUGINS --%> + <div class="col-12 col-lg-5"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Installed plugins</h6> + <span class="badge text-bg-light border"> + ${pluginManager.allPlugins.size()} + </span> + </div> + + <div class="table-responsive"> + <table class="table table-sm table-striped table-hover" data-sortable="true"> + <thead class="table-light small"> + <tr> + <th scope="col" + class="text-body-secondary ps-0 fw-semibold sortable" + data-sort-key="name" + role="button" + tabindex="0" + aria-label="Sort by name"> + Name <span class="sort-hint" aria-hidden="true"></span> + </th> + <th scope="col" + class="text-body-secondary ps-0 fw-semibold text-end sortable" + data-sort-key="version" + role="button" + tabindex="0" + aria-label="Sort by version"> + <span class="sort-hint" aria-hidden="true"></span> Version + </th> + <th scope="col" + class="text-body-secondary text-end pe-0 sortable" + data-sort-key="order" + role="button" + tabindex="0" + aria-label="Sort by load order"> + <span class="sort-hint" aria-hidden="true"></span> Load order + </th> + </tr> + </thead> + <tbody class="small"> + <g:each var="row" in="${pluginsWithOrder}"> + <g:set var="pluginName" + value="${row.plugin.name + .replaceAll(/([A-Z]+)([A-Z][a-z])/, '$1 $2') + .replaceAll(/([a-z0-9])([A-Z])/, '$1 $2') + .replaceAll(/[_-]+/, ' ') + .trim() + .capitalize()}" + /> + <tr data-name="${pluginName}" data-version="${row.plugin.version}" data-order="${row.order}"> + <td class="text-truncate"> + ${pluginName} + </td> + <td class="text-end" style="font-variant-numeric: tabular-nums;"> + ${row.plugin.version} + </td> + <td class="text-end text-body-secondary" style="font-variant-numeric: tabular-nums;"> + ${row.order} + </td> + </tr> + </g:each> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + </div> +</main> +<asset:javascript src="welcome.js"/> </body> </html> diff --git a/grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp b/grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp index 6f09bcc995..023449b955 100644 --- a/grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp +++ b/grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp @@ -1,12 +1,9 @@ <!doctype html> -<html lang="en" class="no-js"> +<html lang="en"> <head> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <meta http-equiv="X-UA-Compatible" content="IE=edge"/> - <title> - <g:layoutTitle default="Grails"/> - </title> + <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title><g:layoutTitle default="Grails"/></title> <asset:link rel="icon" href="favicon.ico" type="image/x-ico"/> <asset:stylesheet src="application.css"/> <g:layoutHead/> @@ -14,68 +11,76 @@ <body> -<nav class="navbar navbar-expand-lg bg-body-tertiary"> - <div class="container-fluid"> - <a class="navbar-brand" href="/#"><asset:image class="w-75" src="grails.svg" alt="Grails Logo"/></a> - <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> - - <div class="collapse navbar-collapse" aria-expanded="false" id="navbarContent"> - <ul class="navbar-nav"> - <g:pageProperty name="page.nav"/> - </ul> - </div> +<nav class="navbar navbar-expand-lg bg-body border-bottom shadow-sm"> + <div class="container-lg"> + <a class="navbar-brand d-flex align-items-center" href="${request.contextPath}/"> + <asset:image class="w-75" src="grails.svg" alt="Grails Logo"/> + </a> </div> </nav> -<g:layoutBody/> +<div class="bg-body-tertiary"> + <div class="container-lg py-4"> + <g:layoutBody/> + </div> +</div> -<div class="footer" role="contentinfo"> - <div class="container-fluid"> - <div class="row"> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://guides.grails.org" target="_blank"> - <asset:image src="advancedgrails.svg" alt="Grails Guides" class="me-2" width="34" />Grails Guides - </a> - </h6> - <p class="card-text">Building your first Grails app? Looking to add security, or create a Single-Page-App? Check out the <a href="https://guides.grails.org" target="_blank">Grails Guides</a> for step-by-step tutorials.</p> - </div> +<footer class="border-top py-5" role="contentinfo"> + <div class="container-lg"> + <div class="row g-4"> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://guides.grails.org" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Grails Guides</h6> + <asset:image src="advancedgrails.svg" alt="Grails Guides" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Building your first Grails app? Looking to add security, or create a Single-Page-App? + Check out the Grails Guides for step-by-step tutorials. + </p> + </div> + </a> </div> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://grails.apache.org/docs/" target="_blank"> - <asset:image src="documentation.svg" alt="Grails Documentation" class="me-2" width="34" />Documentation - </a> - </h6> - <p class="card-text">Ready to dig in? You can find in-depth documentation for all the features of Grails in the <a href="https://grails.apache.org/docs/" target="_blank">User Guide</a>.</p> - </div> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://grails.apache.org/docs/" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Documentation</h6> + <asset:image src="documentation.svg" alt="Grails Documentation" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Ready to dig in? You can find in-depth documentation for all the features + of Grails in the User Guide. + </p> + </div> + </a> </div> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://slack.grails.org" target="_blank"> - <asset:image src="slack.svg" alt="Grails Slack" class="me-2" width="34" />Join the Community - </a> - </h6> - <p class="card-text">Get feedback and share your experience with other Grails developers in the community <a href="https://slack.grails.org" target="_blank">Slack channel</a>.</p> - </div> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://slack.grails.org" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Join the Community</h6> + <asset:image src="slack.svg" alt="Grails Slack" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Get feedback and share your experience with other Grails developers + in the community Slack channel. + </p> + </div> + </a> </div> </div> </div> -</div> - +</footer> <div id="spinner" class="position-absolute top-0 end-0 p-1" style="display:none;"> <div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> - - <asset:javascript src="application.js"/> - </body> </html> diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/advancedgrails.svg b/grails-profiles/web/skeleton/grails-app/assets/images/advancedgrails.svg index 8b63ec8be5..9036e3cf5f 100644 --- a/grails-profiles/web/skeleton/grails-app/assets/images/advancedgrails.svg +++ b/grails-profiles/web/skeleton/grails-app/assets/images/advancedgrails.svg @@ -5,14 +5,14 @@ width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve"> <g> <g> - <circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.79" cy="46.789" r="45.374"/> + <circle fill="#FEB672" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.79" cy="46.789" r="45.374"/> </g> <g> - <path fill="#FEB672" d="M71.126,29.576c0,0.414-0.337,0.75-0.75,0.75h-3.25v3.25c0,0.415-0.337,0.751-0.751,0.751h-1.499 + <path fill="white" d="M71.126,29.576c0,0.414-0.337,0.75-0.75,0.75h-3.25v3.25c0,0.415-0.337,0.751-0.751,0.751h-1.499 c-0.415,0-0.75-0.336-0.75-0.751v-3.25h-3.251c-0.414,0-0.749-0.336-0.749-0.75v-1.498c0-0.416,0.335-0.752,0.749-0.752h3.251 v-3.249c0-0.414,0.335-0.75,0.75-0.75h1.499c0.414,0,0.751,0.336,0.751,0.75v3.249h3.25c0.413,0,0.75,0.336,0.75,0.752V29.576z"/> </g> - <path fill="#FEB672" d="M50.42,60.386c0.554,1.467,0.855,1.951,1.493,3.44c0.271,0.627,0.523,1.228,0.649,1.518 + <path fill="white" d="M50.42,60.386c0.554,1.467,0.855,1.951,1.493,3.44c0.271,0.627,0.523,1.228,0.649,1.518 c0.049,0.117,0.036,0.248-0.033,0.355c-0.172,0.259-0.552,0.747-1.181,1.086c-1.098,0.594-3.409,0.809-4.555,0.812h-0.006 c-1.146-0.004-3.457-0.219-4.558-0.812c-0.627-0.339-1.006-0.827-1.177-1.086c-0.07-0.107-0.083-0.238-0.032-0.355 c0.123-0.29,0.376-0.891,0.646-1.518c0.64-1.489,0.941-1.974,1.495-3.44c0.485-1.294,0.729-3.175,0.745-4.593 diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/documentation.svg b/grails-profiles/web/skeleton/grails-app/assets/images/documentation.svg index 29bc9d57d3..78ff4dc4b9 100644 --- a/grails-profiles/web/skeleton/grails-app/assets/images/documentation.svg +++ b/grails-profiles/web/skeleton/grails-app/assets/images/documentation.svg @@ -5,9 +5,9 @@ width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve"> <g> <g> - <circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.88" cy="46.792" r="45.374"/> + <circle fill="#FEB672" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.88" cy="46.792" r="45.374"/> </g> - <path fill="#FEB672" d="M64.379,40.958v24.062c0,1.208-0.979,2.188-2.188,2.188H31.567c-1.208,0-2.188-0.979-2.188-2.188V28.562 + <path fill="white" d="M64.379,40.958v24.062c0,1.208-0.979,2.188-2.188,2.188H31.567c-1.208,0-2.188-0.979-2.188-2.188V28.562 c0-1.208,0.98-2.188,2.188-2.188h18.229v12.396c0,1.208,0.979,2.188,2.188,2.188H64.379z M55.629,44.604 c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729v1.458c0,0.41,0.319,0.729,0.729,0.729H54.9 c0.41,0,0.729-0.319,0.729-0.729V44.604z M55.629,50.438c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729 diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/favicon.ico b/grails-profiles/web/skeleton/grails-app/assets/images/favicon.ico index 76e4b11fed..6e0e0ecff9 100644 Binary files a/grails-profiles/web/skeleton/grails-app/assets/images/favicon.ico and b/grails-profiles/web/skeleton/grails-app/assets/images/favicon.ico differ diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/groovy.svg b/grails-profiles/web/skeleton/grails-app/assets/images/groovy.svg new file mode 100644 index 0000000000..4bb39ed848 --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/images/groovy.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><path d="M103.555 95.851L64 80.52 24.446 95.847l15.618-24.436L0 56.447l49.208.23L63.999 32.1l14.794 24.578L128 56.453l-40.065 14.96 15.62 24.438"/><path d="M98.204 91.48l-34.17-13.244-34.168 13.242 13.491-21.11-34.61-12.926 42.506.198L64.03 36.41l12.78 21.23 42.509-.194L84.71 70.37l13.493 21.11" fill="#619cbc"/><path d="M37.804 44.66c3.413 0 1.424 8.528.21 11.1-1.04 2.201-3.38 5.377-6.153 5.377-3.327 0-3.228-3.727-1.904-6.532. [...] \ No newline at end of file diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/java.svg b/grails-profiles/web/skeleton/grails-app/assets/images/java.svg new file mode 100644 index 0000000000..051bf254ad --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/images/java.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#0074BD" d="M47.617 98.12s-4.767 2.774 3.397 3.71c9.892 1.13 14.947.968 25.845-1.092 0 0 2.871 1.795 6.873 3.351-24.439 10.47-55.308-.607-36.115-5.969zm-2.988-13.665s-5.348 3.959 2.823 4.805c10.567 1.091 18.91 1.18 33.354-1.6 0 0 1.993 2.025 5.132 3.131-29.542 8.64-62.446.68-41.309-6.336z"/><path fill="#EA2D2E" d="M69.802 61.271c6.025 6.935-1.58 13.17-1.58 13.17s15.289-7.891 8.269-17.777c-6.559-9.215-11.587-13.792 [...] \ No newline at end of file diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/spring-boot.svg b/grails-profiles/web/skeleton/grails-app/assets/images/spring-boot.svg new file mode 100644 index 0000000000..d84ae1cf9f --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/images/spring-boot.svg @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 510 457.8" style="enable-background:new 0 0 510 457.8;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#6DB33F;} +</style> +<title>icon-spring-boot</title> +<g id="Layer_2_1_"> + <g id="Layer_1-2"> + <path class="st0" d="M503.5,201.4L403,27.5C394.3,12.4,372.9,0,355.4,0H154.6c-17.4,0-38.9,12.4-47.6,27.5L6.6,201.4 + c-8.7,15.1-8.7,39.8,0,54.9l100.4,174c8.7,15.1,30.1,27.5,47.6,27.5h200.9c17.4,0,38.8-12.4,47.6-27.5l100.4-174 + C512.2,241.2,512.2,216.5,503.5,201.4z M233.3,96.2c0-11.4,9.3-20.7,20.7-20.7c11.4,0,20.7,9.3,20.7,20.7v123.7 + c0,11.4-9.3,20.7-20.7,20.7c-11.4,0-20.7-9.3-20.7-20.7l0,0V96.2z M254,360.3c-77.4,0-140.4-63-140.4-140.4 + c0.1-44.4,21.1-86.1,56.7-112.7c8.2-6.1,19.7-4.4,25.8,3.8s4.4,19.7-3.8,25.8l0,0c-45.9,34.1-55.5,99-21.4,144.9 + s99,55.5,144.9,21.4c26.3-19.5,41.8-50.4,41.8-83.2c-0.1-32.9-15.7-63.8-42.2-83.4c-8.2-6-9.9-17.6-3.9-25.8s17.6-9.9,25.8-3.9 + c35.9,26.5,57,68.5,57.1,113.1C394.4,297.4,331.4,360.3,254,360.3z"/> + </g> +</g> +</svg> diff --git a/grails-profiles/web/skeleton/grails-app/assets/images/spring.svg b/grails-profiles/web/skeleton/grails-app/assets/images/spring.svg new file mode 100644 index 0000000000..93d6bde281 --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/images/spring.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 97.1 97"><defs><style>.cls-1{fill:#6db33f;}</style></defs><title>spring-icon</title><g id="Layer_2" data-name="Layer 2"><g id="logos"><path class="cls-1" d="M88.4,5.6a42.32,42.32,0,0,1-5.2,9.1A48.46,48.46,0,1,0,15.5,84l1.8,1.6A48.41,48.41,0,0,0,96.8,52C98.2,39.8,94.5,24.2,88.4,5.6ZM22.5,84.4a4.12,4.12,0,1,1-.6-5.8A4.21,4.21,0,0,1,22.5,84.4ZM88.1,69.9C76.2,85.8,50.6,80.4,34.3,81.2c0,0-2.9.2-5.8.6,0,0,1.1-.5,2.5-1,11.5-4,16.9-4.8,23.9-8. [...] \ No newline at end of file diff --git a/grails-profiles/web/skeleton/grails-app/assets/javascripts/welcome.js b/grails-profiles/web/skeleton/grails-app/assets/javascripts/welcome.js new file mode 100644 index 0000000000..e2f6333006 --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/javascripts/welcome.js @@ -0,0 +1,115 @@ +(function () { + function compare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; + } + + function parseVersionParts(v) { + const cleaned = String(v || '').trim().replace(/[^0-9.]+/g, '.'); + if (!cleaned) return []; + return cleaned.split('.').filter(Boolean).map((s) => Number(s) || 0); + } + + function compareVersions(a, b) { + const pa = parseVersionParts(a); + const pb = parseVersionParts(b); + const n = Math.max(pa.length, pb.length); + for (let i = 0; i < n; i++) { + const da = pa[i] ?? 0; + const db = pb[i] ?? 0; + if (da !== db) return da < db ? -1 : 1; + } + const sa = String(a || '').toLowerCase(); + const sb = String(b || '').toLowerCase(); + return compare(sa, sb); + } + + function getValue(tr, key) { + if (key === 'name') return (tr.dataset.name || '').toLowerCase(); + if (key === 'version') return (tr.dataset.version || ''); + if (key === 'order') return Number(tr.dataset.order || '0'); + return ''; + } + + function sortTableBy(table, key, dir) { + const tbody = table.tBodies[0]; + if (!tbody) return; + + const rows = Array.from(tbody.querySelectorAll('tr')); + rows.sort((ra, rb) => { + if (key === 'version') { + return compareVersions(getValue(ra, key), getValue(rb, key)) * dir; + } + return compare(getValue(ra, key), getValue(rb, key)) * dir; + }); + + const frag = document.createDocumentFragment(); + rows.forEach((r) => frag.appendChild(r)); + tbody.appendChild(frag); + } + + function setSortIndicator(table, activeTh, ariaSortValue, dataSortDirValue) { + table.querySelectorAll('th.sortable, th[data-sort-key]').forEach((th) => { + if (th !== activeTh) th.removeAttribute('data-sort-dir'); + }); + + if (dataSortDirValue) { + activeTh.setAttribute('data-sort-dir', dataSortDirValue); + } else { + activeTh.removeAttribute('data-sort-dir'); + } + + table.querySelectorAll('th.sortable, th[data-sort-key]').forEach((th) => { + th.removeAttribute('aria-sort'); + }); + activeTh.setAttribute('aria-sort', ariaSortValue); + } + + function initSortableTable(table) { + const headers = Array.from( + table.querySelectorAll('th.sortable[data-sort-key], th[data-sort-key]') + ); + const state = { key: null, dir: 1 }; + + function activateSort(th) { + const key = th.getAttribute('data-sort-key'); + if (!key) return; + + state.dir = state.key === key ? state.dir * -1 : 1; + state.key = key; + + const ariaSort = state.dir === 1 ? 'ascending' : 'descending'; + const dataSortDir = state.dir === 1 ? 'asc' : 'desc'; + + setSortIndicator(table, th, ariaSort, dataSortDir); + sortTableBy(table, key, state.dir); + } + + headers.forEach((th) => { + th.style.cursor = 'pointer'; + + th.addEventListener('click', () => activateSort(th)); + + // Keyboard support: Enter / Space should sort like a click. + th.addEventListener('keydown', (e) => { + const k = e.key; + if (k === 'Enter' || k === ' ' || k === 'Spacebar') { + e.preventDefault(); + activateSort(th); + } + }); + }); + + // Initial load: show ascending indicator on Name (and sort to match). + const defaultKey = 'name'; + const defaultTh = headers.find((th) => (th.getAttribute('data-sort-key') || '') === defaultKey); + if (defaultTh) { + state.key = defaultKey; + state.dir = 1; + setSortIndicator(table, defaultTh, 'ascending', 'asc'); + } + } + + document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('table[data-sortable]').forEach(initSortableTable); + }); +})(); \ No newline at end of file diff --git a/grails-profiles/web/skeleton/grails-app/assets/stylesheets/welcome.css b/grails-profiles/web/skeleton/grails-app/assets/stylesheets/welcome.css new file mode 100644 index 0000000000..ba0a393921 --- /dev/null +++ b/grails-profiles/web/skeleton/grails-app/assets/stylesheets/welcome.css @@ -0,0 +1,63 @@ +.reload-indicator { + display: inline-flex; + align-items: center; + gap: .5rem; + padding: .35rem .6rem; + border: 1px solid var(--bs-border-color); + border-radius: 999px; + background: var(--bs-body-bg); + font-size: .875rem; + line-height: 1; + white-space: nowrap; +} + +.reload-dot { + position: relative; + width: .6rem; + height: .6rem; + border-radius: 999px; + background: currentColor; + flex: 0 0 auto; +} + +.reload-dot.ping::before { + content: ""; + position: absolute; + inset: 0; + border-radius: 999px; + background: currentColor; + opacity: .35; + animation: reload-ping 1.25s ease-out infinite; +} + +.sortable { + cursor: pointer; + user-select: none; +} + +.sort-hint { + display: inline-block; + width: .9em; +} + +.sort-hint::before { + content: ''; +} + +th.sortable[data-sort-dir="asc"] .sort-hint::before { + content: '↑'; +} + +th.sortable[data-sort-dir="desc"] .sort-hint::before { + content: '↓'; +} + +@keyframes reload-ping { + 0% { transform: scale(1); opacity: .35; } + 70% { transform: scale(2.25); opacity: 0; } + 100% { transform: scale(2.25); opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .reload-dot::before { animation: none; } +} diff --git a/grails-profiles/web/skeleton/grails-app/views/index.gsp b/grails-profiles/web/skeleton/grails-app/views/index.gsp index c7544050a7..3581e0e031 100644 --- a/grails-profiles/web/skeleton/grails-app/views/index.gsp +++ b/grails-profiles/web/skeleton/grails-app/views/index.gsp @@ -1,84 +1,343 @@ -<%@ page import="grails.util.Environment; org.springframework.core.SpringVersion; org.springframework.boot.SpringBootVersion" -%><!doctype html> +<%@ page import="grails.util.Environment"%> +<%@ page import="org.springframework.boot.SpringBootVersion"%> +<%@ page import="org.springframework.core.SpringVersion"%> +<g:set var="pluginManager" bean="pluginManager"/> +<g:set var="servletContext" bean="servletContext"/> +<g:set var="pluginsWithOrder" + value="${pluginManager.allPlugins.toList() + .withIndex() + .collect { p, i -> [plugin: p, order: i + 1] } + .sort { a, b -> a.plugin.name.toLowerCase() <=> b.plugin.name.toLowerCase() }}" +/> +<g:set var="numControllers" value="${grailsApplication.controllerClasses.size()}"/> +<!doctype html> <html> <head> - <meta name="layout" content="main"/> <title>Welcome to Grails</title> + <meta name="layout" content="main"/> + <asset:stylesheet src="welcome.css"/> </head> <body> -<content tag="nav"> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Application Status <span class="caret"></span></a> - <ul class="dropdown-menu"> - <li><a class="dropdown-item" href="#">Server: ${request.getServletContext().getServerInfo()}</a></li> - <li><a class="dropdown-item" href="#">Host: ${InetAddress.getLocalHost()}</a></li> - <li><a class="dropdown-item" href="#">Environment: ${Environment.current.name}</a></li> - <li><a class="dropdown-item" href="#">App version: - <g:meta name="info.app.version"/></a> - </li> - <li><a class="dropdown-item" href="#">App profile: ${grailsApplication.config.getProperty('grails.profile')}</a></li> - <li><hr class="dropdown-divider"></li> - <li><a class="dropdown-item" href="#">Grails version: - <g:meta name="info.app.grailsVersion"/></a> - </li> - <li><a class="dropdown-item" href="#">Groovy version: ${GroovySystem.getVersion()}</a></li> - <li><a class="dropdown-item" href="#">JVM version: ${System.getProperty('java.version')}</a></li> - <li><a class="dropdown-item" href="#">Spring Boot version: ${SpringBootVersion.getVersion()}</a></li> - <li><a class="dropdown-item" href="#">Spring version: ${SpringVersion.getVersion()}</a></li> - <li><hr class="dropdown-divider"></li> - <li><a class="dropdown-item" href="#">Reloading active: ${Environment.reloadingAgentEnabled}</a></li> - </ul> - </li> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Artefacts <span class="caret"></span></a> - <ul class="dropdown-menu"> - <li><a class="dropdown-item" href="#">Controllers: ${grailsApplication.controllerClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Domains: ${grailsApplication.domainClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Services: ${grailsApplication.serviceClasses.size()}</a></li> - <li><a class="dropdown-item" href="#">Tag Libraries: ${grailsApplication.tagLibClasses.size()}</a></li> - </ul> - </li> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Installed Plugins<span class="caret"></span></a> - <ul class="dropdown-menu dropdown-menu-right"> - <g:each var="plugin" in="${applicationContext.getBean('pluginManager').allPlugins}"> - <li><a class="dropdown-item" href="#">${plugin.name} - ${plugin.version}</a></li> - </g:each> - </ul> - </li> -</content> +<main id="content" role="main" class="pb-4 pb-md-5"> + <div class="container-lg py-2 py-md-3"> + <div class="row align-items-top g-4"> -<div class="svg" role="presentation"> - <div class="bg-dark-subtle text-center"> - <asset:image src="grails-cupsonly-logo-white.svg" class="w-50"/> + <%-- WELCOME MESSAGE --%> + <div class="col-12 col-md-7"> + <h1 class="display-6 fw-semibold mb-2">Welcome to Grails</h1> + <p class="lead text-body-secondary"> + Congratulations, you have successfully started a Grails application. + </p> + <p class="text-body-secondary"> + At the moment this is the default page, feel free to modify it to either + redirect to a controller or display whatever content you may choose. + </p> + </div> + + <%-- RUNTIME VERSIONS --%> + <div class="col-12 col-md-5"> + <div class="card border-1 shadow-sm"> + <div class="card-body"> + <h6 class="card-title mb-3 fw-semibold">Runtime versions</h6> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="grails.svg" alt="Grails" width="18" height="18" class="me-2"/> + Grails + </span> + <g:meta name="info.app.grailsVersion"/> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="spring-boot.svg" alt="Spring Boot" width="18" height="18" class="me-2"/> + Spring Boot + </span> + ${SpringBootVersion.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="spring.svg" alt="Spring" width="18" height="18" class="me-2"/> + Spring + </span> + ${SpringVersion.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="groovy.svg" alt="Groovy" width="18" height="18" class="me-2"/> + Groovy + </span> + ${GroovySystem.getVersion()} + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="d-inline-flex align-items-center text-body-secondary"> + <asset:image src="java.svg" alt="Java" width="18" height="18" class="me-2"/> + JVM (${System.getProperty('java.vendor')}) + </span> + ${System.getProperty('java.version')} + </li> + </ul> + </div> + </div> + </div> + </div> </div> -</div> + <div class="container-lg"> + <div class="row g-4 align-items-stretch"> -<div id="content" role="main"> - <div class="container"> - <section class="row colset-2-its"> - <h1>Welcome to Grails</h1> + <%-- APPLICATION INFO --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Application</h6> + <g:if test="${Environment.reloadingAgentEnabled}"> + <span class="reload-indicator text-success" role="status" aria-label="Reloading active"> + <span class="reload-dot ping" aria-hidden="true"></span> + <span class="text-body-secondary">Reloading active</span> + </span> + </g:if> + <g:else> + <span class="reload-indicator text-danger" role="status" aria-label="Reloading inactive"> + <span class="reload-dot" aria-hidden="true"></span> + <span class="text-body-secondary">Reloading inactive</span> + </span> + </g:else> + </div> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Name</span> + <span class="fw-medium text-truncate ms-3"><g:meta name="info.app.name"/></span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Version</span> + <span class="fw-medium" style="font-variant-numeric: tabular-nums;"> + <g:meta name="info.app.version"/> + </span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Profile</span> + <span class="fw-medium text-truncate ms-3"> + ${grailsApplication.config.getProperty('grails.profile')} + </span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Environment</span> + <span class="fw-medium">${Environment.current.name}</span> + </li> + </ul> + </div> + </div> + </div> + + <%-- SERVER INFO --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Server</h6> + </div> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Servlet Container</span> + <span class="fw-medium text-truncate ms-3">${servletContext.serverInfo}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Host</span> + <span class="fw-medium text-truncate ms-3">${InetAddress.localHost}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">OS</span> + <span class="fw-medium text-truncate ms-3"> + ${System.getProperty('os.name')} ${System.getProperty('os.version')} (${System.getProperty('os.arch')}) + </span> + </li> + </ul> + </div> + </div> + </div> - <p> - Congratulations, you have successfully started your first Grails application! At the moment - this is the default page, feel free to modify it to either redirect to a controller or display - whatever content you may choose. Below is a list of controllers that are currently deployed in - this application, click on each to execute its default action: - </p> + <%-- ARTEFACT COUNTS --%> + <div class="col-12 col-lg-4"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Artefact counts</h6> + </div> - <div id="controllers" role="navigation"> - <h2>Available Controllers:</h2> - <ul> - <g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }"> - <li class="controller"> - <g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link> - </li> - </g:each> - </ul> + <ul class="list-group list-group-flush small"> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Controllers</span> + <span class="fw-medium">${numControllers}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Domains</span> + <span class="fw-medium">${grailsApplication.domainClasses.size()}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Services</span> + <span class="fw-medium">${grailsApplication.serviceClasses.size()}</span> + </li> + <li class="list-group-item d-flex justify-content-between align-items-center px-0"> + <span class="text-body-secondary">Tag Libraries</span> + <span class="fw-medium">${grailsApplication.tagLibClasses.size()}</span> + </li> + </ul> + </div> + </div> </div> - </section> + + </div> </div> -</div> + <%-- AVAILABLE CONTROLLERS --%> + <div class="container-lg mt-4"> + <div class="row g-4 align-items-start"> + <div class="col-12 col-lg-7"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body p-4 p-md-5"> + <div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2"> + <div> + <h2 class="h4 mb-1">Available Controllers</h2> + <p class="text-body-secondary mb-0"> + ${numControllers} controller${numControllers != 1 ? 's' : ''} detected. + </p> + </div> + <g:if test="${numControllers != 0}"> + <div class="small text-body-secondary"> + Click a controller to execute its default action. + </div> + </g:if> + </div> + <hr class="my-3 my-md-4 text-body-tertiary"/> + <g:set var="controllersByNamespace" + value="${grailsApplication.controllerClasses + .groupBy { cc -> ((cc.namespace ?: '').trim()) ?: 'default' } + .sort { a, b -> a.key.toString().toLowerCase() <=> b.key.toString().toLowerCase() }}"/> + + <g:each var="nsEntry" in="${controllersByNamespace}" status="nsIndex"> + <div class="${nsIndex > 0 ? 'mt-4' : ''}"> + <div class="px-0 py-2 bg-body-tertiary"> + <div class="d-flex align-items-center justify-content-between"> + <div class="small text-uppercase text-body-secondary fw-semibold" + style="letter-spacing: .04em;"> + <g:if test="${nsEntry.key != 'default'}"> + ${nsEntry.key} + </g:if> + <g:else> + Default namespace + </g:else> + </div> + </div> + </div> + + <ul class="list-group list-group-flush"> + <g:each var="c" in="${nsEntry.value.sort { it.fullName }}"> + <g:set var="simpleName" value="${(c.fullName ?: '') + .tokenize('.') + .last() + .replaceFirst(/Controller$/, '')}"/> + + <g:set var="controllerUrl" + value="${createLink(controller: c.logicalPropertyName, namespace: c.namespace)}"/> + + <li class="list-group-item px-0"> + <div class="d-flex align-items-center justify-content-between gap-3"> + <g:link controller="${c.logicalPropertyName}" + namespace="${c.namespace}" + class="d-flex align-items-center gap-3 text-decoration-none min-w-0 flex-grow-1"> + <div class="min-w-0"> + <div class="fw-semibold text-body text-truncate"> + ${simpleName} + </div> + </div> + </g:link> + + <a href="${controllerUrl}" + class="small link-primary link-offset-2 link-underline-opacity-0 link-underline-opacity-75-hover flex-shrink-0"> + ${controllerUrl} + </a> + </div> + </li> + </g:each> + </ul> + </div> + </g:each> + </div> + </div> + </div> + + <%-- PLUGINS --%> + <div class="col-12 col-lg-5"> + <div class="card border-1 shadow-sm h-100"> + <div class="card-body"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <h6 class="card-title mb-0 fw-semibold">Installed plugins</h6> + <span class="badge text-bg-light border"> + ${pluginManager.allPlugins.size()} + </span> + </div> + + <div class="table-responsive"> + <table class="table table-sm table-striped table-hover" data-sortable="true"> + <thead class="table-light small"> + <tr> + <th scope="col" + class="text-body-secondary ps-0 fw-semibold sortable" + data-sort-key="name" + role="button" + tabindex="0" + aria-label="Sort by name"> + Name <span class="sort-hint" aria-hidden="true"></span> + </th> + <th scope="col" + class="text-body-secondary ps-0 fw-semibold text-end sortable" + data-sort-key="version" + role="button" + tabindex="0" + aria-label="Sort by version"> + <span class="sort-hint" aria-hidden="true"></span> Version + </th> + <th scope="col" + class="text-body-secondary text-end pe-0 sortable" + data-sort-key="order" + role="button" + tabindex="0" + aria-label="Sort by load order"> + <span class="sort-hint" aria-hidden="true"></span> Load order + </th> + </tr> + </thead> + <tbody class="small"> + <g:each var="row" in="${pluginsWithOrder}"> + <g:set var="pluginName" + value="${row.plugin.name + .replaceAll(/([A-Z]+)([A-Z][a-z])/, '$1 $2') + .replaceAll(/([a-z0-9])([A-Z])/, '$1 $2') + .replaceAll(/[_-]+/, ' ') + .trim() + .capitalize()}" + /> + <tr data-name="${pluginName}" data-version="${row.plugin.version}" data-order="${row.order}"> + <td class="text-truncate"> + ${pluginName} + </td> + <td class="text-end" style="font-variant-numeric: tabular-nums;"> + ${row.plugin.version} + </td> + <td class="text-end text-body-secondary" style="font-variant-numeric: tabular-nums;"> + ${row.order} + </td> + </tr> + </g:each> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + </div> +</main> +<asset:javascript src="welcome.js"/> </body> </html> diff --git a/grails-profiles/web/skeleton/grails-app/views/layouts/main.gsp b/grails-profiles/web/skeleton/grails-app/views/layouts/main.gsp index 6f09bcc995..023449b955 100644 --- a/grails-profiles/web/skeleton/grails-app/views/layouts/main.gsp +++ b/grails-profiles/web/skeleton/grails-app/views/layouts/main.gsp @@ -1,12 +1,9 @@ <!doctype html> -<html lang="en" class="no-js"> +<html lang="en"> <head> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <meta http-equiv="X-UA-Compatible" content="IE=edge"/> - <title> - <g:layoutTitle default="Grails"/> - </title> + <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title><g:layoutTitle default="Grails"/></title> <asset:link rel="icon" href="favicon.ico" type="image/x-ico"/> <asset:stylesheet src="application.css"/> <g:layoutHead/> @@ -14,68 +11,76 @@ <body> -<nav class="navbar navbar-expand-lg bg-body-tertiary"> - <div class="container-fluid"> - <a class="navbar-brand" href="/#"><asset:image class="w-75" src="grails.svg" alt="Grails Logo"/></a> - <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> - - <div class="collapse navbar-collapse" aria-expanded="false" id="navbarContent"> - <ul class="navbar-nav"> - <g:pageProperty name="page.nav"/> - </ul> - </div> +<nav class="navbar navbar-expand-lg bg-body border-bottom shadow-sm"> + <div class="container-lg"> + <a class="navbar-brand d-flex align-items-center" href="${request.contextPath}/"> + <asset:image class="w-75" src="grails.svg" alt="Grails Logo"/> + </a> </div> </nav> -<g:layoutBody/> +<div class="bg-body-tertiary"> + <div class="container-lg py-4"> + <g:layoutBody/> + </div> +</div> -<div class="footer" role="contentinfo"> - <div class="container-fluid"> - <div class="row"> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://guides.grails.org" target="_blank"> - <asset:image src="advancedgrails.svg" alt="Grails Guides" class="me-2" width="34" />Grails Guides - </a> - </h6> - <p class="card-text">Building your first Grails app? Looking to add security, or create a Single-Page-App? Check out the <a href="https://guides.grails.org" target="_blank">Grails Guides</a> for step-by-step tutorials.</p> - </div> +<footer class="border-top py-5" role="contentinfo"> + <div class="container-lg"> + <div class="row g-4"> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://guides.grails.org" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Grails Guides</h6> + <asset:image src="advancedgrails.svg" alt="Grails Guides" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Building your first Grails app? Looking to add security, or create a Single-Page-App? + Check out the Grails Guides for step-by-step tutorials. + </p> + </div> + </a> </div> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://grails.apache.org/docs/" target="_blank"> - <asset:image src="documentation.svg" alt="Grails Documentation" class="me-2" width="34" />Documentation - </a> - </h6> - <p class="card-text">Ready to dig in? You can find in-depth documentation for all the features of Grails in the <a href="https://grails.apache.org/docs/" target="_blank">User Guide</a>.</p> - </div> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://grails.apache.org/docs/" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Documentation</h6> + <asset:image src="documentation.svg" alt="Grails Documentation" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Ready to dig in? You can find in-depth documentation for all the features + of Grails in the User Guide. + </p> + </div> + </a> </div> - <div class="card border-0 col-12 col-md"> - <div class="card-body"> - <h6 class="card-title"> - <a class="link-underline link-underline-opacity-0" href="https://slack.grails.org" target="_blank"> - <asset:image src="slack.svg" alt="Grails Slack" class="me-2" width="34" />Join the Community - </a> - </h6> - <p class="card-text">Get feedback and share your experience with other Grails developers in the community <a href="https://slack.grails.org" target="_blank">Slack channel</a>.</p> - </div> + <div class="col-12 col-md-4"> + <a class="card h-100 text-decoration-none shadow-sm border-1" + href="https://slack.grails.org" target="_blank" rel="noopener"> + <div class="card-body p-4"> + <div class="d-flex align-items-center justify-content-between mb-2"> + <h6 class="card-title mb-0 fw-semibold">Join the Community</h6> + <asset:image src="slack.svg" alt="Grails Slack" width="34" height="34"/> + </div> + <p class="card-text text-body-secondary mb-0"> + Get feedback and share your experience with other Grails developers + in the community Slack channel. + </p> + </div> + </a> </div> </div> </div> -</div> - +</footer> <div id="spinner" class="position-absolute top-0 end-0 p-1" style="display:none;"> <div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> - - <asset:javascript src="application.js"/> - </body> </html>
