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:
1 parent
6b97807856
commit
e667843b59
38 files changed
+1161
-316
No files matched your search
@@ -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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user