/** * ============================================================================ * 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..dark` → text, icons, borders, solid buttons. * `CHROMA..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; /** Bright green for text/highlights over dark surfaces/scrims. */ export const GREEN_BRIGHT = CHROMA.GREEN.light; 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 — kept very soft, no hard banding. */ export const NAV_BACKGROUND = `linear-gradient(90deg, ${mix(NAV_SURFACE, DARK_6, 0.16)} 0%, ${mix(NAV_SURFACE, WHITE, 0.04)} 25%, ${NAV_SURFACE} 60%, ${mix(NAV_SURFACE, DARK_6, 0.12)} 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; /** * Sub-navigation (2nd level) sits on the light app background, not on the * dark sidebar — so its active surface only needs a mid dip to let the light * accent read. Well short of the fully deep `NAV_ITEM_ACTIVE`. */ export const NAV_SUB_ACTIVE = mix(MAIN_BACKGROUND, NAV_ITEM_ACTIVE, 0.55); export const NAV_SUB_ACTIVE_HOVER = mix(MAIN_BACKGROUND, NAV_ITEM_ACTIVE, 0.55); /* ──────────────────────────────────────────────────────────────────────────── * 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}`; /* ──────────────────────────────────────────────────────────────────────────── * 10b. DIALOG FOG * Shared backdrop for every dialog (`src/components/AppDialog.jsx`). * Darker and more bluish than `MAIN_BACKGROUND`: deep-navy solid base * plus a translucent variant for the MUI backdrop layer. The solid * variant is also published as `--df-dialog-fog` so the * scrollbar-gutter placeholder can switch to the fog color while a * dialog is open (`html.df-dialog-open` in `src/index.css`). * -------------------------------------------------------------------------*/ /** Raw deep-navy base tint used for the dialog fog backdrop. */ export const DIALOG_FOG_NAVY = mix(DARK_6, CHROMA.BLUE.dark, 0.42); /** Translucent backdrop wash painted over the page. */ export const DIALOG_FOG = alpha(DIALOG_FOG_NAVY, 0.72); /** * Solid fog color seen when DIALOG_FOG (72% opacity) is drawn over MAIN_BACKGROUND. * Published as `--df-dialog-fog` so the reserved scrollbar-gutter strip on * `html.df-dialog-open` seamlessly matches the translucent fog backdrop over the app. */ export const DIALOG_FOG_SOLID = mix(MAIN_BACKGROUND, DIALOG_FOG_NAVY, 0.72); /* ──────────────────────────────────────────────────────────────────────────── * 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-dialog-fog': DIALOG_FOG_SOLID, '--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); } }