This commit is contained in:
seb
2026-10-05 01:01:03 +02:00
parent 81b59fd309
commit b766769634
59 changed files with 903 additions and 280 deletions

View File

@@ -1,2 +1,346 @@
// Mirrored in index.html and index.css for paint before JS loads.
export const MAIN_BACKGROUND = '#85b7ad';
/**
* ============================================================================
* CENTRAL COLOR PALETTE — the single source of truth for `dashfront`.
* ============================================================================
*
* The palette is intentionally tiny: 1 white + 6 light greys + 6 dark greys
* + 7 chromatic families (each a `dark`/`light` pair) = 20 base values.
* There is no black: the darkest neutral is `DARK_6`, and every shadow or
* scrim is an alpha of it (see `shade()`).
*
* EVERYTHING ELSE IN THIS FILE IS DERIVED from those 20 values. Change one
* base and the whole app follows, live, via Vite HMR.
*
* HOW TO PLAY WITH THE COLORS
* ---------------------------
* 1. Edit any of the 20 base values in section 1.
* 2. `CHROMA.<name>.dark` → text, icons, borders, solid buttons.
* `CHROMA.<name>.light` → panel fills, chips, tints.
*
* RULES FOR NEW CODE
* ------------------
* * Never write a hex/rgba literal in a component — import a token.
* * Prefer a base token. Reach for a derived one only when it reads better.
*/
/* ────────────────────────────────────────────────────────────────────────────
* 1. THE 20 BASE VALUES
* -------------------------------------------------------------------------*/
/** Pure white — cards, dialogs, page panels. */
export const WHITE = '#ffffff';
/** Light greys, ascending darkness. `LIGHT_1` is the faintest surface. */
export const LIGHT_1 = '#fcfcfc';
export const LIGHT_2 = '#fafafa';
export const LIGHT_3 = '#f8fafc';
export const LIGHT_4 = '#f1f5f9';
export const LIGHT_5 = '#e2e8f0';
export const LIGHT_6 = '#cbd5e1';
/** Dark greys, descending lightness. `DARK_1` is muted text, `DARK_6` darkest. */
export const DARK_1 = '#94a3b8';
export const DARK_2 = '#64748b';
export const DARK_3 = '#475569';
export const DARK_4 = '#334155';
export const DARK_5 = '#1e293b';
export const DARK_6 = '#0f172a';
/**
* The seven chromatic families.
* dark → text, icons, borders, solid buttons (readable on white)
* light → panel fills, chips, tints
*/
export const CHROMA = {
RED: { dark: '#b91c1c', light: '#fecaca' },
ORANGE: { dark: '#d97706', light: '#fed7aa' },
YELLOW: { dark: '#ca8a04', light: '#fde68a' },
GREEN: { dark: '#15803d', light: '#bbf7d0' },
PETROL: { dark: '#0f766e', light: '#99f6e4' },
BLUE: { dark: '#1d4ed8', light: '#bfdbfe' },
PURPLE: { dark: '#6d28d9', light: '#ddd6fe' },
};
/* ────────────────────────────────────────────────────────────────────────────
* 2. COLOR MATH (the only functions in this file)
* -------------------------------------------------------------------------*/
function toHex(parts) {
return `#${parts
.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
.join('')}`;
}
function channels(hex) {
return [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
}
/** Linear blend: `amount` 0 → `a`, 1 → `b`. */
export function mix(a, b, amount) {
const ca = channels(a);
const cb = channels(b);
return toHex(ca.map((v, i) => v + (cb[i] - v) * amount));
}
/** `color` at `opacity`, as an `rgba()` string. */
export function alpha(color, opacity) {
const [r, g, b] = channels(color);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
/** An alpha of `DARK_6` — our stand-in for "black". */
export const shade = (opacity) => alpha(DARK_6, opacity);
/** An alpha of `WHITE` — used for text and highlights over dark surfaces. */
export const veil = (opacity) => alpha(WHITE, opacity);
/** A lighter tint of a chroma family: 0 → `light`, 1 → white. */
const tint = (family, amount) => mix(CHROMA[family].light, WHITE, amount);
/** Resolve a `{ dark, light }` pair against the active MUI theme. */
export function byMode(theme, pair, fallback) {
if (theme.palette.mode === 'dark' && pair.dark != null) return pair.dark;
return pair.light ?? fallback;
}
/* ────────────────────────────────────────────────────────────────────────────
* 3. BRAND & SHELL
* -------------------------------------------------------------------------*/
/** App background — a soft petrol wash. */
export const MAIN_BACKGROUND = mix(WHITE, CHROMA.PETROL.dark, 0.52);
/** MUI `palette.primary.main`. */
export const PRIMARY = CHROMA.PETROL.dark;
/** MUI `palette.background.paper`. */
export const PAPER = WHITE;
/** `meta[name="theme-color"]` in `index.html`. */
export const THEME_COLOR = CHROMA.BLUE.dark;
/* ────────────────────────────────────────────────────────────────────────────
* 4. NEUTRAL SURFACES
* -------------------------------------------------------------------------*/
export const SURFACE_FAINT = LIGHT_1; // attachment cards
export const SURFACE_SUBTLE = LIGHT_2; // placeholders, letterboxes
export const SURFACE_BASE = LIGHT_3; // panels, tiles, default tint
export const SURFACE_RAISED = LIGHT_4; // inset boxes, table heads, chips
export const BORDER = LIGHT_5; // dividers, outlines, placeholders
export const BORDER_STRONG = LIGHT_6; // muted rules, checkerboard
export const TEXT_MUTED = DARK_1;
export const TEXT_SECONDARY = DARK_2;
export const TEXT_BODY = DARK_3;
export const TEXT_STRONG = DARK_4;
export const INK = DARK_5; // barcode strokes
export const INK_STRONG = DARK_6; // label text on light chips
/** Semi-transparent panel wash for empty states. */
export const SURFACE_WASH = alpha(SURFACE_BASE, 0.7);
/** Neutral bubble / dialog fill that follows the active mode. */
export const NEUTRAL_BUBBLE = { dark: WHITE, light: LIGHT_4 };
/* ────────────────────────────────────────────────────────────────────────────
* 5. CHROMA DERIVATIONS
* Every pastel used for panels, chips and tiles comes from here.
* -------------------------------------------------------------------------*/
export const RED_BG = tint('RED', 0.85);
export const RED_PANEL = CHROMA.RED.light;
export const RED_SOFT = tint('RED', 0.35);
export const RED = CHROMA.RED.dark;
export const RED_HOVER = mix(CHROMA.RED.dark, CHROMA.ORANGE.dark, 0.5);
export const RED_TINT = alpha(CHROMA.RED.dark, 0.12);
export const RED_GLOW = alpha(CHROMA.RED.dark, 0.6);
export const ORANGE_BG = tint('ORANGE', 0.7);
export const ORANGE_PANEL = CHROMA.ORANGE.light;
export const ORANGE = CHROMA.ORANGE.dark;
export const YELLOW_BG = tint('YELLOW', 0.75);
export const YELLOW_PANEL = tint('YELLOW', 0.4);
export const YELLOW = CHROMA.YELLOW.dark;
export const GREEN_BG = tint('GREEN', 0.75);
export const GREEN_PANEL = CHROMA.GREEN.light;
export const GREEN_SOFT = tint('GREEN', 0.15);
export const GREEN = CHROMA.GREEN.dark;
export const PETROL_BG = tint('PETROL', 0.85);
export const PETROL_PANEL = CHROMA.PETROL.light;
export const PETROL = CHROMA.PETROL.dark;
export const BLUE_BG = tint('BLUE', 0.85);
export const BLUE_PANEL = CHROMA.BLUE.light;
export const BLUE_SOFT = tint('BLUE', 0.25);
export const BLUE = CHROMA.BLUE.dark;
export const BLUE_GLOW = alpha(CHROMA.BLUE.dark, 0.6);
export const BLUE_SHADOW = alpha(CHROMA.BLUE.dark, 0.25);
export const BLUE_TINT = alpha(CHROMA.BLUE.dark, 0.12);
export const BLUE_BORDER = alpha(CHROMA.BLUE.dark, 0.3);
export const PURPLE_BG = tint('PURPLE', 0.6);
export const PURPLE_PANEL = CHROMA.PURPLE.light;
export const PURPLE_SOFT = tint('PURPLE', 0.3);
export const PURPLE = CHROMA.PURPLE.dark;
/** Between violet and red — used for the article list editor. */
export const MAGENTA_PANEL = mix(CHROMA.PURPLE.light, CHROMA.RED.light, 0.45);
/** Between blue and petrol — used for brand text over photos. */
export const ICE = mix(CHROMA.BLUE.light, CHROMA.PETROL.light, 0.4);
/** Transparent checkerboard behind image-search results. */
export const CHECKERBOARD = `linear-gradient(45deg, ${BORDER_STRONG} 25%, transparent 25%), linear-gradient(-45deg, ${BORDER_STRONG} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${BORDER_STRONG} 75%), linear-gradient(-45deg, transparent 75%, ${BORDER_STRONG} 75%)`;
/* ────────────────────────────────────────────────────────────────────────────
* 6. STATUS & FEEDBACK
* -------------------------------------------------------------------------*/
export const ERROR = RED;
export const ERROR_HOVER = mix(RED, INK, 0.35);
export const LINK = BLUE;
/** Selection state for list rows. */
export const SELECT_BG = BLUE_TINT;
export const SELECT_BG_HOVER = alpha(CHROMA.BLUE.dark, 0.16);
export const SELECT_BORDER = BLUE_BORDER;
/** SortableJS drag ghost — also published as a CSS variable. */
export const SORT_GHOST = GREEN;
export const SORT_GHOST_BG = alpha(CHROMA.GREEN.dark, 0.12);
/** "Processing…" ribbon on image search results. */
export const INFO_RIBBON = alpha(CHROMA.BLUE.dark, 0.92);
/** Delicate fills used on top of panel colors. */
export const TINT_FAINT = shade(0.01);
export const TINT_HAIRLINE = shade(0.03);
export const TINT_SUBTLE = shade(0.04);
export const TINT_ROW_HOVER = shade(0.05);
export const TINT_CHIP = shade(0.06);
export const TINT_CARD_HOVER = shade(0.06);
export const TINT_PILL = shade(0.08);
export const TINT_STRONG = shade(0.12);
export const TINT_PILL_ACTIVE = shade(0.14);
/** Darkening for white text/icons sitting on imagery. */
export const SCRIM_SOFT = shade(0.35);
export const SCRIM_MEDIUM = shade(0.45);
export const SCRIM_BADGE = shade(0.5);
export const SCRIM_BUTTON = shade(0.55);
export const SCRIM_ACTION = shade(0.6);
export const SCRIM_LABEL = shade(0.65);
export const SCRIM_STRONG = shade(0.75);
export const SCRIM_HEAVY = shade(0.85);
export const SCRIM_INK = shade(0.95);
/** White overlays: hover states on dark chrome, text on imagery. */
export const VEIL_FAINT = veil(0.04);
export const VEIL_SUBTLE = veil(0.05);
export const VEIL_CHIP = veil(0.08);
export const VEIL_HOVER = veil(0.15);
export const VEIL_DISABLED = veil(0.3);
export const VEIL_MUTED = veil(0.35);
export const VEIL_PANEL = veil(0.55);
export const VEIL_TEXT = veil(0.8);
export const VEIL_CARD = veil(0.85);
export const VEIL_CARD_STRONG = veil(0.9);
export const VEIL_OVERLAY = veil(0.92);
export const VEIL_SOLID = veil(1);
/* ────────────────────────────────────────────────────────────────────────────
* 7. DARK-MODE PAIRS (resolve with `byMode(theme, PAIR)`)
* -------------------------------------------------------------------------*/
export const SURFACE_SUBTLE_PAIR = { light: TINT_HAIRLINE, dark: VEIL_FAINT };
export const TABLE_HEADER_PAIR = { light: TINT_SUBTLE, dark: VEIL_SUBTLE };
export const CODE_INLINE_PAIR = { light: TINT_CHIP, dark: VEIL_CHIP };
export const CODE_BLOCK_PAIR = { light: TINT_CHIP, dark: SCRIM_SOFT };
export const ASK_PROMPT_BG_PAIR = { light: BLUE_BG, dark: BLUE_TINT };
export const ASK_PROMPT_BORDER_PAIR = { light: BLUE_PANEL, dark: BLUE_BORDER };
/* ────────────────────────────────────────────────────────────────────────────
* 8. SIDEBAR NAVIGATION
* -------------------------------------------------------------------------*/
/** Deep petrol — the app's structural dark. */
export const NAV_SURFACE = mix(DARK_6, CHROMA.PETROL.dark, 0.3);
/** Horizontal sheen across the sidebar. */
export const NAV_BACKGROUND = `linear-gradient(90deg, ${mix(NAV_SURFACE, DARK_6, 0.4)} 0%, ${mix(NAV_SURFACE, WHITE, 0.06)} 25%, ${NAV_SURFACE} 60%, ${mix(NAV_SURFACE, DARK_6, 0.3)} 100%)`;
export const NAV_ITEM = mix(WHITE, CHROMA.PETROL.dark, 0.45);
export const NAV_ITEM_HOVER = mix(DARK_5, CHROMA.PETROL.dark, 0.3);
export const NAV_ITEM_HOVER_TEXT = mix(WHITE, CHROMA.PETROL.dark, 0.1);
export const NAV_ITEM_ACTIVE = mix(DARK_5, CHROMA.PETROL.dark, 0.55);
export const NAV_ITEM_ACTIVE_TEXT = mix(CHROMA.PETROL.light, WHITE, 0.15);
/** Accent for active items and live draft badges. */
export const NAV_ITEM_ACTIVE_ACCENT = CHROMA.PETROL.light;
/* ────────────────────────────────────────────────────────────────────────────
* 9. CHAT / AI ASSISTANT
* -------------------------------------------------------------------------*/
export const CHAT = CHROMA.BLUE.dark;
export const CHAT_BUSY = CHROMA.ORANGE.dark;
export const CHAT_PENDING = mix(CHROMA.ORANGE.dark, CHROMA.RED.dark, 0.5);
/** Pulsing "assistant needs input" halo on the FAB. */
export const CHAT_FAB_PENDING = `linear-gradient(135deg, ${CHAT_BUSY} 0%, ${CHAT_PENDING} 100%)`;
/** Busy ("thinking") FAB. */
export const CHAT_FAB_BUSY = `linear-gradient(135deg, ${CHAT_BUSY} 0%, ${mix(CHAT_BUSY, INK, 0.25)} 100%)`;
/** Matching drop shadows, so the FAB appears to float in the active color. */
export const CHAT_FAB_SHADOW_IDLE = `0 8px 24px ${alpha(CHAT, 0.35)}`;
export const CHAT_FAB_SHADOW_BUSY = `0 8px 24px ${alpha(CHAT_BUSY, 0.35)}`;
export const CHAT_FAB_SHADOW_PENDING = `0 8px 24px ${alpha(CHAT_PENDING, 0.45)}`;
/* ────────────────────────────────────────────────────────────────────────────
* 10. GRADIENTS & SHADOWS
* -------------------------------------------------------------------------*/
/** Bottom scrims that keep captions legible over photos. */
export const SCRIM_TILE = `linear-gradient(transparent, ${SCRIM_LABEL})`;
export const SCRIM_ATTACHMENT = `linear-gradient(transparent, ${SCRIM_STRONG} 40%, ${SCRIM_HEAVY})`;
/** Text glow for tile captions drawn over imagery. */
export const TEXT_GLOW_DARK = `0 1px 3px ${SCRIM_INK}, 0 0 2px ${SCRIM_HEAVY}`;
export const TEXT_GLOW_LIGHT = `0 0 3px ${VEIL_SOLID}, 0 0 6px ${VEIL_CARD_STRONG}, 0 1px 2px ${VEIL_CARD_STRONG}`;
export const SHADOW_CARD = `0 1px 4px ${TINT_CARD_HOVER}`;
export const SHADOW_DIALOG = `0 20px 40px ${shade(0.2)}`;
export const SHADOW_POPOVER = `0 8px 28px ${shade(0.2)}`;
export const SHADOW_BADGE = `0 2px 5px ${SCRIM_SOFT}`;
export const SHADOW_DRAG = `0 8px 20px ${shade(0.28)}`;
export const SHADOW_SORTABLE = `0 4px 16px ${TINT_STRONG}`;
export const SHADOW_DRAWER = `-8px 0 32px ${TINT_STRONG}`;
export const SHADOW_THUMB_RING = `0 0 0 1px ${TINT_STRONG}`;
export const SHADOW_TAB = `drop-shadow(0px 2px 4px ${shade(0.2)}) drop-shadow(0px 4px 5px ${TINT_PILL_ACTIVE}) drop-shadow(0px 1px 10px ${TINT_STRONG})`;
export const SHADOW_BUBBLE_USER = `0 2px 8px ${BLUE_SHADOW}`;
export const SHADOW_BUBBLE_ASSISTANT = `0 1px 4px ${TINT_ROW_HOVER}`;
/* ────────────────────────────────────────────────────────────────────────────
* 11. CSS VARIABLE BRIDGE
* `src/index.css` cannot import this module, so these few tokens are
* published as `var(--df-*)` custom properties by `installCssVars()`.
* -------------------------------------------------------------------------*/
export const CSS_VARS = {
'--df-main-background': MAIN_BACKGROUND,
'--df-sort-ghost': SORT_GHOST,
'--df-sort-ghost-tint': SORT_GHOST_BG,
'--df-shadow-sortable': SHADOW_SORTABLE,
'--df-shadow-drag': SHADOW_DRAG,
};
/** Publish {@link CSS_VARS} onto `:root`. Call once at startup. */
export function installCssVars(target = document.documentElement) {
for (const [name, value] of Object.entries(CSS_VARS)) {
target.style.setProperty(name, value);
}
}