app/theme-presets.css
Rama main · réplica de lectura del repositorio original.
/* ── Theme presets (MIN-696) ──────────────────────────────────────────────
One palette per mode, chosen on the account (`lib/theme-presets.ts`). The
pre-paint script writes both choices onto <html> before the first paint:
<html class="dark" data-theme-light="nord-light" data-theme-dark="nord-dark">
and the mode class decides which one is read. A light palette is keyed on
`html:not(.dark)`, a dark one on `html.dark`, so switching modes (or the OS
switching under "system") lands on the palette saved for that mode.
Specificity is deliberate: `html:not(.dark)[…]` and `html.dark[…]` weigh
(0,2,1), above every mode-level override in globals.css — `:root`, `.dark`
and the app shell's `.dark[data-app-shell]` (0,2,0). That is why the
presets live in their own file without having to come last.
The minddy presets have NO rule here: an absent or default attribute leaves
app/tokens.css untouched, pixel for pixel, with its lavender accent
(MIN-840). An unknown id, such as the retired `violet-*`, falls back to it.
Only neutral surfaces, text, focus rings and the primary action move.
Borders are not set here: app/tokens.css draws them as a light veil of
`--foreground`, so each palette tints them with its own text color at the
same weight. Only the high-contrast presets draw them heavier. Status, priority, destructive, project, mention and rich-text colors
keep their meaning in every palette. Values are plain hexadecimal so
`lib/theme-presets.test.ts` can measure each pair's WCAG contrast: normal
text 4.5:1, focus ring 3:1, and 7:1 text with 3:1 borders for the
high-contrast presets. */
/* ── Accent presets ── only the accent color changes: primary actions and
the focus ring. Every surface — content, sidebar, top and bottom bars,
tabs, selection — keeps minddy's own neutral values, and `--brand` stays
lavender.
"Noir" (light) and "Blanc" (dark) are the ink actions minddy had before
lavender became its default: near-black buttons in light mode, near-white
ones in dark mode, with a neutral focus ring. */
html:not(.dark)[data-theme-light="ink-light"] {
--primary: #16181e;
--primary-foreground: #ffffff;
--ring: #555555;
--sidebar-primary: #16181e;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #555555;
}
html.dark[data-theme-dark="ink-dark"] {
--primary: #f4f5f8;
--primary-foreground: #0f1216;
--ring: #b7b7b7;
--sidebar-primary: #f4f5f8;
--sidebar-primary-foreground: #0f1216;
--sidebar-ring: #b7b7b7;
}
html:not(.dark)[data-theme-light="blue-light"] {
--primary: #2563eb;
--primary-foreground: #ffffff;
--ring: #2563eb;
--sidebar-primary: #2563eb;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #2563eb;
}
html.dark[data-theme-dark="blue-dark"] {
--primary: #60a5fa;
--primary-foreground: #0a0a0a;
--ring: #60a5fa;
--sidebar-primary: #60a5fa;
--sidebar-primary-foreground: #0a0a0a;
--sidebar-ring: #60a5fa;
}
html:not(.dark)[data-theme-light="green-light"] {
--primary: #15803d;
--primary-foreground: #ffffff;
--ring: #15803d;
--sidebar-primary: #15803d;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #15803d;
}
html.dark[data-theme-dark="green-dark"] {
--primary: #4ade80;
--primary-foreground: #0a0a0a;
--ring: #4ade80;
--sidebar-primary: #4ade80;
--sidebar-primary-foreground: #0a0a0a;
--sidebar-ring: #4ade80;
}
html:not(.dark)[data-theme-light="orange-light"] {
--primary: #c2410c;
--primary-foreground: #ffffff;
--ring: #c2410c;
--sidebar-primary: #c2410c;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #c2410c;
}
html.dark[data-theme-dark="orange-dark"] {
--primary: #fb923c;
--primary-foreground: #0a0a0a;
--ring: #fb923c;
--sidebar-primary: #fb923c;
--sidebar-primary-foreground: #0a0a0a;
--sidebar-ring: #fb923c;
}
html:not(.dark)[data-theme-light="rose-light"] {
--primary: #e11d48;
--primary-foreground: #ffffff;
--ring: #e11d48;
--sidebar-primary: #e11d48;
--sidebar-primary-foreground: #ffffff;
--sidebar-ring: #e11d48;
}
html.dark[data-theme-dark="rose-dark"] {
--primary: #fb7185;
--primary-foreground: #0a0a0a;
--ring: #fb7185;
--sidebar-primary: #fb7185;
--sidebar-primary-foreground: #0a0a0a;
--sidebar-ring: #fb7185;
}
/* ── Solarized Light ── base3 / base2 surfaces, base02 ink, blue focus. */
html:not(.dark)[data-theme-light="solarized-light"] {
--background: #fdf6e3;
--foreground: #073642;
--card: #fdf6e3;
--card-foreground: #073642;
--popover: #fdf6e3;
--popover-foreground: #073642;
--primary: #073642;
--primary-foreground: #fdf6e3;
--secondary: #eee8d5;
--secondary-foreground: #073642;
--muted: #eee8d5;
--muted-foreground: #4f646b;
--accent: #e9e2cc;
--accent-foreground: #073642;
--input: #e2dbc4;
--ring: #268bd2;
--sidebar: #eee8d5;
--sidebar-foreground: #073642;
--sidebar-primary: #073642;
--sidebar-primary-foreground: #fdf6e3;
--sidebar-accent: #e4ddc6;
--sidebar-accent-foreground: #073642;
--sidebar-border: #e2dbc4;
--sidebar-ring: #268bd2;
--control: #f4eddb;
--control-hover: #e9e2cc;
--surface-raised: #fdf6e3;
--surface-sunken: #eee8d5;
--surface-glass: rgb(238 232 213 / 60%);
--text-tertiary: #4f646b;
}
/* ── Catppuccin Latte ── base / mantle / crust, text, mauve action. */
html:not(.dark)[data-theme-light="catppuccin-latte"] {
--background: #eff1f5;
--foreground: #4c4f69;
--card: #eff1f5;
--card-foreground: #4c4f69;
--popover: #eff1f5;
--popover-foreground: #4c4f69;
--primary: #8839ef;
--primary-foreground: #ffffff;
--secondary: #e6e9ef;
--secondary-foreground: #4c4f69;
--muted: #e6e9ef;
--muted-foreground: #5c5f77;
--accent: #dce0e8;
--accent-foreground: #4c4f69;
--input: #ccd0da;
--ring: #1e66f5;
--sidebar: #e6e9ef;
--sidebar-foreground: #4c4f69;
--sidebar-primary: #8839ef;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #dce0e8;
--sidebar-accent-foreground: #4c4f69;
--sidebar-border: #ccd0da;
--sidebar-ring: #1e66f5;
--control: #e6e9ef;
--control-hover: #dce0e8;
--surface-raised: #eff1f5;
--surface-sunken: #e6e9ef;
--surface-glass: rgb(230 233 239 / 60%);
--text-tertiary: #5c5f77;
}
/* ── Nord Light ── Snow Storm surfaces, Polar Night ink, Frost action. */
html:not(.dark)[data-theme-light="nord-light"] {
--background: #eceff4;
--foreground: #2e3440;
--card: #eceff4;
--card-foreground: #2e3440;
--popover: #eceff4;
--popover-foreground: #2e3440;
--primary: #4a6b94;
--primary-foreground: #ffffff;
--secondary: #e5e9f0;
--secondary-foreground: #2e3440;
--muted: #e5e9f0;
--muted-foreground: #4c566a;
--accent: #d8dee9;
--accent-foreground: #2e3440;
--input: #d8dee9;
--ring: #5e81ac;
--sidebar: #e5e9f0;
--sidebar-foreground: #2e3440;
--sidebar-primary: #4a6b94;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #d8dee9;
--sidebar-accent-foreground: #2e3440;
--sidebar-border: #d8dee9;
--sidebar-ring: #5e81ac;
--control: #e5e9f0;
--control-hover: #d8dee9;
--surface-raised: #eceff4;
--surface-sunken: #e5e9f0;
--surface-glass: rgb(229 233 240 / 60%);
--text-tertiary: #4c566a;
}
/* ── High contrast (light) ── pure black on white, borders that read. */
html:not(.dark)[data-theme-light="high-contrast-light"] {
--background: #ffffff;
--foreground: #000000;
--card: #ffffff;
--card-foreground: #000000;
--popover: #ffffff;
--popover-foreground: #000000;
--primary: #000000;
--primary-foreground: #ffffff;
--secondary: #ebebeb;
--secondary-foreground: #000000;
--muted: #ebebeb;
--muted-foreground: #333333;
--accent: #e0e0e0;
--accent-foreground: #000000;
--border: #767676;
--input: #767676;
--ring: #0047b3;
--sidebar: #f0f0f0;
--sidebar-foreground: #000000;
--sidebar-primary: #000000;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #dcdcdc;
--sidebar-accent-foreground: #000000;
--sidebar-border: #767676;
--sidebar-ring: #0047b3;
--control: #ebebeb;
--control-hover: #dcdcdc;
--surface-raised: #ffffff;
--surface-sunken: #f0f0f0;
--surface-glass: rgb(240 240 240 / 95%);
--text-tertiary: #333333;
--scrollbar-thumb: rgb(0 0 0 / 45%);
--scrollbar-thumb-hover: rgb(0 0 0 / 65%);
}
/* ── Solarized Dark ── base03 content, base02 chrome, base2 ink. */
html.dark[data-theme-dark="solarized-dark"] {
--background: #002b36;
--foreground: #eee8d5;
--card: #002b36;
--card-foreground: #eee8d5;
--popover: #002b36;
--popover-foreground: #eee8d5;
--primary: #eee8d5;
--primary-foreground: #002b36;
--secondary: #0a3d4a;
--secondary-foreground: #eee8d5;
--muted: #073642;
--muted-foreground: #93a1a1;
--accent: #0d4351;
--accent-foreground: #eee8d5;
--input: #1a4c58;
--ring: #268bd2;
--sidebar: #073642;
--sidebar-foreground: #eee8d5;
--sidebar-primary: #eee8d5;
--sidebar-primary-foreground: #002b36;
--sidebar-accent: #0d4351;
--sidebar-accent-foreground: #eee8d5;
--sidebar-border: #164651;
--sidebar-ring: #268bd2;
--control: #0a3d4a;
--control-hover: #114957;
--surface-raised: #073642;
--surface-sunken: #00222b;
--surface-glass: rgb(7 54 66 / 60%);
--text-tertiary: #93a1a1;
}
/* ── Catppuccin Mocha ── base / mantle / crust, text, mauve action. */
html.dark[data-theme-dark="catppuccin-mocha"] {
--background: #1e1e2e;
--foreground: #cdd6f4;
--card: #1e1e2e;
--card-foreground: #cdd6f4;
--popover: #181825;
--popover-foreground: #cdd6f4;
--primary: #cba6f7;
--primary-foreground: #11111b;
--secondary: #313244;
--secondary-foreground: #cdd6f4;
--muted: #313244;
--muted-foreground: #a6adc8;
--accent: #313244;
--accent-foreground: #cdd6f4;
--input: #45475a;
--ring: #b4befe;
--sidebar: #181825;
--sidebar-foreground: #cdd6f4;
--sidebar-primary: #cba6f7;
--sidebar-primary-foreground: #11111b;
--sidebar-accent: #313244;
--sidebar-accent-foreground: #cdd6f4;
--sidebar-border: #313244;
--sidebar-ring: #b4befe;
--control: #313244;
--control-hover: #45475a;
--surface-raised: #1e1e2e;
--surface-sunken: #11111b;
--surface-glass: rgb(24 24 37 / 60%);
--text-tertiary: #9399b2;
}
/* ── Nord ── Polar Night surfaces, Snow Storm ink, Frost action. */
html.dark[data-theme-dark="nord-dark"] {
--background: #2e3440;
--foreground: #eceff4;
--card: #2e3440;
--card-foreground: #eceff4;
--popover: #2e3440;
--popover-foreground: #eceff4;
--primary: #88c0d0;
--primary-foreground: #2e3440;
--secondary: #3b4252;
--secondary-foreground: #eceff4;
--muted: #3b4252;
--muted-foreground: #b4bccb;
--accent: #434c5e;
--accent-foreground: #eceff4;
--input: #434c5e;
--ring: #88c0d0;
--sidebar: #272c36;
--sidebar-foreground: #eceff4;
--sidebar-primary: #88c0d0;
--sidebar-primary-foreground: #2e3440;
--sidebar-accent: #3b4252;
--sidebar-accent-foreground: #eceff4;
--sidebar-border: #3b4252;
--sidebar-ring: #88c0d0;
--control: #3b4252;
--control-hover: #434c5e;
--surface-raised: #3b4252;
--surface-sunken: #242933;
--surface-glass: rgb(46 52 64 / 60%);
--text-tertiary: #b4bccb;
}
/* ── High contrast (dark) ── pure white on black, borders that read. */
html.dark[data-theme-dark="high-contrast-dark"] {
--background: #000000;
--foreground: #ffffff;
--card: #000000;
--card-foreground: #ffffff;
--popover: #0d0d0d;
--popover-foreground: #ffffff;
--primary: #ffffff;
--primary-foreground: #000000;
--secondary: #1f1f1f;
--secondary-foreground: #ffffff;
--muted: #1f1f1f;
--muted-foreground: #d4d4d4;
--accent: #2b2b2b;
--accent-foreground: #ffffff;
--border: #8c8c8c;
--input: #8c8c8c;
--ring: #66b3ff;
--sidebar: #0d0d0d;
--sidebar-foreground: #ffffff;
--sidebar-primary: #ffffff;
--sidebar-primary-foreground: #000000;
--sidebar-accent: #2b2b2b;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #8c8c8c;
--sidebar-ring: #66b3ff;
--control: #1f1f1f;
--control-hover: #2e2e2e;
--surface-raised: #0d0d0d;
--surface-sunken: #000000;
--surface-glass: rgb(13 13 13 / 95%);
--text-tertiary: #d4d4d4;
--scrollbar-thumb: rgb(255 255 255 / 45%);
--scrollbar-thumb-hover: rgb(255 255 255 / 65%);
}
/* Code blocks carry a fixed GitHub-like surface (globals.css). Under a
preset they take the palette's sunken surface instead, so a Solarized page
does not hold a gray box; the syntax colors stay the GitHub ones, which
read on every light (resp. dark) surface above. */
/* Accent presets keep minddy's surfaces, so they keep its code blocks too. */
:is(
html:not(.dark):is(
[data-theme-light="solarized-light"],
[data-theme-light="catppuccin-latte"],
[data-theme-light="nord-light"],
[data-theme-light="high-contrast-light"]
),
html.dark:is(
[data-theme-dark="solarized-dark"],
[data-theme-dark="catppuccin-mocha"],
[data-theme-dark="nord-dark"],
[data-theme-dark="high-contrast-dark"]
)
) .code-block-node-view {
--code-block-background: var(--surface-sunken);
--code-block-foreground: var(--foreground);
--code-block-muted: var(--muted-foreground);
--code-block-hover: color-mix(in oklab, var(--foreground) 8%, transparent);
}
/* ── Transparency off ─────────────────────────────────────────────────────
The account can turn the glass of floating surfaces off (Account →
Preferences). Popovers, menus, selects and the command palette then paint
their opaque `--popover`, and no backdrop blur runs anywhere in the app.
The high-contrast presets imply it: a menu showing the page through itself
loses exactly the contrast those presets exist to guarantee. */
:is(
html[data-transparency="off"],
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is(
[data-slot="popover-content"],
[data-slot="dropdown-menu-content"],
[data-slot="dropdown-menu-sub-content"],
[data-slot="select-content"],
[data-floating-surface],
[role="menu"],
.desktop-palette-overlay > .desktop-palette-surface
) {
background-color: var(--popover);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
/* Pseudo-elements cannot sit inside `:is()`, hence three selectors. */
:is(
html[data-transparency="off"],
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) *,
:is(
html[data-transparency="off"],
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) *::before,
:is(
html[data-transparency="off"],
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) *::after {
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
}
/* ── High contrast: boundaries the borderless design leaves out ────────────
minddy draws controls and surfaces without borders on purpose; separation
comes from soft fills that high contrast cannot rely on (a #ebebeb input
on white is 1.2:1). These rules apply ONLY under the two high-contrast
palettes and add what WCAG 1.4.11 asks of them: a visible boundary of at
least 3:1 for controls and floating surfaces, a visible keyboard focus —
The field and select primitives declare `outline-none ring-0` — and a
current/selected state that does not depend on a faint background.
Outlines rather than borders: they take no layout space, so nothing moves
in this mode either, and they follow `border-radius`. Unlayered, they win
over the Tailwind utilities (`outline-none`, `outline-hidden`) that live in
the utilities layer. `--border` is #767676 / #8c8c8c here (≥ 4.5:1). */
/* Controls: fields, selects, outline buttons, switches, input groups. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is(
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"],
[data-slot="input-group"],
[data-slot="command-input-wrapper"],
[data-slot="switch"],
button[data-variant="outline"],
/* App-built fields (rich-text composers, filters, chips) share the same
borderless fill rather than a slot. */
[class~="bg-control"],
textarea,
input:is([type="text"], [type="search"], [type="email"], [type="password"], [type="url"], [type="number"], :not([type])):not([data-slot="input-group-control"])
) {
outline: 1px solid var(--border);
outline-offset: -1px;
}
/* An unchecked switch's white thumb sat on a near-white track. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) [data-slot="switch-thumb"] {
box-shadow: 0 0 0 1px var(--border);
}
/* Floating surfaces, dialogs and sheets: their edge, not their shadow. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is(
[data-slot="popover-content"],
[data-slot="dropdown-menu-content"],
[data-slot="dropdown-menu-sub-content"],
[data-slot="select-content"],
[data-slot="hover-card-content"],
[data-slot="tooltip-content"],
[data-slot="dialog-content"],
[data-slot="sheet-content"],
[data-slot="side-panel-content"],
[data-floating-surface],
[role="menu"],
.desktop-palette-surface
) {
outline: 1px solid var(--border);
outline-offset: -1px;
}
/* Scrims dim the page enough to read the dialog as the only active layer. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is([data-slot="dialog-overlay"], [data-slot="sheet-overlay"], [data-slot="side-panel-overlay"]) {
background-color: rgb(0 0 0 / 55%);
}
/* Translucent borders (`border-border/60`, an issue card's
`border-foreground/10`…) lose the contrast the palette gives `--border`:
draw them at full strength. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is([class*="border-border/"], [data-card-surface]) {
border-color: var(--border);
}
/* Tabs: every tab gets an edge; the selected one a strong one. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) .app-tab {
outline: 1px solid var(--border);
outline-offset: -1px;
}
/* Current, selected and highlighted states: a 2px ink outline, readable
whatever the fill underneath — sidebar page, selected tab, pressed
segment, highlighted menu / select / command-palette row. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :is(
[aria-current]:not([aria-current="false"]),
[role="tab"][aria-selected="true"],
[aria-pressed="true"],
[role="option"][aria-selected="true"],
[data-slot="command-item"][data-selected="true"]:not(:where(html[data-app-layout="mobile"] *)),
[data-highlighted],
[data-slot="select-item"]:focus,
[role="menuitem"]:focus,
[role="menuitemradio"]:focus,
[role="menuitemcheckbox"]:focus
) {
outline: 2px solid var(--foreground);
outline-offset: -2px;
}
/* Keyboard focus, everywhere: the focus ring color, outside the element.
Menu rows and options keep the inset outline above — outside, their
scrolling list would clip it. Containers that take focus when they open
(dialogs, panels, menus) keep their 1px edge instead of a frame. */
:is(
html:not(.dark)[data-theme-light="high-contrast-light"],
html.dark[data-theme-dark="high-contrast-dark"]
) :focus-visible:not(
[role^="menuitem"], [role="option"], [data-slot="select-item"],
[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"],
[data-slot$="-content"], .desktop-palette-surface
) {
outline: 2px solid var(--ring);
outline-offset: 1px;
}