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-base: #fbf1c7;
|
||||||
--color-mantle: #f3eac1;
|
--color-mantle: #f3eac1;
|
||||||
--color-crust: #e7deb7;
|
--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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -104,6 +109,11 @@
|
|||||||
--color-base: #1e1e2e;
|
--color-base: #1e1e2e;
|
||||||
--color-mantle: #141620;
|
--color-mantle: #141620;
|
||||||
--color-crust: #0e0f16;
|
--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 {
|
@theme {
|
||||||
--color-rosewater: #efc9c2;
|
--color-rosewater: #efc9c2;
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ function sanitizeHtml(html: string): string {
|
|||||||
)
|
)
|
||||||
.forEach((el) => el.remove());
|
.forEach((el) => el.remove());
|
||||||
|
|
||||||
// Remove event handler attributes and dangerous URLs from all elements
|
// Remove event handler attributes and dangerous URLs from all elements.
|
||||||
doc.querySelectorAll("[on*], [href], [style], [action]").forEach((el) => {
|
// 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);
|
const attrs = Array.from(el.attributes);
|
||||||
attrs.forEach((attr) => {
|
attrs.forEach((attr) => {
|
||||||
const name = attr.name;
|
const name = attr.name;
|
||||||
|
|||||||
@@ -7,6 +7,97 @@ import {
|
|||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { Spinner } from "~/components/Spinner";
|
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> {
|
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: "primary" | "secondary" | "danger" | "ghost" | "download";
|
variant?: "primary" | "secondary" | "danger" | "ghost" | "download";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
@@ -65,8 +156,8 @@ export default function Button(props: ButtonProps) {
|
|||||||
: "bg-surface0 hover:brightness-125 active:scale-90";
|
: "bg-surface0 hover:brightness-125 active:scale-90";
|
||||||
case "download":
|
case "download":
|
||||||
return isDisabledOrLoading
|
return isDisabledOrLoading
|
||||||
? "text-base cursor-not-allowed brightness-75"
|
? "cursor-not-allowed brightness-75"
|
||||||
: "text-base hover:brightness-125 active:scale-90";
|
: "hover:brightness-125 active:scale-90";
|
||||||
case "danger":
|
case "danger":
|
||||||
return isDisabledOrLoading
|
return isDisabledOrLoading
|
||||||
? "bg-red cursor-not-allowed brightness-75"
|
? "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 = () => {
|
const sizeClasses = () => {
|
||||||
switch (size()) {
|
switch (size()) {
|
||||||
case "sm":
|
case "sm":
|
||||||
@@ -96,10 +196,16 @@ export default function Button(props: ButtonProps) {
|
|||||||
const widthClass = () => (local.fullWidth ? "w-full" : "");
|
const widthClass = () => (local.fullWidth ? "w-full" : "");
|
||||||
|
|
||||||
const buttonStyle = (): JSX.CSSProperties => {
|
const buttonStyle = (): JSX.CSSProperties => {
|
||||||
if (local.color) {
|
if (variant() === "download") {
|
||||||
return { background: local.color };
|
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 (
|
return (
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
|||||||
brandColor: "#527640",
|
brandColor: "#527640",
|
||||||
brandColorDark: "#6CA86C",
|
brandColorDark: "#6CA86C",
|
||||||
ogDefaultImage: "/nessa/og-default.png",
|
ogDefaultImage: "/nessa/og-default.png",
|
||||||
faviconPath: "/nessa/favicon.ico"
|
faviconPath: "/nessa/favicon/favicon.ico"
|
||||||
},
|
},
|
||||||
lineage: {
|
lineage: {
|
||||||
id: "lineage",
|
id: "lineage",
|
||||||
@@ -99,7 +99,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
|||||||
titleSuffix: " | Life and Lineage",
|
titleSuffix: " | Life and Lineage",
|
||||||
brandColor: "#a13536",
|
brandColor: "#a13536",
|
||||||
ogDefaultImage: "/lineage/og-default.png",
|
ogDefaultImage: "/lineage/og-default.png",
|
||||||
faviconPath: "/lineage/favicon.ico"
|
faviconPath: "/lineage/favicon/favicon.ico"
|
||||||
},
|
},
|
||||||
gaze: {
|
gaze: {
|
||||||
id: "gaze",
|
id: "gaze",
|
||||||
@@ -110,7 +110,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
|||||||
titleSuffix: " | Gaze",
|
titleSuffix: " | Gaze",
|
||||||
brandColor: "#f9e2af",
|
brandColor: "#f9e2af",
|
||||||
ogDefaultImage: "/gaze/og-default.png",
|
ogDefaultImage: "/gaze/og-default.png",
|
||||||
faviconPath: "/gaze/favicon.ico"
|
faviconPath: "/gaze/favicon/favicon.ico"
|
||||||
},
|
},
|
||||||
inputhalo: {
|
inputhalo: {
|
||||||
id: "inputhalo",
|
id: "inputhalo",
|
||||||
@@ -121,7 +121,7 @@ export const SITE_CONFIG: Record<SiteId, Site> = {
|
|||||||
titleSuffix: " | InputHalo",
|
titleSuffix: " | InputHalo",
|
||||||
brandColor: "#f38ba8",
|
brandColor: "#f38ba8",
|
||||||
ogDefaultImage: "/inputhalo/og-default.png",
|
ogDefaultImage: "/inputhalo/og-default.png",
|
||||||
faviconPath: "/inputhalo/favicon.ico"
|
faviconPath: "/inputhalo/favicon/favicon.ico"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function NessaLanding() {
|
|||||||
</A>
|
</A>
|
||||||
<A
|
<A
|
||||||
href="/contact"
|
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
|
Request beta access
|
||||||
</A>
|
</A>
|
||||||
|
|||||||
Reference in New Issue
Block a user