fix: color and blog fixes, favicon pointers
This commit is contained in:
10
src/app.css
10
src/app.css
@@ -74,6 +74,11 @@
|
||||
--color-base: #fbf1c7;
|
||||
--color-mantle: #f3eac1;
|
||||
--color-crust: #e7deb7;
|
||||
|
||||
/* Button text colors: white on dark bg variants (primary/danger),
|
||||
theme text on light bg variants (secondary). */
|
||||
--color-button-text: #ffffff;
|
||||
--color-button-text-alt: var(--color-text);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -104,6 +109,11 @@
|
||||
--color-base: #1e1e2e;
|
||||
--color-mantle: #141620;
|
||||
--color-crust: #0e0f16;
|
||||
|
||||
/* Dark mode: dark text on lighter bg variants (primary/danger),
|
||||
white text on dark bg variants (secondary). */
|
||||
--color-button-text: var(--color-crust);
|
||||
--color-button-text-alt: #ffffff;
|
||||
}
|
||||
@theme {
|
||||
--color-rosewater: #efc9c2;
|
||||
|
||||
@@ -18,8 +18,11 @@ function sanitizeHtml(html: string): string {
|
||||
)
|
||||
.forEach((el) => el.remove());
|
||||
|
||||
// Remove event handler attributes and dangerous URLs from all elements
|
||||
doc.querySelectorAll("[on*], [href], [style], [action]").forEach((el) => {
|
||||
// Remove event handler attributes and dangerous URLs from all elements.
|
||||
// NOTE: attribute-name wildcards (e.g. [on*]) are not valid CSS selectors and
|
||||
// throw a SyntaxError on querySelectorAll, so we match all elements here and
|
||||
// the per-attribute checks below handle on*/href/style/action filtering.
|
||||
doc.querySelectorAll("*").forEach((el) => {
|
||||
const attrs = Array.from(el.attributes);
|
||||
attrs.forEach((attr) => {
|
||||
const name = attr.name;
|
||||
|
||||
@@ -7,6 +7,97 @@ import {
|
||||
} from "solid-js";
|
||||
import { Spinner } from "~/components/Spinner";
|
||||
|
||||
/** Parse a hex color string like `#f9e2af` into `[r, g, b]` in 0..255. */
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
const h = hex.replace(/^#/, "");
|
||||
return [
|
||||
parseInt(h.substring(0, 2), 16),
|
||||
parseInt(h.substring(2, 4), 16),
|
||||
parseInt(h.substring(4, 6), 16)
|
||||
];
|
||||
}
|
||||
|
||||
/** Convert `[r, g, b]` 0..255 to `[h, s, l]` — h in 0..360, s and l in 0..1. */
|
||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||
const rn = r / 255;
|
||||
const gn = g / 255;
|
||||
const bn = b / 255;
|
||||
const max = Math.max(rn, gn, bn);
|
||||
const min = Math.min(rn, gn, bn);
|
||||
const l = (max + min) / 2;
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case rn:
|
||||
h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
|
||||
break;
|
||||
case gn:
|
||||
h = ((bn - rn) / d + 2) / 6;
|
||||
break;
|
||||
case bn:
|
||||
h = ((rn - gn) / d + 4) / 6;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [h * 360, s, l];
|
||||
}
|
||||
|
||||
/** Convert `[h, s, l]` (h in 0..360, s and l in 0..1) to `[r, g, b]` 0..255. */
|
||||
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12;
|
||||
return Math.round(255 * (l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)));
|
||||
};
|
||||
return [f(0), f(8), f(4)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the relative luminance (WCAG) of an sRGB color, given linear
|
||||
* channel values in 0..1.
|
||||
*/
|
||||
function luminance(r: number, g: number, b: number): number {
|
||||
return (
|
||||
0.2126 * (r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92) +
|
||||
0.7152 * (g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92) +
|
||||
0.0722 * (b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a background color, return a pair of `{ bg, text }` CSS color
|
||||
* strings that guarantee WCAG AA contrast.
|
||||
*
|
||||
* Light backgrounds (luminance > 0.4) are darkened in HSL space so the
|
||||
* result is dark enough for white text. Dark backgrounds are kept as-is
|
||||
* with white text. This is used by the download variant whose background
|
||||
* is a product brand color that can be arbitrarily light.
|
||||
*/
|
||||
function adjustForContrast(color: string): { bg: string; text: string } {
|
||||
const [r, g, b] = hexToRgb(color);
|
||||
const [h, s, l] = rgbToHsl(r, g, b);
|
||||
const lum = luminance(r / 255, g / 255, b / 255);
|
||||
|
||||
if (lum > 0.4) {
|
||||
// Darken: keep hue and saturation, clamp lightness so the result is
|
||||
// dark enough for white text (l ~ 0.35 → luminance ~ 0.17, contrast
|
||||
// with white ≈ 6:1).
|
||||
const dl = Math.min(l, 0.35);
|
||||
const [dr, dg, db] = hslToRgb(h, s, dl);
|
||||
return {
|
||||
bg: `rgb(${dr}, ${dg}, ${db})`,
|
||||
text: "#ffffff"
|
||||
};
|
||||
}
|
||||
return {
|
||||
bg: `rgb(${r}, ${g}, ${b})`,
|
||||
text: "#ffffff"
|
||||
};
|
||||
}
|
||||
|
||||
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "danger" | "ghost" | "download";
|
||||
size?: "sm" | "md" | "lg";
|
||||
@@ -65,8 +156,8 @@ export default function Button(props: ButtonProps) {
|
||||
: "bg-surface0 hover:brightness-125 active:scale-90";
|
||||
case "download":
|
||||
return isDisabledOrLoading
|
||||
? "text-base cursor-not-allowed brightness-75"
|
||||
: "text-base hover:brightness-125 active:scale-90";
|
||||
? "cursor-not-allowed brightness-75"
|
||||
: "hover:brightness-125 active:scale-90";
|
||||
case "danger":
|
||||
return isDisabledOrLoading
|
||||
? "bg-red cursor-not-allowed brightness-75"
|
||||
@@ -80,6 +171,15 @@ export default function Button(props: ButtonProps) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Compute background + text color for the download variant. */
|
||||
const downloadColors = () => {
|
||||
const isDisabledOrLoading = local.disabled || local.loading;
|
||||
if (isDisabledOrLoading) {
|
||||
return { bg: "var(--color-base)", text: "#ffffff" };
|
||||
}
|
||||
return adjustForContrast(local.color || "var(--color-blue)");
|
||||
};
|
||||
|
||||
const sizeClasses = () => {
|
||||
switch (size()) {
|
||||
case "sm":
|
||||
@@ -96,10 +196,16 @@ export default function Button(props: ButtonProps) {
|
||||
const widthClass = () => (local.fullWidth ? "w-full" : "");
|
||||
|
||||
const buttonStyle = (): JSX.CSSProperties => {
|
||||
if (local.color) {
|
||||
return { background: local.color };
|
||||
if (variant() === "download") {
|
||||
const { bg, text } = downloadColors();
|
||||
return { background: bg, color: text };
|
||||
}
|
||||
return {};
|
||||
// Theme-aware text: white on dark bg variants (primary/danger),
|
||||
// theme text on light bg variants (secondary). The CSS variables
|
||||
// flip between light/dark modes so the contrast stays good in both.
|
||||
if (variant() === "secondary")
|
||||
return { color: "var(--color-button-text-alt)" };
|
||||
return { color: "var(--color-button-text)" };
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -88,7 +88,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
||||
brandColor: "#527640",
|
||||
brandColorDark: "#6CA86C",
|
||||
ogDefaultImage: "/nessa/og-default.png",
|
||||
faviconPath: "/nessa/favicon.ico"
|
||||
faviconPath: "/nessa/favicon/favicon.ico"
|
||||
},
|
||||
lineage: {
|
||||
id: "lineage",
|
||||
@@ -99,7 +99,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
||||
titleSuffix: " | Life and Lineage",
|
||||
brandColor: "#a13536",
|
||||
ogDefaultImage: "/lineage/og-default.png",
|
||||
faviconPath: "/lineage/favicon.ico"
|
||||
faviconPath: "/lineage/favicon/favicon.ico"
|
||||
},
|
||||
gaze: {
|
||||
id: "gaze",
|
||||
@@ -110,7 +110,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
||||
titleSuffix: " | Gaze",
|
||||
brandColor: "#f9e2af",
|
||||
ogDefaultImage: "/gaze/og-default.png",
|
||||
faviconPath: "/gaze/favicon.ico"
|
||||
faviconPath: "/gaze/favicon/favicon.ico"
|
||||
},
|
||||
inputhalo: {
|
||||
id: "inputhalo",
|
||||
@@ -121,7 +121,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
||||
titleSuffix: " | InputHalo",
|
||||
brandColor: "#f38ba8",
|
||||
ogDefaultImage: "/inputhalo/og-default.png",
|
||||
faviconPath: "/inputhalo/favicon.ico"
|
||||
faviconPath: "/inputhalo/favicon/favicon.ico"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function NessaLanding() {
|
||||
</A>
|
||||
<A
|
||||
href="/contact"
|
||||
class="border-surface2 hover:bg-surface0/40 rounded-full border-2 px-8 py-3 text-base font-semibold transition-colors"
|
||||
class="border-surface2 hover:bg-surface0/40 text-accent rounded-full border-2 px-8 py-3 font-semibold transition-colors"
|
||||
>
|
||||
Request beta access
|
||||
</A>
|
||||
|
||||
Reference in New Issue
Block a user