mirror of
https://github.com/coder/coder.git
synced 2026-11-01 12:53:26 +08:00
This is part 1 to lay the foundation for the theme changes. Part 2 which adds the UI implementation is in this PR, https://github.com/coder/coder/pull/24680 ----------------- Adds four sitewide colorblind-friendly theme palettes alongside the existing light and dark themes. The palettes retune the red/green and blue/yellow semantic axes so success/error, warning, and diff additions/deletions remain distinguishable under the most common color vision deficiencies. | Preference ID | Purpose | |---|---| | `dark-protan-deuter` / `light-protan-deuter` | Protanopia & deuteranopia (red/green). Success and additions shift to sky-blue; destructive and deletions shift to vermilion/orange; warning shifts to fuchsia so warning and destructive states do not collapse onto the same hue. | | `dark-tritan` / `light-tritan` | Tritanopia (blue/yellow). Warning shifts from amber to fuchsia; red/green semantic pair is preserved. | The diff panel and every semantic role (`success`, `error`, `warning`, `notice`, `danger`) pick up the new palette automatically because they consume the sitewide CSS variables in `site/src/index.css`. No backend or database change is required: `theme_preference` is already a free-form `text` column. The existing `"dark"`, `"light"`, and `"auto"` preferences are unchanged. This PR ships the palettes and the resolution machinery (`CONCRETE_THEMES`, `resolveThemeName`, `isConcreteThemeName`, and the ThemeProvider/AgentEmbedPage plumbing). It intentionally does **not** add UI to select the new themes; the follow-up PR #24680 adds a Theme mode dropdown (Sync with system / Single theme) that exposes every concrete theme, including the four added here. Produced with Coder Agents assistance. <details> <summary>Implementation plan and decision log</summary> Full plan (investigation, file layout, TDD phases, open risks) is attached to the chat that produced this PR. Key decisions: - **Sitewide, not agent-scoped.** The diff panel already consumes the sitewide theme via CSS variables. Keeping the change at the user appearance layer also fixes red/green accents in alerts, badges, and build states in one change, and matches how comparable products (e.g. GitHub) ship this feature. - **No backend change.** `codersdk/users.go` already accepts any `theme_preference` string, and `ThemeProvider` now has a shared resolver (`resolveThemeName`) that maps any persisted value to a concrete theme, tolerating unknowns and the legacy `"auto"` value. - **Palette provenance.** The protan/deuter palette is inspired by CVD-safe blue/orange palettes and tuned within the existing Tailwind color scales; the tritan palette keeps red/green semantics intact and shifts warning to fuchsia. This PR does not claim exact Okabe-Ito or WCAG AA derivation without recorded contrast data. - **UI deferred.** Selecting the new themes is gated on the follow-up PR #24680 which replaces the flat theme grid with a Theme mode dropdown. </details>
370 lines
11 KiB
CSS
370 lines
11 KiB
CSS
/*
|
||
To enable opacity in colors, please use the new HSL format.
|
||
Related issue: https://github.com/shadcn-ui/ui/issues/805#issuecomment-1616021820
|
||
*/
|
||
|
||
@tailwind base;
|
||
@tailwind components;
|
||
@tailwind utilities;
|
||
|
||
@font-face {
|
||
font-family: "seti";
|
||
src: url("/seti.woff") format("woff");
|
||
font-weight: normal;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
|
||
@layer components {
|
||
@media (max-width: 767px) {
|
||
/*
|
||
Full-width mobile dropdowns. We set a --mobile-dropdown-bottom
|
||
custom property on the chat input container so the dropdown
|
||
position tracks the actual input box, not a hardcoded offset.
|
||
*/
|
||
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown) {
|
||
position: fixed !important;
|
||
left: 1rem !important;
|
||
width: calc(100vw - 2rem) !important;
|
||
min-width: 0 !important;
|
||
transform: none !important;
|
||
bottom: var(--mobile-dropdown-bottom, 5rem) !important;
|
||
top: auto !important;
|
||
}
|
||
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown-top) {
|
||
bottom: auto !important;
|
||
top: var(--mobile-dropdown-top, 3.5rem) !important;
|
||
}
|
||
[data-radix-popper-content-wrapper]:has(
|
||
> .mobile-full-width-dropdown-top-below-header
|
||
) {
|
||
bottom: auto !important;
|
||
top: 5rem !important;
|
||
}
|
||
.mobile-full-width-dropdown {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
max-width: none !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
@layer base {
|
||
:root,
|
||
.light {
|
||
--content-primary: 240 10% 4%;
|
||
--content-secondary: 240 5% 34%;
|
||
--content-link: 221 83% 53%;
|
||
--content-invert: 0 0% 98%;
|
||
--content-disabled: 240 5% 65%;
|
||
--content-success: 142 72% 29%;
|
||
--content-warning: 27 96% 61%;
|
||
--content-destructive: 0 84% 60%;
|
||
--surface-primary: 0 0% 100%;
|
||
--surface-secondary: 240 5% 96%;
|
||
--surface-tertiary: 240 6% 90%;
|
||
--surface-quaternary: 240 5% 84%;
|
||
--surface-invert-primary: 240 4% 16%;
|
||
--surface-invert-secondary: 240 5% 26%;
|
||
--surface-destructive: 0 93% 94%;
|
||
--surface-green: 141 79% 85%;
|
||
--surface-grey: 240 5% 96%;
|
||
--surface-orange: 34 100% 92%;
|
||
--surface-sky: 191, 67%, 89%;
|
||
--surface-red: 0 93% 94%;
|
||
--surface-purple: 259, 100%, 95%;
|
||
--surface-magenta: 289 100% 98%;
|
||
--border-purple: 255 92% 76%;
|
||
--border-default: 240 6% 90%;
|
||
--border-success: 142 76% 36%;
|
||
--border-sky: 203 90% 40%;
|
||
--border-green: 138 82% 82%;
|
||
--border-magenta: 295, 68%, 46%;
|
||
--border-warning: 30.66, 97.16%, 72.35%;
|
||
--border-destructive: 0 84% 60%;
|
||
--border-secondary: 240 5% 65%;
|
||
--overlay-default: 240 5% 84% / 80%;
|
||
--radius: 0.5rem;
|
||
--highlight-purple: 271, 61%, 35%;
|
||
--highlight-green: 143 64% 24%;
|
||
--highlight-orange: 30 100% 32%;
|
||
--highlight-sky: 195, 61%, 22%;
|
||
--highlight-red: 0 74% 42%;
|
||
--highlight-magenta: 295, 68%, 40%;
|
||
--syntax-key: 211 95% 33%;
|
||
--syntax-string: 0 77% 36%;
|
||
--syntax-number: 158 88% 28%;
|
||
--syntax-boolean: 240 100% 50%;
|
||
--git-added: 142 72% 29%;
|
||
--git-deleted: 0 74% 42%;
|
||
--git-modified: 17 88% 40%;
|
||
--git-merged: 271 91% 65%;
|
||
--git-added-bright: 142 72% 29%;
|
||
--git-deleted-bright: 0 74% 42%;
|
||
--git-merged-bright: 272 72% 47%;
|
||
--surface-git-added: 141 84% 93%;
|
||
--surface-git-deleted: 0 93% 94%;
|
||
--surface-git-merged: 269 100% 95%;
|
||
--border: 240 5.9% 90%;
|
||
--input: 240 5.9% 90%;
|
||
--ring: 240 10% 3.9%;
|
||
/*
|
||
shadcn-compatible aliases consumed by streamdown and
|
||
internal components (e.g. shimmer). These names are
|
||
intentionally generic so they match shadcn conventions.
|
||
Do NOT rename without updating every consumer.
|
||
*/
|
||
--background: var(--surface-primary);
|
||
--foreground: var(--content-primary);
|
||
--muted: var(--surface-secondary);
|
||
--muted-foreground: var(--content-secondary);
|
||
--primary: var(--content-link);
|
||
--primary-foreground: var(--surface-primary);
|
||
--avatar-lg: 2.5rem;
|
||
--avatar-default: 2rem;
|
||
--avatar-sm: 1.5rem;
|
||
}
|
||
.dark {
|
||
--content-primary: 0 0% 100%;
|
||
--content-secondary: 240 5% 65%;
|
||
--content-link: 213 94% 68%;
|
||
--content-invert: 240 10% 4%;
|
||
--content-disabled: 240 5% 26%;
|
||
--content-success: 142 76% 36%;
|
||
--content-warning: 31 97% 72%;
|
||
--content-destructive: 0 91% 71%;
|
||
--surface-primary: 240 10% 4%;
|
||
--surface-secondary: 240 6% 10%;
|
||
--surface-tertiary: 240 4% 16%;
|
||
--surface-quaternary: 240 5% 26%;
|
||
--surface-invert-primary: 240 6% 90%;
|
||
--surface-invert-secondary: 240 5% 65%;
|
||
--surface-destructive: 0 75% 15%;
|
||
--surface-green: 145 80% 10%;
|
||
--surface-grey: 240 6% 10%;
|
||
--surface-orange: 13 81% 15%;
|
||
--surface-sky: 196, 67%, 12%;
|
||
--surface-red: 0 75% 15%;
|
||
--surface-purple: 268, 68%, 17%;
|
||
--surface-magenta: 291, 74%, 15%;
|
||
--border-purple: 255 92% 76%;
|
||
--border-default: 240 4% 16%;
|
||
--border-success: 142 76% 36%;
|
||
--border-magenta: 292, 100%, 78%;
|
||
--border-warning: 30.66, 97.16%, 72.35%;
|
||
--border-destructive: 0 91% 71%;
|
||
--border-sky: 194 90% 62%;
|
||
--border-green: 143 77% 87%;
|
||
--border-secondary: 240 5% 26%;
|
||
--overlay-default: 240 10% 4% / 80%;
|
||
--highlight-purple: 269, 100%, 74%;
|
||
--highlight-green: 141 79% 85%;
|
||
--highlight-orange: 31 100% 70%;
|
||
--highlight-sky: 188, 75%, 80%;
|
||
--highlight-red: 0 91% 71%;
|
||
--highlight-magenta: 292, 100%, 78%;
|
||
--syntax-key: 201 98% 80%;
|
||
--syntax-string: 18 47% 64%;
|
||
--syntax-number: 100 28% 73%;
|
||
--syntax-boolean: 207 61% 59%;
|
||
--git-added: 142 77% 73%;
|
||
--git-deleted: 0 94% 82%;
|
||
--git-modified: 31 97% 72%;
|
||
--git-merged: 271 91% 65%;
|
||
--git-added-bright: 142 71% 45%;
|
||
--git-deleted-bright: 0 91% 71%;
|
||
--git-merged-bright: 270 95% 75%;
|
||
--surface-git-added: 145 80% 10%;
|
||
--surface-git-deleted: 0 75% 15%;
|
||
--surface-git-merged: 274 87% 21%;
|
||
--border: 240 3.7% 15.9%;
|
||
--input: 240 3.7% 15.9%;
|
||
--ring: 240 4.9% 83.9%;
|
||
/* shadcn-compatible aliases – see :root block for details. */
|
||
--background: var(--surface-primary);
|
||
--foreground: var(--content-primary);
|
||
--muted: var(--surface-secondary);
|
||
--muted-foreground: var(--content-secondary);
|
||
--primary: var(--content-link);
|
||
--primary-foreground: var(--surface-primary);
|
||
}
|
||
/*
|
||
Colorblind-friendly variants. ThemeProvider applies these classes
|
||
alongside the base mode class (`dark` or `light`), so unchanged
|
||
variables cascade from the base mode and each block only overrides
|
||
the semantic colors that shift for colorblind accessibility.
|
||
|
||
Palette rationale:
|
||
- dark-protan-deuter / light-protan-deuter: shift the red/green
|
||
success+error axis onto sky-blue (success) + vermilion/orange
|
||
(destructive). Warning shifts to fuchsia so it does not collide
|
||
with destructive states on the orange axis.
|
||
- dark-tritan / light-tritan: keep the red/green success+error
|
||
axis intact and move warning from amber to fuchsia because
|
||
amber and sky-blue blur together under tritanopia.
|
||
*/
|
||
.light-protan-deuter {
|
||
--content-success: 199 89% 48%;
|
||
--content-warning: 322 81% 43%;
|
||
--content-destructive: 24 95% 53%;
|
||
--surface-destructive: 34 100% 92%;
|
||
--surface-green: 201 94% 86%;
|
||
--surface-orange: 289 100% 98%;
|
||
--surface-red: 34 100% 92%;
|
||
--border-success: 199 89% 48%;
|
||
--border-green: 201 94% 86%;
|
||
--border-warning: 322 81% 60%;
|
||
--border-destructive: 24 95% 53%;
|
||
--highlight-green: 201 94% 36%;
|
||
--highlight-orange: 322 81% 43%;
|
||
--highlight-red: 24 95% 42%;
|
||
--syntax-string: 24 95% 42%;
|
||
--syntax-number: 199 89% 38%;
|
||
--git-added: 199 89% 48%;
|
||
--git-deleted: 24 95% 53%;
|
||
--git-modified: 271 91% 45%;
|
||
--git-added-bright: 199 89% 48%;
|
||
--git-deleted-bright: 24 95% 53%;
|
||
--surface-git-added: 204 94% 94%;
|
||
--surface-git-deleted: 33 100% 93%;
|
||
}
|
||
.dark-protan-deuter {
|
||
--content-success: 199 82% 67%;
|
||
--content-warning: 322 81% 67%;
|
||
--content-destructive: 27 96% 67%;
|
||
--surface-destructive: 17 75% 15%;
|
||
--surface-green: 201 80% 14%;
|
||
--surface-orange: 322 70% 15%;
|
||
--surface-red: 17 75% 15%;
|
||
--border-success: 199 82% 67%;
|
||
--border-warning: 322 81% 67%;
|
||
--border-destructive: 27 96% 67%;
|
||
--border-green: 201 94% 86%;
|
||
--highlight-green: 201 94% 86%;
|
||
--highlight-orange: 322 81% 67%;
|
||
--highlight-red: 27 96% 67%;
|
||
--git-added: 199 82% 67%;
|
||
--git-deleted: 27 96% 67%;
|
||
--git-modified: 271 91% 65%;
|
||
--git-added-bright: 199 89% 48%;
|
||
--git-deleted-bright: 24 95% 53%;
|
||
--surface-git-added: 201 80% 14%;
|
||
--surface-git-deleted: 17 75% 15%;
|
||
}
|
||
.light-tritan {
|
||
--content-warning: 322 81% 43%;
|
||
--surface-orange: 289 100% 98%;
|
||
--border-warning: 322 81% 60%;
|
||
--highlight-orange: 322 81% 43%;
|
||
--highlight-magenta: 322, 81%, 43%;
|
||
--syntax-boolean: 322 81% 43%;
|
||
--git-modified: 322 81% 43%;
|
||
}
|
||
.dark-tritan {
|
||
--content-warning: 322 81% 67%;
|
||
--surface-orange: 322 70% 15%;
|
||
--surface-magenta: 322 70% 15%;
|
||
--border-magenta: 322 81% 72%;
|
||
--border-warning: 322 81% 67%;
|
||
--highlight-orange: 322 81% 67%;
|
||
--highlight-magenta: 322 81% 72%;
|
||
--syntax-boolean: 322 81% 67%;
|
||
--git-modified: 322 81% 72%;
|
||
}
|
||
}
|
||
|
||
@layer base {
|
||
* {
|
||
@apply border-border;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#root,
|
||
#storybook-root {
|
||
@apply h-full;
|
||
}
|
||
|
||
button,
|
||
input {
|
||
@apply font-sans;
|
||
}
|
||
|
||
input:-webkit-autofill,
|
||
input:-webkit-autofill:hover,
|
||
input:-webkit-autofill:focus,
|
||
input:-webkit-autofill:active {
|
||
-webkit-box-shadow: 0 0 0 100px hsl(var(--surface-primary)) inset !important;
|
||
}
|
||
|
||
::placeholder {
|
||
@apply text-content-secondary;
|
||
}
|
||
|
||
fieldset {
|
||
@apply border-none p-0 m-0 w-full;
|
||
}
|
||
|
||
/* Global scrollbar color baseline: surface-quaternary thumb on
|
||
transparent track. scrollbar-color is inherited, so this cascades
|
||
to all scroll containers. We intentionally do NOT set
|
||
scrollbar-width globally because any non-auto value opts out of
|
||
overlay scrollbars on macOS/Chromium, causing a permanent gutter
|
||
on every container (even when content does not overflow). */
|
||
:root {
|
||
scrollbar-color: hsl(var(--surface-quaternary)) transparent;
|
||
}
|
||
|
||
/* Prevent layout shift when modals open by maintaining scrollbar width.
|
||
scrollbar-gutter: stable reserves space for the scrollbar so Radix's
|
||
scroll-bar compensation (margin-right/padding-right) is unnecessary
|
||
and causes double spacing. We zero it out with !important to win
|
||
over Radix's injected !important styles. The @supports guard ensures
|
||
we only do this on browsers that actually support scrollbar-gutter.
|
||
|
||
There’s a related issue on GitHub: Radix UI Primitives Issue #3251
|
||
https://github.com/radix-ui/primitives/issues/3251
|
||
*/
|
||
@supports (scrollbar-gutter: stable) {
|
||
html {
|
||
scrollbar-gutter: stable;
|
||
}
|
||
|
||
html body[data-scroll-locked] {
|
||
--removed-body-scroll-bar-size: 0 !important;
|
||
margin-right: 0 !important;
|
||
overflow-y: scroll !important;
|
||
}
|
||
}
|
||
|
||
/*
|
||
This is a temporary fix for MUI Modals/Popovers until they are removed.
|
||
When html has scrollbar-gutter: stable, the browser reserves space for the scrollbar.
|
||
MUI Modals/Popovers, when locking body scroll, add `overflow: hidden` and `padding-right`
|
||
to the body to compensate for the scrollbar they are hiding. This added padding-right
|
||
conflicts with the already reserved gutter space, causing a layout shift.
|
||
This rule overrides MUI's added padding-right on the body specifically when MUI
|
||
is likely to have set both overflow:hidden and padding-right.
|
||
*/
|
||
body[style*="overflow: hidden"][style*="padding-right"] {
|
||
padding-right: 0px;
|
||
}
|
||
}
|
||
|
||
@layer utilities {
|
||
.text-xs-mono {
|
||
@apply text-xs font-mono font-medium;
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
scroll-behavior: auto !important;
|
||
}
|
||
}
|