feat(ui): improve markup attention colors (#13224)

## Refactor styles of attention markdown formatting

* simplify CSS
* simplify generated HTML a bit
* get rid of a few `!important` properties by using higher specificity selectors
* bring everything to one file and out of `base.css`
* apply declarative purpose-built colors introduced in forgejo/forgejo!9643
    * create new colors for `violet` and `blue`
    * move `oklch` declarations out of themes to `base.css`, leave themes with access to all parameters
    * use declarative styles in all themes, not just `forgejo-dark`, remove variable fallbacks from styles
* remove rule overrides from `forgejo-dark`. _it should ideally only contain variables, not rules_

What's improved:
* lightness and saturation are now same for all color variants of different attention types within one theme
* code is more maintainable
* custom themes are easier to make, our example themes are dumping less RGB values at theme authors and allow changing some important colors just by tweaking numeric values (lightness, chroma, hue)
* forgejo and gitea dark themes are now using good contrast

### Preview

#### Forgejo themes

|dark before|dark after|light before|light after|
|-|-|-|-|
|![](https://codeberg.org/attachments/b59e793e-abe7-41ef-9008-368e79325783)|![](https://codeberg.org/attachments/fe4c75bf-4a6c-4aec-9de2-5d1b0af812dd)|![](https://codeberg.org/attachments/13920738-8193-4a9d-82f7-3d93bea6573b)|![](https://codeberg.org/attachments/a4678d87-8b98-4dec-a333-727dc758faa7)|

#### Gitea themes

|dark before|dark after|light before|light after|
|-|-|-|-|
|![](https://codeberg.org/attachments/d20c34f9-90f9-46b0-8d26-fe76d832c896)|![](https://codeberg.org/attachments/88837fc6-a75c-4679-9716-760853e14755)|![](https://codeberg.org/attachments/4830fb41-f29e-46a5-afc1-8edaaa082d78)|![](https://codeberg.org/attachments/e299e186-f581-40c2-8ebf-8e73a339ac7d)|

Some previously (or now) unused color variables were also removed. They have existed for years and were never applied anywhere.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13224
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
0ko 2026-07-07 06:33:16 +02:00
commit 1d72ad088c
17 changed files with 154 additions and 117 deletions

View file

@ -125,7 +125,7 @@ func (r *GitHubCalloutHTMLRenderer) renderAttention(w util.BufWriter, source []b
default:
octiconName = "info"
}
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16)))
}
return ast.WalkContinue, nil
}

View file

@ -68,7 +68,7 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-title$`)).OnElements("p")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-[\w-]+$`)).OnElements("svg")
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
policy.AllowAttrs("fill-rule", "d").OnElements("path")

1
release-notes/13224.md Normal file
View file

@ -0,0 +1 @@
feat(ui): improve markup attention colors. Note: CSS variables were changed, custom themes may need to be updated accordingly

View file

