Show the build in a footer, linked to its release or commit
The version moves from the header to a "Powered by Jarl" footer. A build of a release tag (HEAD tagged v<VERSION>, no tracked changes; the release workflow sets RELEASE_TAG) shows and links the release; any other build shows and links its commit, marked -modified with uncommitted changes. jarld version, jarl --version, /status and /auth/whoami report the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b05711b13e
commit
db9ebb14dd
14 files changed
+176
-14
No files matched your search
@@ -48,6 +48,10 @@ jobs:
|
|||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
# Checked against VERSION above; don't depend on the checkout
|
||||||
|
# having the tag.
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
export DEB_MAINTAINER="$(git log -1 --format='%an <%ae>')"
|
export DEB_MAINTAINER="$(git log -1 --format='%an <%ae>')"
|
||||||
make dist
|
make dist
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- The version moved from the header to a footer: "Powered by Jarl v0.1.0",
|
||||||
|
linking to the release on Gitea. A build that isn't of a release tag
|
||||||
|
shows its commit instead, linked to the commit and marked `-modified`
|
||||||
|
with uncommitted changes. Before, every build of a commit after a
|
||||||
|
release still showed the release's version. `jarld version`,
|
||||||
|
`jarl --version`, `GET /status` and `/auth/whoami` report the same, the
|
||||||
|
latter two as `build` (docs/development.md).
|
||||||
|
|
||||||
- The view in the zone list, and on the zone page, links to that view,
|
- The view in the zone list, and on the zone page, links to that view,
|
||||||
and the views page highlights it. It used to be plain text, and the zone
|
and the views page highlights it. It used to be plain text, and the zone
|
||||||
page linked to the views page as a whole.
|
page linked to the views page as a whole.
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
# (jarl-cli-<version>-<os>-<arch>[.exe])
|
# (jarl-cli-<version>-<os>-<arch>[.exe])
|
||||||
|
|
||||||
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
||||||
LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION)
|
# RELEASE_TAG marks a build of a release (the web UI then shows and links
|
||||||
|
# the release rather than the commit): set when HEAD is tagged v$(VERSION)
|
||||||
|
# and no tracked file is changed.
|
||||||
|
RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t)
|
||||||
|
LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) -X jarl/internal/version.Tag=$(RELEASE_TAG)
|
||||||
GOFLAGS := -trimpath
|
GOFLAGS := -trimpath
|
||||||
export CGO_ENABLED := 0
|
export CGO_ENABLED := 0
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -225,7 +225,7 @@ Exit codes: 0 ok, 1 error, 3 not authenticated or not allowed, 4 not found,
|
|||||||
5 conflict, 6 invalid or rejected by validation, 7 server or BIND error.`,
|
5 conflict, 6 invalid or rejected by validation, 7 server or BIND error.`,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
Version: version.Version,
|
Version: version.String(),
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return checkFanOut(cmd)
|
return checkFanOut(cmd)
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -383,7 +383,7 @@ func versionCmd() *cobra.Command {
|
|||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print the version",
|
Short: "Print the version",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("jarld", version.Version)
|
fmt.Println("jarld", version.String())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-3
@@ -50,7 +50,7 @@ func rootCmd() *cobra.Command {
|
|||||||
Short: "Jarl: manage BIND (named) through a web UI, a REST API and a CLI",
|
Short: "Jarl: manage BIND (named) through a web UI, a REST API and a CLI",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
Version: version.Version,
|
Version: version.String(),
|
||||||
}
|
}
|
||||||
def := os.Getenv("JARL_CONFIG")
|
def := os.Getenv("JARL_CONFIG")
|
||||||
if def == "" {
|
if def == "" {
|
||||||
@@ -295,7 +295,7 @@ func serveCmd() *cobra.Command {
|
|||||||
Listen: a.cfg.Listen, TLSCertFile: a.cfg.TLS.CertFile, TLSKeyFile: a.cfg.TLS.KeyFile,
|
Listen: a.cfg.Listen, TLSCertFile: a.cfg.TLS.CertFile, TLSKeyFile: a.cfg.TLS.KeyFile,
|
||||||
TrustedProxies: a.cfg.Proxies(), SessionLifetime: time.Duration(a.cfg.Session.Lifetime),
|
TrustedProxies: a.cfg.Proxies(), SessionLifetime: time.Duration(a.cfg.Session.Lifetime),
|
||||||
SessionIdle: time.Duration(a.cfg.Session.IdleTimeout), CookieSecure: a.cfg.Session.CookieSecure,
|
SessionIdle: time.Duration(a.cfg.Session.IdleTimeout), CookieSecure: a.cfg.Session.CookieSecure,
|
||||||
Version: version.Version, Stats: collector, Socket: a.cfg.Socket,
|
Version: version.String(), Stats: collector, Socket: a.cfg.Socket,
|
||||||
BackupDir: a.cfg.Backup.Dir, BackupWarnAfter: time.Duration(a.cfg.Backup.WarnAfter),
|
BackupDir: a.cfg.Backup.Dir, BackupWarnAfter: time.Duration(a.cfg.Backup.WarnAfter),
|
||||||
HTTPReq: server.HTTPReq{
|
HTTPReq: server.HTTPReq{
|
||||||
Enabled: a.cfg.ACME.HTTPReq.Enabled,
|
Enabled: a.cfg.ACME.HTTPReq.Enabled,
|
||||||
@@ -313,7 +313,7 @@ func serveCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go auditRetention(ctx, a)
|
go auditRetention(ctx, a)
|
||||||
a.log.Info("jarld starting", "version", version.Version, "database", a.cfg.Database, "bind_config", a.cfg.BIND.ConfigDir)
|
a.log.Info("jarld starting", "version", version.String(), "database", a.cfg.Database, "bind_config", a.cfg.BIND.ConfigDir)
|
||||||
err = srv.Serve(ctx, func() { sdNotify("READY=1\nSTATUS=serving") })
|
err = srv.Serve(ctx, func() { sdNotify("READY=1\nSTATUS=serving") })
|
||||||
sdNotify("STOPPING=1")
|
sdNotify("STOPPING=1")
|
||||||
a.log.Info("jarld stopped")
|
a.log.Info("jarld stopped")
|
||||||
|
|||||||
@@ -202,6 +202,22 @@ component `main` ([install.md](install.md#from-the-package-registry)). A
|
|||||||
version with a hyphen (`1.0.0-rc.1`) becomes a pre-release; its packages
|
version with a hyphen (`1.0.0-rc.1`) becomes a pre-release; its packages
|
||||||
get version `1.0.0~rc.1`, which apt sorts before `1.0.0`.
|
get version `1.0.0~rc.1`, which apt sorts before `1.0.0`.
|
||||||
|
|
||||||
|
### Which version a build reports
|
||||||
|
|
||||||
|
The footer of the web UI, `jarld version` and `jarl --version` say what a
|
||||||
|
binary was built from, linking to it on Gitea:
|
||||||
|
|
||||||
|
* **A release**: `make` run on the commit tagged `v<VERSION>` with no
|
||||||
|
tracked file changed. The Makefile then sets `RELEASE_TAG`, and the
|
||||||
|
footer reads "Powered by Jarl v0.2.0", linking to the release. The
|
||||||
|
release workflow sets `RELEASE_TAG` itself.
|
||||||
|
* **Any other build**: the commit, which Go records from git: "Powered by
|
||||||
|
Jarl 1a2b3c4d5e6f", linking to the commit. With uncommitted changes it
|
||||||
|
is marked `-modified`. The version commands print `0.2.0 (1a2b3c4d5e6f)`.
|
||||||
|
* **No git** (a build from a source tarball): just `VERSION`.
|
||||||
|
|
||||||
|
`RELEASE_TAG=v0.2.0 make` forces the first case.
|
||||||
|
|
||||||
Pushing the same tag again (`git tag -f`, `git push -f`) re-runs it, and
|
Pushing the same tag again (`git tag -f`, `git push -f`) re-runs it, and
|
||||||
the script replaces the attachments and packages, so a failed release can
|
the script replaces the attachments and packages, so a failed release can
|
||||||
simply be retried.
|
simply be retried.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"jarl/internal/model"
|
"jarl/internal/model"
|
||||||
"jarl/internal/nscheck"
|
"jarl/internal/nscheck"
|
||||||
|
"jarl/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is the API path prefix.
|
// Version is the API path prefix.
|
||||||
@@ -92,6 +93,8 @@ type Whoami struct {
|
|||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
// Build is what the server was built from (release or commit).
|
||||||
|
Build version.Build `json:"build"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginRequest logs in.
|
// LoginRequest logs in.
|
||||||
@@ -156,6 +159,7 @@ type Changeset struct {
|
|||||||
// StatusResponse is GET /status.
|
// StatusResponse is GET /status.
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
Build version.Build `json:"build"`
|
||||||
Engine json.RawMessage `json:"engine"`
|
Engine json.RawMessage `json:"engine"`
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
// ExpiringTokens are tokens that expire within TokenExpiryWarning or
|
// ExpiringTokens are tokens that expire within TokenExpiryWarning or
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"jarl/internal/engine"
|
"jarl/internal/engine"
|
||||||
"jarl/internal/model"
|
"jarl/internal/model"
|
||||||
"jarl/internal/store"
|
"jarl/internal/store"
|
||||||
|
"jarl/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toRequests(in []api.ChangeRequest) []engine.Request {
|
func toRequests(in []api.ChangeRequest) []engine.Request {
|
||||||
@@ -273,7 +274,7 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request, p *auth.Principa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
resp := api.StatusResponse{Version: s.cfg.Version, Engine: raw(st), Time: now, ExpiringTokens: s.expiringTokens(r.Context(), p, now)}
|
resp := api.StatusResponse{Version: s.cfg.Version, Build: version.Info(), Engine: raw(st), Time: now, ExpiringTokens: s.expiringTokens(r.Context(), p, now)}
|
||||||
if p.IsAdmin() {
|
if p.IsAdmin() {
|
||||||
resp.Backup = s.backupStatus(r.Context(), now)
|
resp.Backup = s.backupStatus(r.Context(), now)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"jarl/internal/model"
|
"jarl/internal/model"
|
||||||
"jarl/internal/render"
|
"jarl/internal/render"
|
||||||
"jarl/internal/rr"
|
"jarl/internal/rr"
|
||||||
|
"jarl/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]int) api.Zone {
|
func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]int) api.Zone {
|
||||||
@@ -132,7 +133,7 @@ func presentToken(t *model.Token) api.Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) whoamiOf(p *auth.Principal, se *model.Session) api.Whoami {
|
func (s *Server) whoamiOf(p *auth.Principal, se *model.Session) api.Whoami {
|
||||||
out := api.Whoami{User: presentUser(p.User), Method: p.Method, Permissions: p.Summary(), Admin: p.IsAdmin(), Version: s.cfg.Version}
|
out := api.Whoami{User: presentUser(p.User), Method: p.Method, Permissions: p.Summary(), Admin: p.IsAdmin(), Version: s.cfg.Version, Build: version.Info()}
|
||||||
if p.Token != nil {
|
if p.Token != nil {
|
||||||
t := presentToken(p.Token)
|
t := presentToken(p.Token)
|
||||||
out.Token = &t
|
out.Token = &t
|
||||||
|
|||||||
@@ -1,7 +1,94 @@
|
|||||||
// Package version holds Jarl's version, set at build time with
|
// Package version holds Jarl's version, set at build time with
|
||||||
//
|
//
|
||||||
// go build -ldflags "-X jarl/internal/version.Version=1.2.3"
|
// go build -ldflags "-X jarl/internal/version.Version=1.2.3 -X jarl/internal/version.Tag=v1.2.3"
|
||||||
|
//
|
||||||
|
// The Makefile sets Version from the VERSION file, and Tag only when the
|
||||||
|
// build is of that release: HEAD is the tag v<VERSION> and no tracked file
|
||||||
|
// is changed. Other builds are identified by their commit, which Go
|
||||||
|
// records from git by itself.
|
||||||
package version
|
package version
|
||||||
|
|
||||||
// Version is the Jarl version.
|
import (
|
||||||
|
"runtime/debug"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is the Jarl version (the VERSION file).
|
||||||
var Version = "0.1.0-dev"
|
var Version = "0.1.0-dev"
|
||||||
|
|
||||||
|
// Tag is the release tag the build is of ("" when it isn't a release).
|
||||||
|
var Tag = ""
|
||||||
|
|
||||||
|
// Commit is the git commit the build is of. It is read from the build
|
||||||
|
// information when not set at build time.
|
||||||
|
var Commit = ""
|
||||||
|
|
||||||
|
// Repo is Jarl's source repository.
|
||||||
|
const Repo = "https://gitea.adbraun.lol/adam/jarl"
|
||||||
|
|
||||||
|
// Build describes what a binary was built from.
|
||||||
|
type Build struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
Commit string `json:"commit,omitempty"`
|
||||||
|
Modified bool `json:"modified,omitempty"` // built with uncommitted changes
|
||||||
|
// Label is the short form to show: the tag of a release, otherwise
|
||||||
|
// the abbreviated commit (or the version when the commit is unknown).
|
||||||
|
Label string `json:"label"`
|
||||||
|
// URL is the release or commit in the repository.
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = sync.OnceValue(func() Build {
|
||||||
|
commit, modified := Commit, false
|
||||||
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
for _, s := range bi.Settings {
|
||||||
|
switch s.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
if commit == "" {
|
||||||
|
commit = s.Value
|
||||||
|
}
|
||||||
|
case "vcs.modified":
|
||||||
|
modified = s.Value == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return describe(Version, Tag, commit, modified)
|
||||||
|
})
|
||||||
|
|
||||||
|
func describe(version, tag, commit string, modified bool) Build {
|
||||||
|
b := Build{Version: version, Tag: tag, Commit: commit, Modified: modified}
|
||||||
|
switch {
|
||||||
|
case tag != "":
|
||||||
|
b.Modified = false // the Makefile only sets Tag for clean trees
|
||||||
|
b.Label, b.URL = tag, Repo+"/releases/tag/"+tag
|
||||||
|
case commit != "":
|
||||||
|
b.Label, b.URL = short(commit), Repo+"/commit/"+commit
|
||||||
|
if modified {
|
||||||
|
b.Label += "-modified"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
b.Label, b.URL = version, Repo
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info describes this build.
|
||||||
|
func Info() Build { return info() }
|
||||||
|
|
||||||
|
// String is the version for logs and 'version' commands: "0.2.0" for a
|
||||||
|
// release, otherwise the version and the commit ("0.2.0-dev (a1b2c3d4e5f6)").
|
||||||
|
func String() string {
|
||||||
|
b := Info()
|
||||||
|
if b.Tag != "" || b.Commit == "" {
|
||||||
|
return b.Version
|
||||||
|
}
|
||||||
|
return b.Version + " (" + b.Label + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func short(commit string) string {
|
||||||
|
if len(commit) > 12 {
|
||||||
|
return commit[:12]
|
||||||
|
}
|
||||||
|
return commit
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDescribe(t *testing.T) {
|
||||||
|
const c = "4d00be5e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
tag, commit string
|
||||||
|
modified bool
|
||||||
|
label, url string
|
||||||
|
}{
|
||||||
|
{"v0.1.0", c, true, "v0.1.0", Repo + "/releases/tag/v0.1.0"},
|
||||||
|
{"", c, false, "4d00be5e1f0a", Repo + "/commit/" + c},
|
||||||
|
{"", c, true, "4d00be5e1f0a-modified", Repo + "/commit/" + c},
|
||||||
|
{"", "", false, "0.1.0", Repo},
|
||||||
|
} {
|
||||||
|
b := describe("0.1.0", tc.tag, tc.commit, tc.modified)
|
||||||
|
if b.Label != tc.label || b.URL != tc.url {
|
||||||
|
t.Errorf("describe(%q, %q, %v) = %q %q, want %q %q", tc.tag, tc.commit, tc.modified, b.Label, b.URL, tc.label, tc.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,8 +93,7 @@ function layout() {
|
|||||||
const header = h("header", { class: "top" },
|
const header = h("header", { class: "top" },
|
||||||
h("a", { class: "brand", href: "#/", title: "Dashboard" },
|
h("a", { class: "brand", href: "#/", title: "Dashboard" },
|
||||||
logo(30),
|
logo(30),
|
||||||
wordmark({ rule: false, height: 19, cls: "brand-name" }),
|
wordmark({ rule: false, height: 19, cls: "brand-name" })),
|
||||||
h("span", { class: "brand-version" }, me.version)),
|
|
||||||
navEl,
|
navEl,
|
||||||
h("div", { class: "spacer" }),
|
h("div", { class: "spacer" }),
|
||||||
modeEl = h("span"),
|
modeEl = h("span"),
|
||||||
@@ -102,7 +101,7 @@ function layout() {
|
|||||||
h("a", { href: "#/account", class: "user", title: (me.permissions || []).join("\n") }, me.user.username),
|
h("a", { href: "#/account", class: "user", title: (me.permissions || []).join("\n") }, me.user.username),
|
||||||
me.admin ? h("a", { href: "#/users" }, "Users") : null,
|
me.admin ? h("a", { href: "#/users" }, "Users") : null,
|
||||||
h("button", { class: "small", onclick: logout }, "Log out"));
|
h("button", { class: "small", onclick: logout }, "Log out"));
|
||||||
mount(app, header, barEl, mainEl);
|
mount(app, h("div", { class: "shell" }, header, barEl, mainEl, footer(me.build)));
|
||||||
modeHolder = modeEl;
|
modeHolder = modeEl;
|
||||||
renderNav();
|
renderNav();
|
||||||
renderBar();
|
renderBar();
|
||||||
@@ -110,6 +109,16 @@ function layout() {
|
|||||||
|
|
||||||
let modeHolder;
|
let modeHolder;
|
||||||
|
|
||||||
|
// footer names the Jarl build: the release, or the commit for other
|
||||||
|
// builds, linked to it in the repository.
|
||||||
|
function footer(b) {
|
||||||
|
if (!b) return null;
|
||||||
|
const what = b.tag ? "release " + b.tag
|
||||||
|
: b.commit ? "commit " + b.commit + (b.modified ? ", with uncommitted changes" : "") : "version " + b.version;
|
||||||
|
return h("footer", { class: "site" }, "Powered by ",
|
||||||
|
h("a", { href: b.url, target: "_blank", rel: "noopener", title: "Jarl " + b.version + ", built from " + what }, "Jarl " + b.label));
|
||||||
|
}
|
||||||
|
|
||||||
function renderNav() {
|
function renderNav() {
|
||||||
mount(navEl, navItems.map(([key, href, label]) => h("a", { href, class: current.nav === key ? "active" : "" }, label)));
|
mount(navEl, navItems.map(([key, href, label]) => h("a", { href, class: current.nav === key ? "active" : "" }, label)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ header.top {
|
|||||||
header.top .brand { display: inline-flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; }
|
header.top .brand { display: inline-flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; }
|
||||||
header.top .brand .logo { display: block; transition: transform 0.15s; }
|
header.top .brand .logo { display: block; transition: transform 0.15s; }
|
||||||
header.top .brand .brand-name { display: block; color: var(--text); }
|
header.top .brand .brand-name { display: block; color: var(--text); }
|
||||||
header.top .brand .brand-version { color: var(--muted); font-size: 12px; align-self: flex-end; padding-bottom: 2px; }
|
|
||||||
header.top .brand:hover { text-decoration: none; }
|
header.top .brand:hover { text-decoration: none; }
|
||||||
header.top .brand:hover .brand-name { color: var(--accent); }
|
header.top .brand:hover .brand-name { color: var(--accent); }
|
||||||
header.top .brand:hover .logo { transform: scale(1.06); }
|
header.top .brand:hover .logo { transform: scale(1.06); }
|
||||||
@@ -403,3 +402,10 @@ button.chip:hover { background: var(--accent-soft); border-color: var(--accent);
|
|||||||
.swatch.selected { box-shadow: 0 0 0 2px currentColor; }
|
.swatch.selected { box-shadow: 0 0 0 2px currentColor; }
|
||||||
.swatch:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
|
.swatch:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
|
||||||
tr.selected td { background: var(--accent-soft); }
|
tr.selected td { background: var(--accent-soft); }
|
||||||
|
|
||||||
|
/* the page footer with the Jarl build */
|
||||||
|
.shell { min-height: 100vh; display: flex; flex-direction: column; }
|
||||||
|
.shell > main { flex: 1; width: 100%; }
|
||||||
|
footer.site { padding: 12px 16px 16px; text-align: center; font-size: 12px; color: var(--muted); }
|
||||||
|
footer.site a { color: var(--muted); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 2px; }
|
||||||
|
footer.site a:hover { color: var(--accent); text-decoration-color: currentColor; }
|
||||||
Reference in new issue
Block a user