feat(webmail): the new interface - logo, dark and light themes, command menu

A mark (a lambda on an ultramarine tile with the flap of an envelope), the
favicon, home-screen icons and an install manifest. Every colour is a variable
defined once per theme, and the Tailwind scales the screens already use read
them, so dark (the default) and light are a change of values and not of
classes. Geist is served by the app itself. Ctrl+K opens a command menu for
folders, actions, accounts and appearance, and typing something that matches no
command searches the mail; c writes a message and / goes to the search field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 22:33:15 -03:00
1 parent 6b97807856
commit e667843b59
38 files changed
+1161 -316

No files matched your search

+43 -90
View File
@@ -1,83 +1,31 @@
/**
* Nocturne, expressed as Tailwind scales.
* The interface's colours, as Tailwind scales that read CSS variables.
*
* The redesign replaces two visual languages with one: the webmail's indigo and
* the console's green both become the single blurple accent, and every grey
* comes from one perceptual ramp instead of Tailwind's stock slate. Rather than
* rename several thousand utility classes across the screens, the scales those
* classes already name are remapped onto the design system's own values - so
* `bg-slate-900`, `text-emerald-400` and `border-indigo-500/30` all resolve
* into Nocturne, and a colour cannot enter the interface from outside it.
*
* The values here are the same ones globals.css carries as CSS variables.
* The values live in globals.css, once per theme. The scales the screens
* already name - slate for neutrals, indigo for the accent, amber, rose and
* emerald for the signals - point at those variables, so switching theme
* changes what `text-slate-400` is without touching a class. `ink` is the
* colour that sits on the ground (white on dark, near-black on light): hover
* tints and hairlines are `ink` at low opacity.
*
* @type {import('tailwindcss').Config}
*/
const ramp = (name) =>
Object.fromEntries(
[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map((step) => [
step,
`rgb(var(--${name}-${step}) / <alpha-value>)`,
]),
);
// The neutral ramp, ordered the way Tailwind expects (light to dark) so the
// existing `text-slate-400` / `bg-slate-900` idiom keeps meaning what it meant.
const neutral = {
50: '#f3f5fe',
100: '#e9e9ed', // body text
200: '#e4e7f5',
300: '#cfd3e5',
400: '#9397ab', // muted text
500: '#75798c',
600: '#595d6c',
700: '#3f424d', // borders
800: '#232532', // card
900: '#1b1c28', // panel
950: '#161826', // page ground
};
// The one accent, in the product's own blurple (OKLCH hue 289).
const accent = {
50: '#faf9ff',
100: '#f5f4ff',
200: '#e7e5fe',
300: '#d2cefd',
400: '#b5abfc',
500: '#9184d9',
600: '#796cbf',
700: '#5d5294',
800: '#423a6a',
900: '#2b2741',
950: '#211d33',
};
// Warning and danger are the only other hues, and both are desaturated: on this
// ground a saturated amber or rose reads as an error in the design, not in the
// data.
const warn = {
50: '#fbf8ee',
100: '#f6efd8',
200: '#ecdfb2',
300: '#dfcb8b',
400: '#cbb46a',
500: '#b39b52',
600: '#8f7b41',
700: '#6b5c31',
800: '#473d21',
900: '#2f2917',
950: '#1f1b10',
};
const bad = {
50: '#fcf3f4',
100: '#f8e3e5',
200: '#f0c8cc',
300: '#e2a8ae',
400: '#d08a92',
500: '#b96f78',
600: '#985860',
700: '#74434a',
800: '#4e2e33',
900: '#3a2a2d',
950: '#241a1c',
};
const neutral = ramp('n');
const accent = ramp('a');
const warn = ramp('w');
const bad = ramp('b');
const good = ramp('g');
module.exports = {
darkMode: 'class',
darkMode: ['selector', "[data-theme='dark']"],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
@@ -90,46 +38,51 @@ module.exports = {
gray: neutral,
zinc: neutral,
neutral: neutral,
// Every accent name in the codebase collapses onto the one accent. The
// console's emerald and teal are the point of the exercise: the two
// surfaces are one product and now look like it.
// Every blue, violet and teal name in the codebase is the one accent.
indigo: accent,
violet: accent,
purple: accent,
emerald: accent,
teal: accent,
cyan: accent,
sky: accent,
blue: accent,
brand: accent,
// Green means answered, trusted and healthy, and nothing else.
emerald: good,
teal: good,
green: good,
amber: warn,
yellow: warn,
orange: warn,
rose: bad,
red: bad,
pink: bad,
ink: 'rgb(var(--ink) / <alpha-value>)',
// The solid fill of the one main action on a screen.
fill: {
DEFAULT: 'rgb(var(--a-fill) / <alpha-value>)',
hover: 'rgb(var(--a-fill-hover) / <alpha-value>)',
},
dark: {
rail: '#12131f',
bg: '#161826',
panel: '#1b1c28',
card: '#232532',
border: 'rgba(233, 233, 237, 0.09)',
rail: 'rgb(var(--rail) / <alpha-value>)',
bg: 'rgb(var(--n-950) / <alpha-value>)',
panel: 'rgb(var(--n-900) / <alpha-value>)',
card: 'rgb(var(--n-800) / <alpha-value>)',
border: 'rgb(var(--ink) / 0.09)',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['ui-monospace', 'Menlo', 'Consolas', 'monospace'],
sans: ['"Geist Variable"', 'Geist', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['ui-monospace', '"SF Mono"', 'Menlo', 'Consolas', 'monospace'],
},
borderRadius: {
xl: '12px',
'2xl': '14px',
'2xl': '16px',
},
boxShadow: {
// On a dark ground elevation is an edge plus ambient darkness, never a
// stack of coloured glows.
edge: 'inset 0 0 0 1px rgba(233, 233, 237, 0.09)',
'edge-accent': 'inset 0 0 0 1px rgba(145, 132, 217, 0.4)',
lift: '0 0 0 1px #3f424d, 0 16px 40px rgba(0, 0, 0, 0.5)',
// Elevation is a hairline plus ambient shade; never a coloured glow.
edge: 'inset 0 0 0 1px rgb(var(--ink) / 0.09)',
'edge-accent': 'inset 0 0 0 1px rgb(var(--a-500) / 0.45)',
lift: '0 0 0 1px rgb(var(--ink) / 0.12), var(--lm-shadow)',
},
},
},