Refine Taiga OIDC theming and tests

This commit is contained in:
kevinveenbirkenbach committed 2026-03-28 21:02:06 +01:00
1 parent 6f9a819da4
commit a9b7b66655
6 files changed
+1109 -94

No files matched your search

View File
Whitespace-only changes.
+295 -42
View File
@@ -334,48 +334,66 @@ async function waitForTopLevelLoginRequirement(
throw new Error(errorMessage);
}
test.beforeEach(() => {
expect(taigaBaseUrl, "TAIGA_BASE_URL must be set in the Playwright env file").toBeTruthy();
expect(loginUsername, "LOGIN_USERNAME must be set in the Playwright env file").toBeTruthy();
expect(loginPassword, "LOGIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
if (taigaOauth2Enabled || taigaOidcEnabled) {
expect(oidcIssuerUrl, "OIDC_ISSUER_URL must be set in the Playwright env file").toBeTruthy();
}
});
test("dashboard to taiga login and logout", async ({ page }) => {
test.skip(
!taigaOauth2Enabled && !taigaOidcEnabled,
"Taiga auth flow requires oauth2 or oidc to be enabled."
);
const expectedOidcAuthUrl = `${oidcIssuerUrl.replace(/\/$/, "")}/protocol/openid-connect/auth`;
function getTaigaUrls() {
const expectedTaigaBaseUrl = taigaBaseUrl.replace(/\/$/, "");
const taigaOauth2SignOutUrl = `${expectedTaigaBaseUrl}/oauth2/sign_out`;
const expectedOidcAuthUrl = oidcIssuerUrl
? `${oidcIssuerUrl.replace(/\/$/, "")}/protocol/openid-connect/auth`
: "";
return {
expectedTaigaBaseUrl,
expectedOidcAuthUrl,
taigaOauth2SignOutUrl: `${expectedTaigaBaseUrl}/oauth2/sign_out`,
discoverUrl: `${expectedTaigaBaseUrl}/discover`,
projectsUrl: `${expectedTaigaBaseUrl}/projects`,
projectUrl: `${expectedTaigaBaseUrl}/project/test-project`,
userSettingsUrl: `${expectedTaigaBaseUrl}/user-settings/user-profile`
};
}
async function getComputedStyleValue(locator, propertyName) {
return locator.first().evaluate((element, cssProperty) => getComputedStyle(element).getPropertyValue(cssProperty), propertyName);
}
async function expectGradientBackground(locator, message) {
await expect(locator.first()).toBeVisible({ timeout: 60_000 });
await expect
.poll(
async () => getComputedStyleValue(locator, "background-image"),
{
timeout: 60_000,
message
}
)
.toMatch(/gradient/i);
}
async function loginToTaigaFromDashboard(page) {
const taigaUrls = getTaigaUrls();
const taigaCardLink = page.getByRole("link", { name: "Explore Taiga" });
const taigaIframe = page.locator("#main iframe");
await page.goto("/");
await expect(taigaCardLink).toBeVisible({ timeout: 60_000 });
await taigaCardLink.click();
await expect(taigaIframe).toBeVisible();
await expect(taigaIframe).toBeVisible({ timeout: 60_000 });
const taigaFrame = taigaIframe.contentFrame();
if (taigaOauth2Enabled) {
await waitForFrameUrl(
taigaIframe,
expectedOidcAuthUrl,
taigaUrls.expectedOidcAuthUrl,
60_000,
`Expected Taiga iframe to navigate to Keycloak auth via oauth2-proxy: ${expectedOidcAuthUrl}`
`Expected Taiga iframe to navigate to Keycloak auth via oauth2-proxy: ${taigaUrls.expectedOidcAuthUrl}`
);
} else {
const initialAuthState = await waitForInitialTaigaAuthState(
page,
taigaIframe,
taigaFrame,
expectedTaigaBaseUrl,
expectedOidcAuthUrl,
taigaUrls.expectedTaigaBaseUrl,
taigaUrls.expectedOidcAuthUrl,
60_000,
"Expected Taiga iframe to expose either the Taiga OIDC entry point or the Keycloak login page"
);
@@ -384,9 +402,9 @@ test("dashboard to taiga login and logout", async ({ page }) => {
await initialAuthState.locator.click();
await waitForFrameUrl(
taigaIframe,
expectedOidcAuthUrl,
taigaUrls.expectedOidcAuthUrl,
60_000,
`Expected Taiga OIDC login to navigate to Keycloak auth: ${expectedOidcAuthUrl}`
`Expected Taiga OIDC login to navigate to Keycloak auth: ${taigaUrls.expectedOidcAuthUrl}`
);
}
}
@@ -404,15 +422,15 @@ test("dashboard to taiga login and logout", async ({ page }) => {
await waitForFrameUrl(
taigaIframe,
expectedTaigaBaseUrl,
taigaUrls.expectedTaigaBaseUrl,
60_000,
`Expected Taiga iframe to redirect back to Taiga after Keycloak login: ${expectedTaigaBaseUrl}`
`Expected Taiga iframe to redirect back to Taiga after Keycloak login: ${taigaUrls.expectedTaigaBaseUrl}`
);
if (taigaOauth2Enabled) {
await waitForOauth2ProxyCookie(
page,
expectedTaigaBaseUrl,
taigaUrls.expectedTaigaBaseUrl,
true,
60_000,
"Expected Taiga to establish an oauth2-proxy session after the Keycloak login redirect"
@@ -425,28 +443,41 @@ test("dashboard to taiga login and logout", async ({ page }) => {
"Timed out waiting for a signed-in Taiga shell after the Keycloak login redirect"
);
return {
...taigaUrls,
taigaFrame,
taigaIframe
};
}
async function logoutFromTaigaAndDashboard(page, session) {
if (taigaOauth2Enabled) {
await page.goto(taigaOauth2SignOutUrl);
await page.goto(session.taigaOauth2SignOutUrl);
await waitForOauth2ProxyCookie(
page,
expectedTaigaBaseUrl,
session.expectedTaigaBaseUrl,
false,
60_000,
"Expected Taiga oauth2-proxy session cookie to be cleared after /oauth2/sign_out"
);
} else {
await tryLogoutFromTaiga(page, taigaFrame);
await page.goto(session.expectedTaigaBaseUrl);
await waitForAuthenticatedTaigaShell(
page,
60_000,
"Timed out waiting for the top-level signed-in Taiga shell before logout"
);
await tryLogoutFromTaiga(page, page);
}
await page.goto("/");
await logoutFromDashboardIfNeeded(page);
await page.goto(expectedTaigaBaseUrl);
await page.goto(session.expectedTaigaBaseUrl);
const loggedOutState = await waitForTopLevelLoginRequirement(
page,
expectedTaigaBaseUrl,
expectedOidcAuthUrl,
session.expectedTaigaBaseUrl,
session.expectedOidcAuthUrl,
60_000,
"Expected logged-out access to Taiga to require a fresh login"
);
@@ -462,14 +493,236 @@ test("dashboard to taiga login and logout", async ({ page }) => {
message: "Expected top-level Taiga re-entry to stay on the Keycloak login page after logout"
}
)
.toContain(expectedOidcAuthUrl);
} else {
expect(["keycloak", "taiga-login-page", "taiga-oidc-entry"]).toContain(loggedOutState.kind);
.toContain(session.expectedOidcAuthUrl);
return;
}
if (loggedOutState.kind === "keycloak") {
await expect(page.locator("input[name='username'], input#username").first()).toBeVisible({ timeout: 60_000 });
} else {
await expect(loggedOutState.locator).toBeVisible({ timeout: 60_000 });
expect(["keycloak", "taiga-login-page", "taiga-oidc-entry"]).toContain(loggedOutState.kind);
if (loggedOutState.kind === "keycloak") {
await expect(page.locator("input[name='username'], input#username").first()).toBeVisible({ timeout: 60_000 });
return;
}
await expect(loggedOutState.locator).toBeVisible({ timeout: 60_000 });
}
async function reachTopLevelTaigaAuthEntry(page, taigaUrls, timeout, errorMessage) {
const deadline = Date.now() + timeout;
let loginClicked = false;
while (Date.now() < deadline) {
const currentUrl = page.url();
const keycloakUsernameField = page.locator("input[name='username'], input#username");
if (
currentUrl.includes(taigaUrls.expectedOidcAuthUrl) &&
(await keycloakUsernameField.first().isVisible().catch(() => false))
) {
return { kind: "keycloak" };
}
if (currentUrl.includes(taigaUrls.expectedTaigaBaseUrl)) {
const oidcEntry = await findFirstVisible(getOidcEntryLocators(page));
if (oidcEntry) {
return { kind: "taiga-oidc-entry", locator: oidcEntry };
}
const loginEntry = await findFirstVisible([
page.getByRole("link", { name: /^Login$/i }),
page.getByRole("button", { name: /^Login$/i })
]);
if (loginEntry && !loginClicked) {
await loginEntry.click();
loginClicked = true;
await page.waitForTimeout(500);
continue;
}
const visibleLocalLoginField = await findFirstVisible([
page.locator("input[name='username'], input#username"),
page.locator("input[name='password'], input#password")
]);
if (visibleLocalLoginField) {
await page.waitForTimeout(1_000);
const persistedLocalLoginField = await findFirstVisible([
page.locator("input[name='username'], input#username"),
page.locator("input[name='password'], input#password")
]);
if (persistedLocalLoginField) {
return { kind: "taiga-local-login-visible", locator: persistedLocalLoginField };
}
}
}
await page.waitForTimeout(500);
}
throw new Error(errorMessage);
}
test.beforeEach(() => {
expect(taigaBaseUrl, "TAIGA_BASE_URL must be set in the Playwright env file").toBeTruthy();
expect(loginUsername, "LOGIN_USERNAME must be set in the Playwright env file").toBeTruthy();
expect(loginPassword, "LOGIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
if (taigaOauth2Enabled || taigaOidcEnabled) {
expect(oidcIssuerUrl, "OIDC_ISSUER_URL must be set in the Playwright env file").toBeTruthy();
}
});
test("dashboard to taiga login and logout", async ({ page }) => {
test.skip(
!taigaOauth2Enabled && !taigaOidcEnabled,
"Taiga auth flow requires oauth2 or oidc to be enabled."
);
const session = await loginToTaigaFromDashboard(page);
await logoutFromTaigaAndDashboard(page, session);
});
test("taiga public discover keeps the themed surface and hides local login fields when oidc is active", async ({ page }) => {
test.skip(!taigaOidcEnabled, "This scenario only applies when the Taiga OIDC integration is enabled.");
const taigaUrls = getTaigaUrls();
await page.goto(taigaUrls.discoverUrl);
await expect(page.getByRole("heading", { name: /discover projects/i })).toBeVisible({ timeout: 60_000 });
await expectGradientBackground(
page.locator("div.master"),
"Expected the Taiga discover page to use the themed master background"
);
await expectGradientBackground(
page.locator(".discover-header form"),
"Expected the Taiga discover search form to use the themed surface"
);
await expectGradientBackground(
page.locator(".discover-header input[type='text']"),
"Expected the Taiga discover search input to use the themed input surface"
);
const authState = await reachTopLevelTaigaAuthEntry(
page,
taigaUrls,
60_000,
"Expected Taiga to expose either the OIDC entry point or the Keycloak login page"
);
expect(authState.kind).not.toBe("taiga-local-login-visible");
if (authState.kind === "taiga-oidc-entry") {
await expect(authState.locator).toBeVisible({ timeout: 60_000 });
await expect
.poll(
async () => page.locator("input[name='username'], input#username").first().isVisible().catch(() => false),
{
timeout: 10_000,
message: "Expected the local Taiga username field to stay hidden when OIDC is active"
}
)
.toBe(false);
await expect
.poll(
async () => page.locator("input[name='password'], input#password").first().isVisible().catch(() => false),
{
timeout: 10_000,
message: "Expected the local Taiga password field to stay hidden when OIDC is active"
}
)
.toBe(false);
await expect
.poll(
async () => page.getByText(/^or login with$/i).first().isVisible().catch(() => false),
{
timeout: 10_000,
message: "Expected the legacy Taiga OIDC helper text to stay hidden when OIDC is active"
}
)
.toBe(false);
await expect
.poll(
async () => page.getByText(/^forgot it\?$/i).first().isVisible().catch(() => false),
{
timeout: 10_000,
message: "Expected the legacy Taiga password reset helper text to stay hidden when OIDC is active"
}
)
.toBe(false);
await authState.locator.click();
await expect
.poll(
async () => page.url(),
{
timeout: 60_000,
message: `Expected the Taiga OIDC entry to navigate to Keycloak: ${taigaUrls.expectedOidcAuthUrl}`
}
)
.toContain(taigaUrls.expectedOidcAuthUrl);
}
await expect(page.locator("input[name='username'], input#username").first()).toBeVisible({ timeout: 60_000 });
});
test("taiga themed routes stay aligned across discover projects project details and user settings", async ({ page }) => {
const session = await loginToTaigaFromDashboard(page);
const routeChecks = [
{
url: session.discoverUrl,
ready: page.getByRole("heading", { name: /discover projects/i }),
surface: page.locator(".discover-header form"),
field: page.locator(".discover-header input[type='text']")
},
{
url: session.projectsUrl,
ready: page.getByRole("heading", { name: /my projects/i }),
surface: page.locator(".project-list-wrapper .list-itemtype-project")
},
{
url: session.projectUrl,
ready: page.getByText(/hello world/i).first()
},
{
url: session.userSettingsUrl,
ready: page.getByRole("heading", { name: /user settings/i }),
surface: page.locator(".menu-secondary")
}
];
for (const routeCheck of routeChecks) {
await page.goto(routeCheck.url);
await expect(routeCheck.ready).toBeVisible({ timeout: 60_000 });
await expectGradientBackground(
page.locator("div.master"),
`Expected the Taiga master background to stay themed on ${routeCheck.url}`
);
if (routeCheck.surface) {
await expectGradientBackground(
routeCheck.surface,
`Expected the primary Taiga surface to stay themed on ${routeCheck.url}`
);
}
if (routeCheck.field) {
await expectGradientBackground(
routeCheck.field,
`Expected the Taiga input surface to stay themed on ${routeCheck.url}`
);
}
if (routeCheck.action) {
await expectGradientBackground(
routeCheck.action,
`Expected the main Taiga action button to stay themed on ${routeCheck.url}`
);
}
}
await logoutFromTaigaAndDashboard(page, session);
});
+5 -9
View File
@@ -1,10 +1,6 @@
galaxy_info:
author: Kevin Veen-Birkenbach
description: 'Supercharge your project management with Taiga—a dynamic, agile tool designed for teams that thrive on creativity
and collaboration. Experience a vibrant interface, robust task tracking, and an energetic platform that drives your projects
to success.
'
description: 'Supercharge your project management with Taiga—a dynamic, agile tool designed for teams that thrive on creativity and collaboration. Experience a vibrant interface, robust task tracking, and an energetic platform that drives your projects to success.'
license: Infinito.Nexus Community License (Non-Commercial)
license_url: https://s.infinito.nexus/license
company: |
@@ -15,7 +11,7 @@ galaxy_info:
platforms:
- name: Archlinux
versions:
- rolling
- rolling
galaxy_tags:
- taiga
- docker
@@ -30,6 +26,6 @@ galaxy_info:
logo:
class: fa-solid fa-tasks
run_after:
- web-app-matomo
- web-app-keycloak
lifecycle: alpha
- web-app-matomo
- web-app-keycloak
lifecycle: beta
+62 -1
View File
@@ -1 +1,62 @@
{% include 'templates/roles/web-app/templates/javascripts/sso_warning.js.j2' %}
{% if TAIGA_OIDC_ENABLED | bool %}
(function () {
const hiddenLabelPatterns = [
/^or login with$/i,
/^forgot it\?$/i,
/^not registered yet\? create your free account here$/i,
];
function hideElement(element) {
if (!element) {
return;
}
element.style.setProperty("display", "none", "important");
}
function hideDefaultLoginForm() {
document.querySelectorAll("input").forEach((input) => {
const placeholder = (input.getAttribute("placeholder") || "").toLowerCase();
const isDefaultLoginField =
placeholder.includes("username") ||
placeholder.includes("password") ||
input.type === "password";
if (!isDefaultLoginField) {
return;
}
hideElement(input.closest("form") || input);
});
document.querySelectorAll("body *").forEach((node) => {
if (node.children.length > 0) {
return;
}
const text = (node.textContent || "").replace(/\s+/g, " ").trim();
if (!hiddenLabelPatterns.some((pattern) => pattern.test(text))) {
return;
}
hideElement(node);
});
}
function run() {
hideDefaultLoginForm();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", run, { once: true });
} else {
run();
}
new MutationObserver(run).observe(document.documentElement, {
childList: true,
subtree: true,
});
})();
{% else %}
// Taiga OIDC is disabled, so no custom login UI cleanup is needed.
{% endif %}
@@ -7,6 +7,7 @@
"tribeHost": null,
"importers": [],
"gravatar": false,
"defaultLoginEnabled": {{ 'false' if TAIGA_OIDC_ENABLED | bool else 'true' }},
"rtlLanguages": ["fa"]{% if TAIGA_LDAP_ENABLED | bool %},
"loginFormType": "ldap"{% endif %}{% if TAIGA_OIDC_ENABLED | bool %},
"contribPlugins": ["/plugins/oidc-auth/oidc-auth.json"],
+746 -42
View File
@@ -1,67 +1,771 @@
:host, :root {
--color-01-solid-primary: var(--color-01-83);
--color-01-link-primary: var(--color-01-44);
--color-01-link-tertiary: var(--color-01-45);
--color-01-gray100: var(--color-01-97);
--color-01-gray200: var(--color-01-93);
--color-01-gray300: var(--color-01-90);
--color-01-gray400: var(--color-01-86);
--color-01-black600: var(--color-01-34);
--color-01-black700: var(--color-01-30);
--color-01-black800: var(--color-01-26);
--color-01-black900: var(--color-01-21);
--color-01-black: var(--color-01-01);
:host,
:root {
--infinito-taiga-canvas: var(--color-01-08);
--infinito-taiga-surface: rgba(var(--color-01-rgb-10), 0.96);
--infinito-taiga-surface-strong: rgba(var(--color-01-rgb-14), 0.98);
--infinito-taiga-surface-soft: rgba(var(--color-01-rgb-18), 0.94);
--infinito-taiga-surface-card: rgba(var(--color-01-rgb-16), 0.9);
--infinito-taiga-surface-card-hover: rgba(var(--color-01-rgb-20), 0.94);
--infinito-taiga-surface-cool: rgba(var(--color-06-rgb-18), 0.94);
--infinito-taiga-surface-cool-strong: rgba(var(--color-06-rgb-24), 0.97);
--infinito-taiga-overlay: rgba(var(--color-01-rgb-03), 0.78);
--infinito-taiga-border: rgba(var(--color-02-rgb-60), 0.28);
--infinito-taiga-border-strong: rgba(var(--color-02-rgb-70), 0.46);
--infinito-taiga-line: rgba(var(--color-01-rgb-97), 0.14);
--infinito-taiga-shadow: 0 18px 40px rgba(var(--color-01-rgb-01), 0.28);
--infinito-taiga-glow: 0 0 0 1px rgba(var(--color-03-rgb-58), 0.08), 0 18px 40px rgba(var(--color-01-rgb-01), 0.24);
--infinito-taiga-heading: var(--color-01-99);
--infinito-taiga-text: var(--color-01-95);
--infinito-taiga-text-soft: var(--color-01-88);
--infinito-taiga-text-muted: var(--color-01-76);
--infinito-taiga-link: var(--color-04-62);
--infinito-taiga-link-hover: var(--color-04-72);
--infinito-taiga-warm: var(--color-02-56);
--infinito-taiga-warm-hover: var(--color-02-64);
--infinito-taiga-cool: var(--color-04-60);
--infinito-taiga-cool-hover: var(--color-04-68);
--infinito-taiga-success: var(--color-05-60);
--infinito-taiga-success-hover: var(--color-05-68);
--infinito-taiga-danger: var(--color-07-58);
--infinito-taiga-danger-hover: var(--color-07-66);
--infinito-taiga-badge: var(--color-03-60);
--infinito-taiga-badge-soft: rgba(var(--color-03-rgb-60), 0.16);
--color-01-solid-primary: var(--infinito-taiga-cool);
--color-01-link-primary: var(--infinito-taiga-link);
--color-01-link-tertiary: var(--infinito-taiga-link-hover);
--color-01-gray100: var(--color-06-96);
--color-01-gray200: var(--color-06-90);
--color-01-gray300: var(--color-06-82);
--color-01-gray400: var(--color-06-68);
--color-01-black600: var(--color-06-34);
--color-01-black700: var(--color-06-26);
--color-01-black800: var(--color-01-14);
--color-01-black900: var(--color-01-08);
--color-01-black: var(--color-01-03);
--color-01-white: var(--color-01-99);
/* Alias role-local rgb tokens to the generated shared naming scheme. */
--color-01-rgb-01: var(--color-rgb-01-01);
--color-01-rgb-03: var(--color-rgb-01-03);
--color-01-rgb-08: var(--color-rgb-01-08);
--color-01-rgb-10: var(--color-rgb-01-10);
--color-01-rgb-12: var(--color-rgb-01-12);
--color-01-rgb-14: var(--color-rgb-01-14);
--color-01-rgb-16: var(--color-rgb-01-16);
--color-01-rgb-18: var(--color-rgb-01-18);
--color-01-rgb-20: var(--color-rgb-01-20);
--color-01-rgb-24: var(--color-rgb-01-24);
--color-01-rgb-90: var(--color-rgb-01-90);
--color-01-rgb-95: var(--color-rgb-01-95);
--color-01-rgb-97: var(--color-rgb-01-97);
--color-01-rgb-99: var(--color-rgb-01-99);
--color-02-rgb-42: var(--color-rgb-02-42);
--color-02-rgb-52: var(--color-rgb-02-52);
--color-02-rgb-56: var(--color-rgb-02-56);
--color-02-rgb-58: var(--color-rgb-02-58);
--color-02-rgb-60: var(--color-rgb-02-60);
--color-02-rgb-70: var(--color-rgb-02-70);
--color-03-rgb-36: var(--color-rgb-03-36);
--color-03-rgb-56: var(--color-rgb-03-56);
--color-03-rgb-58: var(--color-rgb-03-58);
--color-03-rgb-60: var(--color-rgb-03-60);
--color-04-rgb-58: var(--color-rgb-04-58);
--color-04-rgb-60: var(--color-rgb-04-60);
--color-04-rgb-62: var(--color-rgb-04-62);
--color-05-rgb-54: var(--color-rgb-05-54);
--color-05-rgb-60: var(--color-rgb-05-60);
--color-06-rgb-16: var(--color-rgb-06-16);
--color-06-rgb-18: var(--color-rgb-06-18);
--color-06-rgb-20: var(--color-rgb-06-20);
--color-06-rgb-24: var(--color-rgb-06-24);
--color-06-rgb-26: var(--color-rgb-06-26);
--color-06-rgb-30: var(--color-rgb-06-30);
--color-06-rgb-32: var(--color-rgb-06-32);
--color-06-rgb-34: var(--color-rgb-06-34);
--color-06-rgb-40: var(--color-rgb-06-40);
--color-06-rgb-42: var(--color-rgb-06-42);
--color-06-rgb-50: var(--color-rgb-06-50);
--color-07-rgb-48: var(--color-rgb-07-48);
--color-07-rgb-58: var(--color-rgb-07-58);
}
section.main.kanban{
background-color: transparent;
div.master,
.blocked-project-detail,
.transfer-project {
background:
radial-gradient(circle at top right, rgba(var(--color-04-rgb-58), 0.1), transparent 30%),
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-01-rgb-08), 0.98),
rgba(var(--color-01-rgb-10), 0.97),
rgba(var(--color-01-rgb-14), 0.94)
);
color: var(--infinito-taiga-text);
}
div.master, div.kanban-header, div.kanban-table-inner, section.kanban button,a.dropdown-project-list-projects{
background-color: var(--color-01-92);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-87), var(--color-01-92), var(--color-01-93), var(--color-01-97));
color: var(--color-01-40);
.wrapper,
.main,
.profile .main,
section.main.kanban {
background-color: transparent;
color: var(--infinito-taiga-text);
}
section.kanban h1, section.kanban h2{
color: var(--color-01-40);
div.master h1,
div.master h2,
div.master h3,
div.master h4,
div.master h5,
div.master h6 {
color: var(--infinito-taiga-heading);
}
.home-project {
background: var(--color-01-88);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-83), var(--color-01-88), var(--color-01-89), var(--color-01-93));
border-color: var(--color-01-60);
color: var(--color-01-12);
div.master h1 a,
div.master h2 a,
div.master h3 a,
div.master h4 a,
div.master h5 a,
div.master h6 a,
div.master a,
div.master a:visited {
color: var(--infinito-taiga-link);
}
.home-wrapper .title-bar {
background: var(--color-01-75);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-70), var(--color-01-75), var(--color-01-76), var(--color-01-80));
div.master a:hover,
div.master a:visited:hover,
.btn-link:hover,
.btn-link[variant="icon"]:hover {
color: var(--infinito-taiga-link-hover);
}
.kanban.swimlane .kanban-header {
background: none;
div.master hr,
.profile-bar .profile-stats,
.profile-timeline,
.profile-timeline .activity-item,
.project-details .get-feedback,
.project-details .looking-for-people,
.transfer-project-detail,
.project-list-wrapper .dropdown-project-list-ul {
border-color: var(--infinito-taiga-line);
}
.menu-secondary,
.menu-secondary li,
.menu-secondary .settings-nav,
.menu-tertiary {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-06-rgb-16), 0.96),
rgba(var(--color-06-rgb-20), 0.97),
rgba(var(--color-06-rgb-24), 0.98)
);
color: var(--infinito-taiga-text-soft);
}
.menu-secondary,
.menu-tertiary {
border-right: 1px solid var(--infinito-taiga-border);
box-shadow: inset -1px 0 0 rgba(var(--color-01-rgb-01), 0.18);
}
.menu-secondary li a,
.menu-tertiary ul li a {
color: var(--infinito-taiga-text-soft);
}
.menu-secondary li:hover,
.menu-secondary li:hover a,
.menu-tertiary ul li:hover,
.menu-tertiary ul li:hover a {
background: rgba(var(--color-04-rgb-58), 0.12);
color: var(--infinito-taiga-heading);
}
.menu-secondary li .active,
.menu-secondary li .active:hover {
background: rgba(var(--color-01-rgb-18), 0.98);
box-shadow: inset 3px 0 0 var(--infinito-taiga-cool);
}
.menu-secondary li .active a,
.menu-secondary li .active:hover a,
.menu-tertiary ul li.active a,
.menu-tertiary ul li.active a:hover {
background: transparent;
color: var(--infinito-taiga-link);
}
.menu-tertiary ul li.active {
background: rgba(var(--color-04-rgb-58), 0.14);
}
.project-list-wrapper .project-list-title,
.project-values-title,
.home-wrapper .title-bar,
div.kanban-header,
.kanban-table-header .task-colum-name {
background-color: var(--color-01-70);
color: var(--color-01-10);
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-02-rgb-42), 0.95),
rgba(var(--color-02-rgb-52), 0.98),
rgba(var(--color-02-rgb-58), 0.97)
);
border: 1px solid var(--infinito-taiga-border);
box-shadow: var(--infinito-taiga-glow);
color: var(--infinito-taiga-heading);
}
input.ng-empty::placeholder,.ng-empty::placeholder {
color: rgba(var(--color-01-rgb-03),0.6);
.project-list-wrapper .project-list-title h1,
.project-values-title h2,
.discover-results-header h2,
.discover-results-header .title,
section.kanban h1,
section.kanban h2 {
color: var(--infinito-taiga-heading);
}
.discover-header form,
.dropdown-project-list .navbar-dropdown,
.lightbox section,
.lightbox form,
.home-project,
.project-list-wrapper .list-itemtype-project,
.project-list-wrapper .gu-mirror,
.single-project,
.highlighted-project,
.featured-project,
.profile-bar,
.project-details .project-details-form-data,
.project-details .project-details-image,
.profile-timeline,
.notification-light {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-01-rgb-12), 0.97),
rgba(var(--color-01-rgb-16), 0.96),
rgba(var(--color-01-rgb-20), 0.94)
);
border: 1px solid var(--infinito-taiga-border);
box-shadow: var(--infinito-taiga-shadow);
color: var(--infinito-taiga-text);
}
.dropdown-project-list .navbar-dropdown:after {
border-bottom-color: rgba(var(--color-01-rgb-12), 0.97);
}
.dropdown-project-list .navbar-dropdown:before {
border-bottom-color: rgba(var(--color-02-rgb-60), 0.22);
}
.project-list-wrapper .list-itemtype-project:hover,
.lightbox-select-user .user-list-single.selected,
.lightbox-select-user .user-list-single:hover,
.lightbox-create-edit .pop-users .user-list-single.selected,
.lightbox-create-edit .pop-users .user-list-single:hover,
.ticket-user-list .user-list-single:hover:not(.read-only),
.dropdown-project-list .dropdown-project-list-link.active,
.dropdown-project-list .dropdown-project-list-link:hover,
.create-project-selector li:hover {
background: rgba(var(--color-04-rgb-58), 0.12);
border-color: rgba(var(--color-04-rgb-60), 0.34);
}
.project-list-wrapper .gu-transit {
background: rgba(var(--color-02-rgb-56), 0.14);
border: 1px dashed var(--infinito-taiga-border-strong);
}
.project-list-wrapper .help-area,
.project-details .private-or-public,
.transfer-project-statistics .transfer-project-members,
.transfer-project-statistics .transfer-project-private,
.profile-bar h2,
.profile-bar .username,
.profile-bar .location,
.profile-timeline .activity-item .activity-date,
.profile-timeline .activity-item .activity-info,
.discover-header .project-number,
.discover-results-header svg,
.dropdown-project-list .dropdown-project-list-desc {
color: var(--infinito-taiga-text-muted);
}
.project-details .private-or-public svg,
.transfer-project-statistics svg,
.discover-results-header svg,
.discover-header .search-button,
.dropdown-project-list .dropdown-project-list-projects .icon-folder svg,
.project-list-wrapper .icon-badge,
.featured-project .project-card-header .icon-badge,
.highlighted-project .project-logo .icon-badge,
.home-project .project-card-header .icon-badge,
.single-project .involved-team .icon-badge,
.table-team .icon-badge {
fill: var(--infinito-taiga-cool);
stroke: var(--infinito-taiga-cool);
}
.dropdown-project-list .dropdown-project-list-ul {
border-bottom-color: rgba(var(--color-04-rgb-60), 0.32);
}
.dropdown-project-list .dropdown-project-list-projects,
.dropdown-project-list .create-project-btn span,
.dropdown-project-list h2,
.dropdown-project-list .see-more-projects-btn {
color: var(--infinito-taiga-heading);
}
.dropdown-project-list .dropdown-project-list-projects span {
color: var(--infinito-taiga-link);
}
.dropdown-project-list .dropdown-project-list-link.active,
.dropdown-project-list .dropdown-project-list-link:hover {
color: var(--infinito-taiga-link);
}
.dropdown-project-list:hover .dropdown-project-list-projects,
.dropdown-project-list.active .dropdown-project-list-projects {
background: rgba(var(--color-04-rgb-58), 0.12);
}
.dropdown-project-list:hover .dropdown-project-list-projects span,
.dropdown-project-list.active .dropdown-project-list-projects span {
color: var(--infinito-taiga-link);
}
.dropdown-project-list .create-project-btn svg {
fill: var(--infinito-taiga-cool);
stroke: var(--infinito-taiga-cool);
}
.dropdown-project-list .create-project-btn:hover span,
.dropdown-project-list .create-project-btn:hover svg {
color: var(--infinito-taiga-heading);
fill: var(--infinito-taiga-heading);
stroke: var(--infinito-taiga-heading);
}
div.master input[type="date"],
div.master input[type="email"],
div.master input[type="number"],
div.master input[type="password"],
div.master input[type="search"],
div.master input[type="text"],
div.master input[type="url"],
div.master select,
div.master textarea,
.discover-header input[type="text"],
.lightbox-generic-bulk .bulk-status-selector {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-01-rgb-16), 0.96),
rgba(var(--color-01-rgb-20), 0.94),
rgba(var(--color-01-rgb-24), 0.92)
);
border: 2px solid var(--infinito-taiga-border-strong);
color: var(--infinito-taiga-text);
box-shadow: inset 0 1px 0 rgba(var(--color-01-rgb-99), 0.04);
}
div.master input[type="date"]::-webkit-input-placeholder,
div.master input[type="email"]::-webkit-input-placeholder,
div.master input[type="number"]::-webkit-input-placeholder,
div.master input[type="password"]::-webkit-input-placeholder,
div.master input[type="search"]::-webkit-input-placeholder,
div.master input[type="text"]::-webkit-input-placeholder,
div.master input[type="url"]::-webkit-input-placeholder,
div.master select::-webkit-input-placeholder,
div.master textarea::-webkit-input-placeholder,
input.ng-empty::placeholder,
.ng-empty::placeholder {
color: rgba(var(--color-01-rgb-90), 0.62);
}
div.master input[type="date"]::-moz-placeholder,
div.master input[type="email"]::-moz-placeholder,
div.master input[type="number"]::-moz-placeholder,
div.master input[type="password"]::-moz-placeholder,
div.master input[type="search"]::-moz-placeholder,
div.master input[type="text"]::-moz-placeholder,
div.master input[type="url"]::-moz-placeholder,
div.master select::-moz-placeholder,
div.master textarea::-moz-placeholder {
color: rgba(var(--color-01-rgb-90), 0.62);
}
div.master input[type="date"]:focus,
div.master input[type="email"]:focus,
div.master input[type="number"]:focus,
div.master input[type="password"]:focus,
div.master input[type="search"]:focus,
div.master input[type="text"]:focus,
div.master input[type="url"]:focus,
div.master select:focus,
div.master textarea:focus,
.discover-header input[type="text"]:focus,
.lightbox-generic-bulk .bulk-status-selector.active {
border-color: var(--infinito-taiga-cool);
box-shadow:
inset 0 1px 0 rgba(var(--color-01-rgb-99), 0.04),
0 0 0 3px rgba(var(--color-04-rgb-60), 0.18);
outline: 0;
}
div.master input[type="date"].checksley-error,
div.master input[type="email"].checksley-error,
div.master input[type="number"].checksley-error,
div.master input[type="password"].checksley-error,
div.master input[type="search"].checksley-error,
div.master input[type="text"].checksley-error,
div.master input[type="url"].checksley-error,
div.master select.checksley-error,
div.master textarea.checksley-error {
border-color: rgba(var(--color-07-rgb-58), 0.42);
}
div.master input[type="date"].checksley-error:focus,
div.master input[type="email"].checksley-error:focus,
div.master input[type="number"].checksley-error:focus,
div.master input[type="password"].checksley-error:focus,
div.master input[type="search"].checksley-error:focus,
div.master input[type="text"].checksley-error:focus,
div.master input[type="url"].checksley-error:focus,
div.master select.checksley-error:focus,
div.master textarea.checksley-error:focus {
border-color: var(--infinito-taiga-danger);
box-shadow:
inset 0 1px 0 rgba(var(--color-01-rgb-99), 0.04),
0 0 0 3px rgba(var(--color-07-rgb-58), 0.18);
}
.discover-header .search-button:hover,
.discover-search .discover-header .search-button:hover {
fill: var(--infinito-taiga-link-hover);
}
.btn-big,
.btn-board,
.btn-filter,
.btn-icon,
.btn-small,
.button-green,
.button-primary,
.button-secondary,
.button-gray,
.button-gray-light,
.button-red,
.button-red-light {
border: 1px solid transparent;
box-shadow: 0 10px 24px rgba(var(--color-01-rgb-01), 0.18);
}
.btn-big,
.btn-board,
.btn-filter,
.btn-icon,
.btn-small {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--color-02-48),
var(--infinito-taiga-warm),
var(--color-02-64)
);
color: var(--infinito-taiga-heading);
}
.btn-big:hover,
.btn-board:hover,
.btn-filter:hover,
.btn-icon:hover,
.btn-small:hover,
.btn-big[variant="secondary"]:hover,
.btn-board[variant="secondary"]:hover,
.btn-filter[variant="secondary"]:hover,
.btn-icon[variant="secondary"]:hover,
.btn-small[variant="secondary"]:hover {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--infinito-taiga-warm),
var(--infinito-taiga-warm-hover),
var(--color-02-72)
);
color: var(--infinito-taiga-heading);
}
.btn-small[variant="primary"],
.btn-big[variant="primary"],
.btn-board[variant="primary"],
.btn-filter[variant="primary"],
.btn-icon[variant="primary"],
.button-green,
.button-primary {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--color-04-52),
var(--infinito-taiga-success),
var(--color-05-66)
);
color: var(--color-01-04);
}
.btn-small[variant="primary"]:hover,
.btn-big[variant="primary"]:hover,
.btn-board[variant="primary"]:hover,
.btn-filter[variant="primary"]:hover,
.btn-icon[variant="primary"]:hover,
.button-green:hover,
.button-primary:hover {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--infinito-taiga-success),
var(--infinito-taiga-success-hover),
var(--color-05-74)
);
color: var(--color-01-03);
}
.btn-small[variant="secondary"],
.btn-big[variant="secondary"],
.btn-board[variant="secondary"],
.btn-filter[variant="secondary"],
.btn-icon[variant="secondary"],
.button-secondary,
.button-gray,
.button-gray-light {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-06-rgb-24), 0.96),
rgba(var(--color-06-rgb-30), 0.97),
rgba(var(--color-06-rgb-34), 0.98)
);
border-color: rgba(var(--color-06-rgb-50), 0.34);
color: var(--infinito-taiga-text);
}
.btn-small[variant="destructive"],
.btn-big[variant="destructive"],
.btn-board[variant="destructive"],
.btn-filter[variant="destructive"],
.btn-icon[variant="destructive"],
.button-red,
.button-red-light {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--color-07-48),
var(--infinito-taiga-danger),
var(--color-07-66)
);
color: var(--infinito-taiga-heading);
}
.btn-small[variant="destructive"]:hover,
.btn-big[variant="destructive"]:hover,
.btn-board[variant="destructive"]:hover,
.btn-filter[variant="destructive"]:hover,
.btn-icon[variant="destructive"]:hover,
.button-red:hover,
.button-red-light:hover {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
var(--infinito-taiga-danger),
var(--infinito-taiga-danger-hover),
var(--color-07-72)
);
color: var(--infinito-taiga-heading);
}
.btn-big:disabled,
.btn-board:disabled,
.btn-filter:disabled,
.btn-icon:disabled,
.btn-small:disabled,
.button-green[disabled],
.button-primary[disabled],
.button-secondary[disabled],
.button-gray[disabled],
.button-gray-light[disabled],
.button-red[disabled],
.button-red-light[disabled],
.button.disabled,
.button[disabled],
.disabled.button-green,
.disabled.button-primary,
.disabled.button-secondary,
.disabled.button-gray,
.disabled.button-gray-light,
.disabled.button-red,
.disabled.button-red-light {
background: rgba(var(--color-06-rgb-26), 0.8);
border-color: rgba(var(--color-06-rgb-40), 0.24);
color: rgba(var(--color-01-rgb-95), 0.5);
box-shadow: none;
}
.btn-link,
.btn-link[variant="icon"],
.project-details .use-default-image,
.project-details .delete-project,
.lightbox-sprint-add-edit .delete-sprint,
.transfer-project-comment-link,
.lightbox-select-user .add-comment a {
color: var(--infinito-taiga-link);
}
.btn-link tg-svg,
.btn-link[variant="icon"] tg-svg {
fill: currentColor;
}
.lightbox {
background: rgba(var(--color-01-rgb-97), .93);
color: var(--color-01-03);
background: var(--infinito-taiga-overlay);
color: var(--infinito-taiga-text);
}
.lightbox h2,
.lightbox-generic-error h2,
.lightbox-generic-loading h2,
.lightbox-generic-success h2 {
color: var(--infinito-taiga-heading);
}
.lightbox .close,
.lightbox-select-user .add-comment .icon-close,
.transfer-project-comment-header .icon-close {
color: var(--infinito-taiga-text-muted);
}
.lightbox-select-user .add-comment .icon-close,
.transfer-project-comment-header .icon-close {
fill: currentColor;
}
.lightbox-generic-bulk .bulk-status-option-wrapper {
background: var(--infinito-taiga-surface-strong);
border: 1px solid var(--infinito-taiga-border);
box-shadow: var(--infinito-taiga-shadow);
}
.lightbox-generic-bulk .bulk-status-option,
.lightbox-generic-bulk .bulk-status-option.selected {
color: var(--infinito-taiga-link);
}
.lightbox-generic-bulk .bulk-status-option:hover {
background: rgba(var(--color-04-rgb-60), 0.12);
}
.lightbox-generic-form .existing-or-new-selector input + label,
.lightbox-relate-to-epic .related-with-selector input + label {
background: rgba(var(--color-06-rgb-26), 0.7);
border: 1px solid rgba(var(--color-06-rgb-42), 0.3);
color: var(--infinito-taiga-text-soft);
}
.lightbox-generic-form .existing-or-new-selector input + label:hover,
.lightbox-relate-to-epic .related-with-selector input + label:hover {
background: rgba(var(--color-04-rgb-60), 0.18);
}
.lightbox-generic-form .existing-or-new-selector input:checked + label,
.lightbox-relate-to-epic .related-with-selector input:checked + label {
background: var(--infinito-taiga-cool);
color: var(--color-01-04);
}
.notification-message-success {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-05-rgb-54), 0.96),
rgba(var(--color-05-rgb-60), 0.98),
rgba(var(--color-04-rgb-62), 0.96)
);
box-shadow: var(--infinito-taiga-shadow);
}
.notification-message-success .warning,
.notification-message-error .warning,
.notification-light .warning,
.notification-light,
.notification-light p {
color: var(--infinito-taiga-heading);
}
.notification-message-error {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-07-rgb-48), 0.96),
rgba(var(--color-07-rgb-58), 0.98),
rgba(var(--color-03-rgb-56), 0.95)
);
}
.notification-message-error .icon-error,
.notification-message-error .icon-close,
.notification-light .close svg {
fill: currentColor;
}
.notification-light {
background:
linear-gradient(
{{ CSS_GRADIENT_ANGLE }}deg,
rgba(var(--color-06-rgb-24), 0.96),
rgba(var(--color-06-rgb-32), 0.98),
rgba(var(--color-03-rgb-36), 0.92)
);
}
.project-details .project-details-image .image,
.single-project .project-logo img,
.highlighted-project .project-logo img {
border: 1px solid rgba(var(--color-01-rgb-99), 0.08);
box-shadow: var(--infinito-taiga-shadow);
}
.profile-bar .profile-edition {
background: rgba(var(--color-01-rgb-03), 0.72);
}
.profile-bar .flag path,
.profile-bar .location path,
.project-details .get-feedback-inner svg,
.project-details .looking-for-people-selector svg {
fill: var(--infinito-taiga-text-muted);
}
.project-values-settings svg.icon-trash,
.profile-bar .flag:hover path,
.lightbox-sprint-add-edit .delete-sprint:hover,
.lightbox-sprint-add-edit .delete-sprint:hover .icon,
.project-details .use-default-image:hover,
.project-details .delete-project:hover {
color: var(--infinito-taiga-danger);
fill: var(--infinito-taiga-danger);
}
.kanban.swimlane .kanban-header {
background: none;
border: 0;
box-shadow: none;
}
.kanban-filter tg-filter {
border-color: var(--color-01-70);
}
.discover-header {
background: none;
border-color: var(--infinito-taiga-border-strong);
}