u
This commit is contained in:
348
src/colors.js
348
src/colors.js
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user