website: fix dark mode, table contrast, prose opacity, add AGENTS.md
Dark mode fix: import liveline.css and liveline-prism.css via CSS @import in globals.css instead of JS import in the route file. This brings them into the Tailwind compilation chain so @variant dark works. @variant dark silently fails in CSS files imported only via JS — documented this constraint in website/AGENTS.md. - Prism dark colors use CSS variables on :root with @variant dark - liveline.css dark overrides use @variant dark nested inside selectors - Table cells use --ll-text-primary for full contrast in both modes - Prose paragraphs at 0.82 opacity, bold and inline code punch through to full opacity for visual hierarchy - Prose line-height increased to 22px - Add website/AGENTS.md documenting dark mode rules
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Website
|
||||
|
||||
## Tailwind dark mode
|
||||
|
||||
Dark mode uses `prefers-color-scheme` media query, configured in `globals.css`:
|
||||
|
||||
```css
|
||||
@custom-variant dark (@media (prefers-color-scheme: dark));
|
||||
```
|
||||
|
||||
### `@variant dark` rules
|
||||
|
||||
1. **Must be nested inside a selector**, never at the top level:
|
||||
|
||||
```css
|
||||
/* WRONG - silently produces no output */
|
||||
@variant dark {
|
||||
.my-class { color: white; }
|
||||
}
|
||||
|
||||
/* CORRECT - nest inside the selector */
|
||||
.my-class {
|
||||
@variant dark {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* CORRECT - define variables inside :root */
|
||||
:root {
|
||||
@variant dark {
|
||||
--my-var: white;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Only works in CSS files that are part of the Tailwind compilation chain.** Files must be imported via CSS `@import` from `globals.css` (which has `@import "tailwindcss"`). JS/TS `import "file.css"` in route files does NOT go through Tailwind — `@variant dark` will silently fail.
|
||||
|
||||
```css
|
||||
/* globals.css — this is the Tailwind entry point */
|
||||
@import "tailwindcss";
|
||||
@import "./liveline.css"; /* these get Tailwind processing */
|
||||
@import "./liveline-prism.css";
|
||||
@custom-variant dark (@media (prefers-color-scheme: dark));
|
||||
```
|
||||
|
||||
If you create a new CSS file that needs `@variant dark`, add it as a CSS `@import` in `globals.css`. Do NOT import it from a `.tsx` file.
|
||||
|
||||
### For many dark overrides, use CSS variables on `:root`
|
||||
|
||||
For files with many dark mode selectors (like prism syntax colors), define CSS variables in `:root { @variant dark { ... } }` and reference them in selectors. This avoids repeating `@variant dark` in every rule.
|
||||
|
||||
## No localStorage dark mode toggle
|
||||
|
||||
There is no JS-based `.dark` class toggle. Do not add inline scripts to toggle `.dark` on `<html>`. The site follows the OS preference only via `prefers-color-scheme`.
|
||||
@@ -214,14 +214,15 @@ export function SectionHeading({ id, children }: { id: string; children: React.R
|
||||
export function P({ children, className = "" }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
className={`ll-prose ${className}`}
|
||||
style={{
|
||||
fontFamily: "var(--ll-font-primary)",
|
||||
fontSize: "14px",
|
||||
fontWeight: 475,
|
||||
lineHeight: "20px",
|
||||
lineHeight: "22px",
|
||||
letterSpacing: "-0.09px",
|
||||
color: "var(--ll-text-primary)",
|
||||
opacity: 0.82,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
@@ -541,7 +542,7 @@ export function ComparisonTable({
|
||||
style={{
|
||||
padding: "4.8px 12px 4.8px 0",
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
fontWeight: 500,
|
||||
fontFamily: "var(--ll-font-code)",
|
||||
color: "var(--ll-text-primary)",
|
||||
borderBottom: "1px solid var(--ll-border)",
|
||||
@@ -554,9 +555,9 @@ export function ComparisonTable({
|
||||
style={{
|
||||
padding: "4.8px 12px 4.8px 0",
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
fontWeight: 500,
|
||||
fontFamily: "var(--ll-font-code)",
|
||||
color: "var(--ll-text-muted)",
|
||||
color: "var(--ll-text-primary)",
|
||||
borderBottom: "1px solid var(--ll-border)",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
@@ -567,9 +568,9 @@ export function ComparisonTable({
|
||||
style={{
|
||||
padding: "4.8px 12px 4.8px 0",
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
fontWeight: 500,
|
||||
fontFamily: "var(--ll-font-code)",
|
||||
color: "var(--ll-text-muted)",
|
||||
color: "var(--ll-text-primary)",
|
||||
borderBottom: "1px solid var(--ll-border)",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
import type { MetaFunction } from "react-router";
|
||||
import dedent from "string-dedent";
|
||||
import "website/src/styles/liveline.css";
|
||||
import "website/src/styles/liveline-prism.css";
|
||||
import {
|
||||
EditorialPage,
|
||||
P,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "./liveline.css";
|
||||
@import "./liveline-prism.css";
|
||||
@custom-variant dark (@media (prefers-color-scheme: dark));
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@@ -138,95 +138,150 @@ pre[class*="language-"] {
|
||||
|
||||
/* ======================================================================
|
||||
Dark mode - GitHub Dark inspired palette.
|
||||
Uses @variant dark from globals.css so the strategy is defined in
|
||||
one place (@custom-variant dark in globals.css).
|
||||
@variant dark works here because globals.css imports this file via
|
||||
CSS @import (part of Tailwind compilation chain). Dark colors are
|
||||
defined as CSS variables on :root, then referenced in selectors.
|
||||
====================================================================== */
|
||||
|
||||
@variant dark {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token.keyword,
|
||||
.token.module,
|
||||
.token.imports,
|
||||
.token.control-flow {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.template-string,
|
||||
.token.attr-value {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.parameter {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.token.tag .token.tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token.tag .token.class-name {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token.tag .token.attr-name,
|
||||
.token.attr-name {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.builtin {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #8b949e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.tag .token.punctuation {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token.spread {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.token.plain-text {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.language-bash .token.function {
|
||||
color: #79c0ff;
|
||||
:root {
|
||||
@variant dark {
|
||||
--prism-fg: #e6edf3;
|
||||
--prism-keyword: #ff7b72;
|
||||
--prism-string: #a5d6ff;
|
||||
--prism-function: #d2a8ff;
|
||||
--prism-property: #ffa657;
|
||||
--prism-tag: #7ee787;
|
||||
--prism-attr: #79c0ff;
|
||||
--prism-number: #79c0ff;
|
||||
--prism-operator: #ff7b72;
|
||||
--prism-punctuation: #e6edf3;
|
||||
--prism-builtin: #79c0ff;
|
||||
--prism-comment: #8b949e;
|
||||
--prism-spread: #ff7b72;
|
||||
--prism-regex: #a5d6ff;
|
||||
--prism-bash-fn: #79c0ff;
|
||||
}
|
||||
}
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
@variant dark {
|
||||
color: var(--prism-fg);
|
||||
}
|
||||
}
|
||||
|
||||
.token.keyword,
|
||||
.token.module,
|
||||
.token.imports,
|
||||
.token.control-flow {
|
||||
@variant dark {
|
||||
color: var(--prism-keyword);
|
||||
}
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.template-string,
|
||||
.token.attr-value {
|
||||
@variant dark {
|
||||
color: var(--prism-string);
|
||||
}
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
@variant dark {
|
||||
color: var(--prism-function);
|
||||
}
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.parameter {
|
||||
@variant dark {
|
||||
color: var(--prism-property);
|
||||
}
|
||||
}
|
||||
|
||||
.token.tag .token.tag {
|
||||
@variant dark {
|
||||
color: var(--prism-tag);
|
||||
}
|
||||
}
|
||||
|
||||
.token.tag .token.class-name {
|
||||
@variant dark {
|
||||
color: var(--prism-tag);
|
||||
}
|
||||
}
|
||||
|
||||
.token.tag .token.attr-name,
|
||||
.token.attr-name {
|
||||
@variant dark {
|
||||
color: var(--prism-attr);
|
||||
}
|
||||
}
|
||||
|
||||
.token.number {
|
||||
@variant dark {
|
||||
color: var(--prism-number);
|
||||
}
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
@variant dark {
|
||||
color: var(--prism-operator);
|
||||
}
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
@variant dark {
|
||||
color: var(--prism-punctuation);
|
||||
}
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.builtin {
|
||||
@variant dark {
|
||||
color: var(--prism-builtin);
|
||||
}
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
@variant dark {
|
||||
color: var(--prism-comment);
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.token.tag .token.punctuation {
|
||||
@variant dark {
|
||||
color: var(--prism-punctuation);
|
||||
}
|
||||
}
|
||||
|
||||
.token.spread {
|
||||
@variant dark {
|
||||
color: var(--prism-spread);
|
||||
}
|
||||
}
|
||||
|
||||
.token.plain-text {
|
||||
@variant dark {
|
||||
color: var(--prism-fg);
|
||||
}
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
@variant dark {
|
||||
color: var(--prism-regex);
|
||||
}
|
||||
}
|
||||
|
||||
.language-bash .token.function {
|
||||
@variant dark {
|
||||
color: var(--prism-bash-fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,55 +113,56 @@
|
||||
--ll-fade-12: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
/* Dark mode overrides — uses @variant dark from globals.css
|
||||
so the strategy is defined in one place */
|
||||
@variant dark {
|
||||
/* Dark mode overrides — @variant dark works here because globals.css
|
||||
imports this file via CSS @import (part of Tailwind compilation chain).
|
||||
Must be nested inside a selector, not at the top level. */
|
||||
.liveline-page {
|
||||
--ll-text-primary: rgba(255, 255, 255, 0.9);
|
||||
--ll-text-secondary: rgba(255, 255, 255, 0.45);
|
||||
--ll-text-tertiary: rgba(255, 255, 255, 0.25);
|
||||
--ll-text-muted: rgba(255, 255, 255, 0.35);
|
||||
--ll-text-hover: rgba(255, 255, 255, 0.7);
|
||||
--ll-bg: rgb(17, 17, 17);
|
||||
--ll-border: rgba(255, 255, 255, 0.1);
|
||||
--ll-divider: rgba(255, 255, 255, 0.06);
|
||||
--ll-code-bg: rgba(255, 255, 255, 0.05);
|
||||
--ll-code-line-nr: rgba(255, 255, 255, 0.3);
|
||||
--ll-selection-bg: rgba(255, 255, 255, 0.1);
|
||||
@variant dark {
|
||||
--ll-text-primary: rgba(255, 255, 255, 0.9);
|
||||
--ll-text-secondary: rgba(255, 255, 255, 0.45);
|
||||
--ll-text-tertiary: rgba(255, 255, 255, 0.25);
|
||||
--ll-text-muted: rgba(255, 255, 255, 0.35);
|
||||
--ll-text-hover: rgba(255, 255, 255, 0.7);
|
||||
--ll-bg: rgb(17, 17, 17);
|
||||
--ll-border: rgba(255, 255, 255, 0.1);
|
||||
--ll-divider: rgba(255, 255, 255, 0.06);
|
||||
--ll-code-bg: rgba(255, 255, 255, 0.05);
|
||||
--ll-code-line-nr: rgba(255, 255, 255, 0.3);
|
||||
--ll-selection-bg: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--ll-btn-bg: rgb(30, 30, 30);
|
||||
--ll-btn-shadow: rgba(255, 255, 255, 0.05) 0px 2px 8px,
|
||||
rgba(255, 255, 255, 0.02) 0px 4px 16px,
|
||||
rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset;
|
||||
--ll-btn-bg: rgb(30, 30, 30);
|
||||
--ll-btn-shadow: rgba(255, 255, 255, 0.05) 0px 2px 8px,
|
||||
rgba(255, 255, 255, 0.02) 0px 4px 16px,
|
||||
rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset;
|
||||
|
||||
--ll-accent: #58a6ff;
|
||||
--ll-primary: #60a5fa;
|
||||
--ll-secondary: #f5a623;
|
||||
--ll-accent: #58a6ff;
|
||||
--ll-primary: #60a5fa;
|
||||
--ll-secondary: #f5a623;
|
||||
|
||||
--ll-overlay-bg: hsla(0, 0%, 7%, 0.8);
|
||||
--ll-overlay-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||
0 1.625rem 3.375rem rgba(0, 0, 0, 0.3),
|
||||
0 1rem 2rem rgba(0, 0, 0, 0.2),
|
||||
0 0.625rem 1rem rgba(0, 0, 0, 0.15),
|
||||
0 0.3125rem 0.5rem rgba(0, 0, 0, 0.1),
|
||||
0 0.125rem 0.25rem rgba(0, 0, 0, 0.08),
|
||||
0 0 0.125rem rgba(0, 0, 0, 0.05);
|
||||
--ll-overlay-bg: hsla(0, 0%, 7%, 0.8);
|
||||
--ll-overlay-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||
0 1.625rem 3.375rem rgba(0, 0, 0, 0.3),
|
||||
0 1rem 2rem rgba(0, 0, 0, 0.2),
|
||||
0 0.625rem 1rem rgba(0, 0, 0, 0.15),
|
||||
0 0.3125rem 0.5rem rgba(0, 0, 0, 0.1),
|
||||
0 0.125rem 0.25rem rgba(0, 0, 0, 0.08),
|
||||
0 0 0.125rem rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* Header fade gradient stops (dark) */
|
||||
--ll-fade-0: rgb(17, 17, 17);
|
||||
--ll-fade-1: rgba(17, 17, 17, 0.737);
|
||||
--ll-fade-2: rgba(17, 17, 17, 0.541);
|
||||
--ll-fade-3: rgba(17, 17, 17, 0.382);
|
||||
--ll-fade-4: rgba(17, 17, 17, 0.278);
|
||||
--ll-fade-5: rgba(17, 17, 17, 0.194);
|
||||
--ll-fade-6: rgba(17, 17, 17, 0.126);
|
||||
--ll-fade-7: rgba(17, 17, 17, 0.075);
|
||||
--ll-fade-8: rgba(17, 17, 17, 0.042);
|
||||
--ll-fade-9: rgba(17, 17, 17, 0.021);
|
||||
--ll-fade-10: rgba(17, 17, 17, 0.008);
|
||||
--ll-fade-11: rgba(17, 17, 17, 0.002);
|
||||
--ll-fade-12: rgba(17, 17, 17, 0);
|
||||
}
|
||||
/* Header fade gradient stops (dark) */
|
||||
--ll-fade-0: rgb(17, 17, 17);
|
||||
--ll-fade-1: rgba(17, 17, 17, 0.737);
|
||||
--ll-fade-2: rgba(17, 17, 17, 0.541);
|
||||
--ll-fade-3: rgba(17, 17, 17, 0.382);
|
||||
--ll-fade-4: rgba(17, 17, 17, 0.278);
|
||||
--ll-fade-5: rgba(17, 17, 17, 0.194);
|
||||
--ll-fade-6: rgba(17, 17, 17, 0.126);
|
||||
--ll-fade-7: rgba(17, 17, 17, 0.075);
|
||||
--ll-fade-8: rgba(17, 17, 17, 0.042);
|
||||
--ll-fade-9: rgba(17, 17, 17, 0.021);
|
||||
--ll-fade-10: rgba(17, 17, 17, 0.008);
|
||||
--ll-fade-11: rgba(17, 17, 17, 0.002);
|
||||
--ll-fade-12: rgba(17, 17, 17, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (color: color(display-p3 1 1 1)) {
|
||||
@@ -184,6 +185,12 @@
|
||||
background: var(--ll-selection-bg);
|
||||
}
|
||||
|
||||
/* Bold and inline code inside prose punch through the reduced opacity */
|
||||
.ll-prose strong,
|
||||
.ll-prose .ll-inline-code {
|
||||
opacity: calc(1 / 0.82);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
Stagger-in animation
|
||||
======================================================================== */
|
||||
@@ -260,12 +267,14 @@
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@variant dark {
|
||||
.ll-inline-code {
|
||||
.ll-inline-code {
|
||||
@variant dark {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.ll-inline-code::before {
|
||||
.ll-inline-code::before {
|
||||
@variant dark {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user