@ -106,6 +106,25 @@ body:
id: textarea-two
attributes:
label: two
`},
}}, nil)
newRepo(t, 2, "markup-attention", nil, []FileChanges{{
Filename: "github-modern.md",
Versions: []string{`
> [!note]
> This text is a note
> [!tip]
> This text is a tip
> [!important]
> This text is important
> [!warning]
> This text is a warning
> [!caution]
> This text is to make someone cautious
`},
}}, nil)
newRepo(t, 11, "dependency-test", map[unit_model.Type]convert.Conversion{

View file

@ -105,6 +105,9 @@ func TestE2e(t *testing.T) {
}
t.Run(testname, func(t *testing.T) {
// Add test-specific fixures or config options.
// Note: this breaks test execution through Playwright Extension for VSCode
// Note: these tests only work properly via `make test-e2e-sqlite#filename`
if testname == "buttons.test.e2e" || testname == "dropdown.test.e2e" || testname == "modal.test.e2e" {
defer test.MockVariableValue(&setting.IsProd, false)()
defer test.MockVariableValue(&testE2eWebRoutes, routers.NormalRoutes())()

View file

@ -1,3 +1,6 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// web_src/css/markup/**
// @watch end
@ -6,7 +9,7 @@ import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('markup with #xyz-mode-only', async ({page}) => {
test('Markup with #xyz-mode-only', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@ -16,3 +19,37 @@ test('markup with #xyz-mode-only', async ({page}) => {
await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden();
await screenshot(page);
});
test('Attention formatting', async ({page}) => {
const response = await page.goto('/user2/markup-attention/src/branch/main/github-modern.md');
expect(response?.status()).toBe(200);
const attentionTypes = [
'note',
'tip',
'important',
'warning',
'caution',
];
await expect(async () => {
await Promise.all(attentionTypes.map(async (attentionType) => {
const selector = `.markup blockquote.attention-header.attention-${attentionType}`;
expect(await page.locator(selector).evaluate((el) => getComputedStyle(el).borderInlineStartWidth)).toBe('4px');
// Get all interesting colors
const borderColor = await page.locator(selector).evaluate((el) => getComputedStyle(el).borderInlineStartColor);
const titleColor = await page.locator(`${selector} > p.attention-title > strong`).evaluate((el) => getComputedStyle(el).color);
const iconColor = await page.locator(`${selector} > p.attention-title > svg`).evaluate((el) => getComputedStyle(el).color);
const ugcColor = await page.locator(`${selector} > p:not(.attention-title)`).evaluate((el) => getComputedStyle(el).color);
// It's difficult to reliably evaluate the actual colors, but checking that the
// colors are same for border/title/icon, and UGC is different is good enough
expect(borderColor).toBe(titleColor);
expect(titleColor).toBe(iconColor);
expect(ugcColor).not.toBe(titleColor);
}));
}).toPass({timeout: 3000});
await screenshot(page);
});

View file

@ -6,22 +6,6 @@
overflow-inline: auto;
}
.runner-container .runner-ops > a {
margin-inline-start: 0.5em;
}
.runner-container .runner-ops-delete {
color: var(--color-red-light);
}
.runner-container .runner-new-text {
color: var(--color-white);
}
.runner-container #runner-new:hover .runner-new-text {
color: var(--color-white) !important;
}
.runner-container .task-status-success {
background-color: var(--color-green);
color: var(--color-white);

View file

@ -29,6 +29,14 @@
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
--button-spacing: 0.33rem;
/* Colors for thin elements: octicons, text, borders */
--color-thin-green: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-green));
--color-thin-red: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-red));
--color-thin-purple: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-purple));
--color-thin-violet: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-violet));
--color-thin-orange: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-orange));
--color-thin-blue: oklch(var(--thin-lightness) var(--regular-chroma) var(--hue-blue));
}
@media (min-width: 768px) and (max-width: 1200px) {
@ -665,11 +673,11 @@ img.ui.avatar,
}
.text.red {
color: var(--color-thin-red, var(--color-red)) !important;
color: var(--color-thin-red) !important;
}
.text.orange {
color: var(--color-thin-orange, var(--color-orange)) !important;
color: var(--color-thin-orange) !important;
}
.text.yellow {
@ -677,7 +685,7 @@ img.ui.avatar,
}
.text.green {
color: var(--color-thin-green, var(--color-green)) !important;
color: var(--color-thin-green) !important;
}
.text.teal {
@ -689,7 +697,7 @@ img.ui.avatar,
}
.text.purple {
color: var(--color-thin-purple, var(--color-purple)) !important;
color: var(--color-thin-purple) !important;
}
.text.brown {
@ -935,50 +943,6 @@ img.ui.avatar,
border-radius: var(--border-radius);
}
.attention-icon {
margin: auto 0.5em auto 0;
}
.attention-title {
align-items: center;
display: flex;
}
blockquote.attention-note {
border-inline-start-color: var(--color-blue-dark-1);
}
strong.attention-note, svg.attention-note {
color: var(--color-blue-dark-1);
}
blockquote.attention-tip {
border-inline-start-color: var(--color-success-text);
}
strong.attention-tip, svg.attention-tip {
color: var(--color-success-text);
}
blockquote.attention-important {
border-inline-start-color: var(--color-violet-dark-1);
}
strong.attention-important, svg.attention-important {
color: var(--color-violet-dark-1);
}
blockquote.attention-warning {
border-inline-start-color: var(--color-warning-text);
}
strong.attention-warning, svg.attention-warning {
color: var(--color-warning-text);
}
blockquote.attention-caution {
border-inline-start-color: var(--color-red-dark-1);
}
strong.attention-caution, svg.attention-caution {
color: var(--color-red-dark-1);
}
.center:not(.popup) {
text-align: center;
}

