feat: add RGB handler for alpine theme (#427)

* add handlers for individual Channels

* add notes

* patch fix theme overrides for adaptive colour

* idk

* Update package.json

* fix issue spelling

* Update OpenWhatsNewPopup.ts

* Update OpenWhatsNewPopup.ts

* fix: remove debug line from .gitignore

* chore: fix up patch notes to be a bit more user friendly

* chore: finalise patch notes and fix grammer

* Add empty line to .gitignore

---------

Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
This commit is contained in:
StroepWafel
2026-04-29 09:54:26 +09:30
committed by GitHub
parent 01cd5d1428
commit 7a4fa1e5bf
7 changed files with 110 additions and 24 deletions
+1
View File
@@ -23,3 +23,4 @@ betterseqtaplus-safari/
.env .env
.env.submit .env.submit
dependency-graph.svg dependency-graph.svg
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "betterseqtaplus", "name": "betterseqtaplus",
"version": "3.6.3", "version": "3.6.4",
"type": "module", "type": "module",
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!",
"browserslist": "> 0.5%, last 2 versions, not dead", "browserslist": "> 0.5%, last 2 versions, not dead",
+3 -2
View File
@@ -1,3 +1,4 @@
@use "sass:meta"; @use "sass:meta";
@import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600,700"); @import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600,700");
@@ -2609,7 +2610,7 @@ body {
[class*="MessageList__unread___"] { [class*="MessageList__unread___"] {
position: relative; position: relative;
background: rgb(228 225 225); background: var(--background-secondary, rgb(228 225 225));
} }
.dark [class*="MessageList__unread___"] { .dark [class*="MessageList__unread___"] {
@@ -2735,7 +2736,7 @@ body {
[class*="MessageList__MessageList___"] [class*="MessageList__MessageList___"]
> ol > ol
> li[class*="MessageList__selected___"] { > li[class*="MessageList__selected___"] {
background: rgb(228 225 225); background: var(--background-secondary, rgb(228 225 225));
color: var(--text-primary); color: var(--text-primary);
box-shadow: none !important; box-shadow: none !important;
position: relative; position: relative;
+3 -1
View File
@@ -25,7 +25,9 @@ type ThemeContent = {
CanChangeColour?: boolean; CanChangeColour?: boolean;
CustomCSS?: string; CustomCSS?: string;
hideThemeName?: boolean; hideThemeName?: boolean;
forceTheme?: boolean;
forceDark?: boolean; forceDark?: boolean;
adaptiveCssVariables?: string[];
images?: { id: string; variableName: string; data: string }[]; // data: base64 images?: { id: string; variableName: string; data: string }[]; // data: base64
}; };
@@ -35,7 +37,7 @@ export type InstallThemeMeta = {
serverUpdatedAtSec?: number; serverUpdatedAtSec?: number;
forceTheme?: boolean; forceTheme?: boolean;
adaptiveCssVariables?: string[]; adaptiveCssVariables?: string[];
images: { id: string; variableName: string; data: string }[]; // data: base64 images?: { id: string; variableName: string; data: string }[]; // data: base64
}; };
export class ThemeManager { export class ThemeManager {
+45 -3
View File
@@ -5,7 +5,7 @@ import { lightenAndPaleColor } from "./lightenAndPaleColor";
import ColorLuminance from "./ColorLuminance"; import ColorLuminance from "./ColorLuminance";
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour"; import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
import { getCustomThemeAdaptiveCssVariables } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import darkLogo from "@/resources/icons/betterseqta-light-full.png";
import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
@@ -84,6 +84,21 @@ function cancelColorTransition() {
} }
} }
function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null {
const parsedHex = parseRepresentativeHex(s);
if (!parsedHex) return null;
try {
const [r, g, b] = Color(parsedHex).rgb().array();
return {
r: Math.round(r),
g: Math.round(g),
b: Math.round(b),
};
} catch {
return null;
}
}
function applyColorsWith(selectedColor: string) { function applyColorsWith(selectedColor: string) {
if (settingsState.transparencyEffects) { if (settingsState.transparencyEffects) {
document.documentElement.classList.add("transparencyEffects"); document.documentElement.classList.add("transparencyEffects");
@@ -129,8 +144,35 @@ function applyColorsWith(selectedColor: string) {
applyProperties({ ...commonProps, ...modeProps, ...dynamicProps }); applyProperties({ ...commonProps, ...modeProps, ...dynamicProps });
if (settingsState.selectedTheme) { if (settingsState.selectedTheme) {
for (const name of getCustomThemeAdaptiveCssVariables()) { const channels = getRepresentativeRgbChannels(selectedColor);
setCSSVar(name, selectedColor); for (const binding of getCustomThemeAdaptiveCssVariableBindings()) {
if (!binding.channel) {
setCSSVar(binding.cssVarName, selectedColor);
continue;
}
if (!channels) {
continue;
}
if (binding.channel === "r") {
setCSSVar(binding.cssVarName, String(channels.r));
} else if (binding.channel === "g") {
setCSSVar(binding.cssVarName, String(channels.g));
} else {
setCSSVar(binding.cssVarName, String(channels.b));
}
}
}
// Let themes opt-in to overriding only adaptive accent output.
// A theme can define `--adaptive-better-main` from adaptive channel bindings.
if (settingsState.selectedTheme && settingsState.adaptiveThemeColour) {
const adaptiveOverride = getComputedStyle(document.documentElement)
.getPropertyValue("--adaptive-better-main")
.trim();
if (adaptiveOverride) {
setCSSVar("--better-main", adaptiveOverride);
} }
} }
@@ -1,20 +1,49 @@
/** Tracks which author-declared CSS variables mirror the effective accent; not persisted in settings storage. */ /** Tracks which author-declared CSS variables mirror the effective accent; not persisted in settings storage. */
const VALID_CUSTOM_PROP = /^--[a-zA-Z0-9_-]{1,120}$/; const VALID_CUSTOM_PROP = /^--[a-zA-Z0-9_-]{1,120}$/;
const VALID_CHANNEL = /^(r|g|b)$/;
let boundNames: string[] = []; export type AdaptiveChannel = "r" | "g" | "b";
export function normalizeAdaptiveCssVariableNames( export type AdaptiveCssVariableBinding = {
cssVarName: string;
channel?: AdaptiveChannel;
};
let boundBindings: AdaptiveCssVariableBinding[] = [];
function parseAdaptiveBinding(
rawBinding: string,
): AdaptiveCssVariableBinding | null {
const trimmed = rawBinding.trim();
if (!trimmed) return null;
const [rawName, rawChannel] = trimmed.split(":", 2);
const cssVarName = rawName?.trim() ?? "";
if (!VALID_CUSTOM_PROP.test(cssVarName)) return null;
if (!rawChannel) return { cssVarName };
const channel = rawChannel.trim().toLowerCase();
if (!VALID_CHANNEL.test(channel)) return null;
return { cssVarName, channel: channel as AdaptiveChannel };
}
export function normalizeAdaptiveCssVariableBindings(
names: string[] | undefined, names: string[] | undefined,
): string[] { ): AdaptiveCssVariableBinding[] {
if (!names?.length) return []; if (!names?.length) return [];
const out: string[] = []; const out: AdaptiveCssVariableBinding[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
for (const raw of names) { for (const raw of names) {
const s = raw.trim(); const parsed = parseAdaptiveBinding(raw);
if (!VALID_CUSTOM_PROP.test(s) || seen.has(s)) continue; if (!parsed) continue;
seen.add(s); const key = `${parsed.cssVarName}:${parsed.channel ?? "full"}`;
out.push(s); if (seen.has(key)) continue;
seen.add(key);
out.push(parsed);
} }
return out; return out;
} }
@@ -22,19 +51,24 @@ export function normalizeAdaptiveCssVariableNames(
export function setCustomThemeAdaptiveCssVariables( export function setCustomThemeAdaptiveCssVariables(
names: string[] | undefined, names: string[] | undefined,
): void { ): void {
for (const n of boundNames) { for (const binding of boundBindings) {
document.documentElement.style.removeProperty(n); document.documentElement.style.removeProperty(binding.cssVarName);
} }
boundNames = normalizeAdaptiveCssVariableNames(names); boundBindings = normalizeAdaptiveCssVariableBindings(names);
} }
export function getCustomThemeAdaptiveCssVariableBindings(): AdaptiveCssVariableBinding[] {
return boundBindings;
}
// Backward-compatible helper for existing callsites.
export function getCustomThemeAdaptiveCssVariables(): string[] { export function getCustomThemeAdaptiveCssVariables(): string[] {
return boundNames; return boundBindings.map((b) => b.cssVarName);
} }
export function clearCustomThemeAdaptiveCssVariables(): void { export function clearCustomThemeAdaptiveCssVariables(): void {
for (const n of boundNames) { for (const binding of boundBindings) {
document.documentElement.style.removeProperty(n); document.documentElement.style.removeProperty(binding.cssVarName);
} }
boundNames = []; boundBindings = [];
} }
@@ -34,6 +34,11 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
const text = stringToHTML(/* html */ ` const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;"> <div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
<h1>3.6.4 - Fix for alpine theme & Assement wieghting improvement</h1>
<li>Added advanced colour adjustments variables for theme customisation.</li>
<li>Improved logic for assement weightings to improve compatibility.</li>
<h1>3.6.3 - Assessment overview fix</h1> <h1>3.6.3 - Assessment overview fix</h1>
<li>Fixed assessments overview failing to load.</li> <li>Fixed assessments overview failing to load.</li>
@@ -50,6 +55,7 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
<li>Updated outdated in-app links and update some under the hood code (Vite 8).</li> <li>Updated outdated in-app links and update some under the hood code (Vite 8).</li>
<li>Added a notifications panel animation to work like settings.</li> <li>Added a notifications panel animation to work like settings.</li>
<li>Fix timetable edit plugin not working correctly.</li> <li>Fix timetable edit plugin not working correctly.</li>
<h1>3.5.3 - Adaptive theme updates</h1> <h1>3.5.3 - Adaptive theme updates</h1>
<li>Fixed adaptive theming on current-year course and assessment pages.</li> <li>Fixed adaptive theming on current-year course and assessment pages.</li>