View file

@ -267,7 +267,7 @@ input:-webkit-autofill:active,
.ui.form .required.fields > .field > label::after,
.ui.form .required.field > label::after,
.ui.form label.required::after {
color: var(--color-thin-red, var(--color-red));
color: var(--color-thin-red);
}
.ui.input {

View file

@ -50,6 +50,7 @@
@import "./features/tom-select.css";
@import "./features/console.css";
@import "./markup/attention.css";
@import "./markup/content.css";
@import "./markup/codecopy.css";
@import "./markup/asciicast.css";

View file

@ -0,0 +1,34 @@
/* Copyright 2026 The Forgejo Authors. All rights reserved.
* SPDX-License-Identifier: GPL-3.0-or-later */
.markup blockquote.attention-header {
padding: 0.5em 0.75em;
color: var(--color-text);
border-inline-start-color: var(--attention-color);
}
.attention-title {
align-items: center;
display: flex;
color: var(--attention-color);
svg {
margin: auto 0.5em auto 0;
}
}
.attention-note {
--attention-color: var(--color-thin-blue);
}
.attention-tip {
--attention-color: var(--color-thin-green);
}
.attention-important {
--attention-color: var(--color-thin-violet);
}
.attention-warning {
--attention-color: var(--color-warning-text);
}
.attention-caution {
--attention-color: var(--color-thin-red);
}

View file

@ -67,8 +67,8 @@
.button.danger {
/* Fallbacks are needed while -thin vars are not supplied by all themes */
background: var(--color-danger-bg, var(--color-error-bg));
border: 1px solid var(--color-thin-red, var(--color-red));
color: var(--color-thin-red, var(--color-red));
border: 1px solid var(--color-thin-red);
color: var(--color-thin-red);
&:hover,
&:focus {

View file

@ -168,8 +168,3 @@ h4.ui.header .sub.header {
color: var(--color-warning-text) !important;
border-color: var(--color-warning-border) !important;
}
.attention-header {
padding: 0.5em 0.75em !important;
color: var(--color-text) !important;
}

View file

@ -27,9 +27,6 @@
--color-primary-dark-2: #fdba74;
--color-primary-dark-3: #fed7aa;
--color-primary-dark-4: #fed7aa;
--color-primary-dark-5: #ffedd5;
--color-primary-dark-6: #ffedd5;
--color-primary-dark-7: #fff7ed;
--color-primary-light-1: #f97316;
--color-primary-light-2: #ea580c;
--color-primary-light-3: #c2410c;
@ -107,8 +104,6 @@
--color-olive-light: #839311;
--color-green-light: #16a34a;
--color-teal-light: #14b8a6;
--color-blue-light: #3b82f6;
--color-violet-light: #8b5cf6;
--color-purple-light: #a855f7;
--color-pink-light: #ec4899;
--color-brown-light: #94674a;
@ -121,7 +116,6 @@
--color-olive-dark-1: #839311;
--color-green-dark-1: #137337;
--color-teal-dark-1: #0c857a;
--color-blue-dark-1: #1554e0;
--color-violet-dark-1: #6a1feb;
--color-purple-dark-1: #8519e7;
--color-pink-dark-1: #c7216b;
@ -134,7 +128,6 @@
--color-olive-dark-2: #91a313;
--color-green-dark-2: #15803d;
--color-teal-dark-2: #0a766d;
--color-blue-dark-2: #2563eb;
--color-violet-dark-2: #5c14d8;
--color-purple-dark-2: #7c3aed;
--color-pink-dark-2: #b11d5f;
@ -196,13 +189,16 @@
--color-orange-badge: #ea580c;
--color-orange-badge-bg: #ea580c22;
--color-orange-badge-hover-bg: #ea580c44;
/* Colors for thin elements: octicons, text, borders */
/* A more declarative way for themes */
--thin-lightness: 0.68;
--regular-chroma: 0.19;
--color-thin-green: oklch(var(--thin-lightness) var(--regular-chroma) 145deg);
--color-thin-red: oklch(var(--thin-lightness) var(--regular-chroma) 27deg);
--color-thin-purple: oklch(var(--thin-lightness) var(--regular-chroma) 298deg);
--color-thin-orange: oklch(var(--thin-lightness) var(--regular-chroma) 41deg);
--hue-green: 145deg;
--hue-red: 27deg;
--hue-purple: 298deg;
--hue-violet: 293deg;
--hue-orange: 41deg;
--hue-blue: 260deg;
/* Ditto but when additional highlight is needed, e.g. on user interaction */
--thin-lightness-highlight: 0.75;
@ -345,15 +341,6 @@ i.grey.icon.icon.icon.icon {
.ui.yellow.label.pending-label {
color: var(--color-warning-text) !important;
}
strong.attention-important, svg.attention-important {
color: var(--color-violet-light);
}
strong.attention-note, svg.attention-note {
color: var(--color-blue-light);
}
strong.attention-caution, svg.attention-caution {
color: var(--color-red-light);
}
.ui.basic.red.button {
background-color: var(--color-red);
color: var(--color-white);

View file

@ -44,9 +44,6 @@
--color-primary-dark-2: #9a3412;
--color-primary-dark-3: #9a3412;
--color-primary-dark-4: #7c2d12;
--color-primary-dark-5: #7c2d12;
--color-primary-dark-6: #7c2d12;
--color-primary-dark-7: #7c2d12;
--color-primary-light-1: #ea580c;
--color-primary-light-2: #f97316;
--color-primary-light-3: #fb923c;
@ -124,8 +121,6 @@
--color-olive-light: #839311;
--color-green-light: #16a34a;
--color-teal-light: #14b8a6;
--color-blue-light: #3b82f6;
--color-violet-light: #8b5cf6;
--color-purple-light: #a855f7;
--color-pink-light: #ec4899;
--color-brown-light: #94674a;
@ -138,7 +133,6 @@
--color-olive-dark-1: #839311;
--color-green-dark-1: #137337;
--color-teal-dark-1: #0c857a;
--color-blue-dark-1: #1554e0;
--color-violet-dark-1: #6a1feb;
--color-purple-dark-1: #8519e7;
--color-pink-dark-1: #c7216b;
@ -151,7 +145,6 @@
--color-olive-dark-2: #74820f;
--color-green-dark-2: #116631;
--color-teal-dark-2: #0a766d;
--color-blue-dark-2: #124bc7;
--color-violet-dark-2: #5c14d8;
--color-purple-dark-2: #7715cf;
--color-pink-dark-2: #b11d5f;
@ -212,6 +205,17 @@
--color-orange-badge: #ea580c;
--color-orange-badge-bg: #ea580c22;
--color-orange-badge-hover-bg: #ea580c44;
/* A more declarative way for themes */
--thin-lightness: 0.53;
--regular-chroma: 0.17;
--hue-green: 145deg;
--hue-red: 27deg;
--hue-purple: 298deg;
--hue-violet: 293deg;
--hue-orange: 41deg;
--hue-blue: 260deg;
/* target-based colors */
--color-body: #fff;
--color-box-header: var(--zinc-100);

View file

@ -10,9 +10,6 @@
--color-primary-dark-2: #679cd0;
--color-primary-dark-3: #7aa8d6;
--color-primary-dark-4: #8db5dc;
--color-primary-dark-5: #b3cde7;
--color-primary-dark-6: #d9e6f3;
--color-primary-dark-7: #f4f8fb;
--color-primary-light-1: #3876b3;
--color-primary-light-2: #31699f;
--color-primary-light-3: #2b5c8b;
@ -89,8 +86,6 @@
--color-olive-light: #abc016;
--color-green-light: #93b373;
--color-teal-light: #00b6ad;
--color-blue-light: #4e96cc;
--color-violet-light: #9b79e4;
--color-purple-light: #ba6ad5;
--color-pink-light: #d74397;
--color-brown-light: #b08061;
@ -102,7 +97,6 @@
--color-olive-dark-1: #839311;
--color-green-dark-1: #7a9e55;
--color-teal-dark-1: #00837c;
--color-blue-dark-1: #347cb3;
--color-violet-dark-1: #7b4edb;
--color-purple-dark-1: #a742c9;
--color-pink-dark-1: #be297d;
@ -115,7 +109,6 @@
--color-olive-dark-2: #74820f;
--color-green-dark-2: #6c8c4c;
--color-teal-dark-2: #00746e;
--color-blue-dark-2: #2e6e9f;
--color-violet-dark-2: #6733d6;
--color-purple-dark-2: #9834b9;
--color-pink-dark-2: #a9246f;
@ -178,6 +171,17 @@
--color-orange-badge: #f2711c;
--color-orange-badge-bg: #f2711c1a;
--color-orange-badge-hover-bg: #f2711c4d;
/* A more declarative way for themes */
--thin-lightness: 0.68;
--regular-chroma: 0.19;
--hue-green: 142deg;
--hue-red: 25deg;
--hue-purple: 298deg;
--hue-violet: 293deg;
--hue-orange: 41deg;
--hue-blue: 245deg;
/* target-based colors */
--color-body: #1c1f25;
--color-box-header: #1a1d1f;

View file

@ -10,9 +10,6 @@
--color-primary-dark-2: #31699f;
--color-primary-dark-3: #2b5c8b;
--color-primary-dark-4: #254f77;
--color-primary-dark-5: #193450;
--color-primary-dark-6: #0c1a28;
--color-primary-dark-7: #04080c;
--color-primary-light-1: #548fca;
--color-primary-light-2: #679cd0;
--color-primary-light-3: #7aa8d6;
@ -89,8 +86,6 @@
--color-olive-light: #d3e942;
--color-green-light: #46de6a;
--color-teal-light: #08fff4;
--color-blue-light: #51a5e3;
--color-violet-light: #8b67d7;
--color-purple-light: #bb64d8;
--color-pink-light: #e86bb1;
--color-brown-light: #c58b66;
@ -102,7 +97,6 @@
--color-olive-dark-1: #a3b816;
--color-green-dark-1: #1ea73e;
--color-teal-dark-1: #00a39c;
--color-blue-dark-1: #1e78bb;
--color-violet-dark-1: #5a30b5;
--color-purple-dark-1: #932eb4;
--color-pink-dark-1: #db228a;
@ -115,7 +109,6 @@
--color-olive-dark-2: #91a313;
--color-green-dark-2: #1a9537;
--color-teal-dark-2: #00918a;
--color-blue-dark-2: #1a6aa6;
--color-violet-dark-2: #502aa1;
--color-purple-dark-2: #8229a0;
--color-pink-dark-2: #c21e7b;
@ -178,6 +171,17 @@
--color-orange-badge: #f2711c;
--color-orange-badge-bg: #f2711c1a;
--color-orange-badge-hover-bg: #f2711c4d;
/* A more declarative way for themes */
--thin-lightness: 0.5555;
--regular-chroma: 0.17;
--hue-green: 142deg;
--hue-red: 27deg;
--hue-purple: 298deg;
--hue-violet: 291deg;
--hue-orange: 41deg;
--hue-blue: 247deg;
/* target-based colors */
--color-body: #ffffff;
--color-box-header: #f1f3f5;