feat(themes): Built-in Solarized and One light/dark themes (#85)

Adds Solarized Light, Solarized Dark, One Light and One Dark as built-in themes across the workbench, editor, terminal and Mermaid, keeping the original palettes (MIT notices in THIRD_PARTY_LICENSES.md, mapping in docs/themes.md).

- Native window colours are read from tokens.css at build time (`theme_frame.rs`) instead of a second colour table.
- Startup restores the built-in palette before first paint.
- Editor schemes, the terminal and Mermaid refresh on switches between themes of the same kind.
- The plugin packages of the same themes are removed.
- A test requires every built-in palette theme to define each token (only listed ones may inherit from the base theme), so new tokens can't silently miss them.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
Walter WangтаClaude authored and GitHub committed 2026-10-27 18:53:12 +08:00
1 джерело 50308a956d
коміт 3b608355a3
48 files changed
+1267 -1351

No files matched your search

+83 -11
Переглянути файл
@@ -100,17 +100,6 @@ c → cpp, php_only), node names adapted to the bundled grammar versions
definitions re-kinded as `field` / `method`, and missing scopes, definitions and
references added; each file's header notes its changes.
## Theme plugin palettes
The sample theme plugins port MIT-licensed palettes; each package's `LICENSE`
holds the full notice and source revisions:
- `plugins/theme-solarized/`: [Solarized](https://github.com/altercation/solarized),
Copyright (c) 2011 Ethan Schoonover.
- `plugins/theme-one/`: Atom [One Light](https://github.com/atom/one-light-syntax) /
[One Dark](https://github.com/atom/one-dark-syntax) syntax themes, Copyright (c) 2016
GitHub Inc., and their UI themes, Copyright (c) 2014 GitHub Inc.
---
Apache License
@@ -168,3 +157,86 @@ If the Work includes a "NOTICE" text file as part of its distribution, then any
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
## Solarized
The Solarized palettes in `src/styles/tokens.css` are adapted from
[Ethan Schoonover’s Solarized](https://github.com/altercation/solarized/tree/62f656a02f93c5190a8753159e34b385588d5ff3).
See [theme sources and mapping](docs/themes.md). MIT license:
```text
Copyright (c) 2011 Ethan Schoonover
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## Atom One Light and One Dark
The One palettes and semantic syntax mapping in `src/styles/tokens.css` are
adapted from GitHub’s Atom One syntax and UI themes.
Source revisions are listed in [theme sources and mapping](docs/themes.md).
Both syntax themes use the following MIT notice:
```text
Copyright (c) 2016 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
Both UI themes use the following MIT notice:
```text
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
+66 -41
Переглянути файл
@@ -22,6 +22,33 @@ fn sample(name: &str) -> PathBuf {
repo().join("plugins").join(name)
}
fn theme_template() -> PathBuf {
repo().join("templates/theme-plugin")
}
/// A theme package with a light and a dark theme, plus licence files, in `dir`.
fn write_theme_pair(dir: &Path) {
std::fs::create_dir_all(dir.join("t")).unwrap();
std::fs::write(
dir.join("plugin.toml"),
"id = \"theme-pair\"\nname = \"Pair\"\nversion = \"1.0.0\"\nopenidea-api = \"0.1\"\n\
[[contributes.themes]]\npath = \"t/day.json\"\n[[contributes.themes]]\npath = \"t/night.json\"\n",
)
.unwrap();
std::fs::write(
dir.join("t/day.json"),
r##"{"id":"day","name":"Day","kind":"light","tokens":{"--bg-frame":"#eeeeee"}}"##,
)
.unwrap();
std::fs::write(
dir.join("t/night.json"),
r##"{"id":"night","name":"Night","kind":"dark","tokens":{"--bg-frame":"#111111"}}"##,
)
.unwrap();
std::fs::write(dir.join("LICENSE"), "MIT License\n").unwrap();
std::fs::write(dir.join("NOTICE"), "Palette notice\n").unwrap();
}
fn misbehave_pkg() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/misbehave")
}
@@ -217,11 +244,11 @@ fn runtime_language_and_theme_registration() {
#[test]
fn contribution_only_plugins() {
let f = fixture(Limits::default());
let theme = f.host.install_from_dir(&sample("theme-solarized")).unwrap();
let theme = f.host.install_from_dir(&theme_template()).unwrap();
assert_eq!(theme.status, PluginStatus::Static);
assert!(!theme.has_code);
let ids: Vec<&str> = theme.themes.iter().map(|t| t.id.as_str()).collect();
assert_eq!(ids, ["solarized-light", "solarized-dark"]);
assert_eq!(ids, ["harbor-night"]);
let lang = f.host.install_from_dir(&sample("language-toy")).unwrap();
assert_eq!(lang.status, PluginStatus::Static);
assert_eq!(lang.languages[0].id, "toy");
@@ -229,14 +256,14 @@ fn contribution_only_plugins() {
assert!(f.run("language-toy", "x", None).unwrap_err().contains("no code"));
f.take_events();
let active = f.host.set_theme(Some(("theme-solarized", "solarized-dark"))).unwrap().unwrap();
assert_eq!(active.theme.tokens["--bg-app"], "#002b36");
assert_eq!(active.theme.tokens["--syn-string"], "#2aa198");
let active = f.host.set_theme(Some(("theme-harbor-night", "harbor-night"))).unwrap().unwrap();
assert_eq!(active.theme.tokens["--bg-app"], "#14171c");
assert_eq!(active.theme.tokens["--syn-string"], "#9ccc7a");
assert!(f.take_events().iter().any(|(_, e)| matches!(e, PluginEvent::ThemeChanged { theme: Some(_) })));
assert_eq!(f.host.list(None).active_theme, Some(active));
// Disabling the plugin withdraws its theme and language.
f.host.set_enabled("theme-solarized", false).unwrap();
f.host.set_enabled("theme-harbor-night", false).unwrap();
assert!(f.host.active_theme().is_none());
assert!(f.take_events().iter().any(|(_, e)| *e == PluginEvent::ThemeChanged { theme: None }));
f.host.set_enabled("language-toy", false).unwrap();
@@ -247,7 +274,7 @@ fn contribution_only_plugins() {
#[test]
fn theme_template_installs_with_its_editor_scheme() {
let f = fixture(Limits::default());
let info = f.host.install_from_dir(&repo().join("templates/theme-plugin")).unwrap();
let info = f.host.install_from_dir(&theme_template()).unwrap();
assert_eq!(info.status, PluginStatus::Static);
assert!(!info.has_code);
assert_eq!(info.themes[0].id, "harbor-night");
@@ -263,44 +290,42 @@ fn theme_template_installs_with_its_editor_scheme() {
}
#[test]
fn bundled_theme_plugins_install_with_their_licence() {
fn theme_packages_install_with_their_licence() {
let f = fixture(Limits::default());
for (pkg, themes) in
[("theme-solarized", ["solarized-light", "solarized-dark"]), ("theme-one", ["one-light", "one-dark"])]
{
let info = f.host.install_from_dir(&sample(pkg)).unwrap();
assert_eq!(info.status, PluginStatus::Static, "{pkg}: {:?}", info.error);
assert!(info.authors.iter().any(|a| a.starts_with("bghtyu")));
let license = std::fs::read_to_string(f.plugins_dir.join(pkg).join("LICENSE")).unwrap();
assert!(license.contains("MIT License"));
for (theme, kind) in themes.iter().zip([ThemeKind::Light, ThemeKind::Dark]) {
let active = f.host.set_theme(Some((pkg, theme))).unwrap().unwrap();
assert_eq!(active.theme.kind, kind);
let tokens = &active.theme.tokens;
assert!(["--bg-frame", "--syn-keyword", "--ansi-red"].iter().all(|t| tokens.contains_key(*t)));
}
let dir = tempfile::tempdir().unwrap();
write_theme_pair(dir.path());
let info = f.host.install_from_dir(dir.path()).unwrap();
assert_eq!(info.status, PluginStatus::Static, "{:?}", info.error);
let installed = f.plugins_dir.join("theme-pair");
assert!(std::fs::read_to_string(installed.join("LICENSE")).unwrap().contains("MIT License"));
assert!(installed.join("NOTICE").is_file());
for (theme, kind) in [("day", ThemeKind::Light), ("night", ThemeKind::Dark)] {
let active = f.host.set_theme(Some(("theme-pair", theme))).unwrap().unwrap();
assert_eq!(active.theme.kind, kind);
}
}
#[test]
fn theme_frame_follows_the_selection() {
let f = fixture(Limits::default());
f.host.install_from_dir(&sample("theme-one")).unwrap();
let dir = tempfile::tempdir().unwrap();
write_theme_pair(dir.path());
f.host.install_from_dir(dir.path()).unwrap();
// Nothing to attach it to without a selected theme.
f.host.set_theme_frame(Some("#21252b")).unwrap();
f.host.set_theme_frame(Some("#111111")).unwrap();
assert_eq!(f.host.startup_frame(), None);
f.host.set_theme(Some(("theme-one", "one-dark"))).unwrap();
f.host.set_theme(Some(("theme-pair", "night"))).unwrap();
assert!(f.host.set_theme_frame(Some("hsl(220, 13%, 15%)")).is_err());
f.host.set_theme_frame(Some("#21252b")).unwrap();
assert_eq!(f.host.startup_frame().as_deref(), Some("#21252b"));
f.host.set_enabled("theme-one", false).unwrap();
f.host.set_theme_frame(Some("#111111")).unwrap();
assert_eq!(f.host.startup_frame().as_deref(), Some("#111111"));
f.host.set_enabled("theme-pair", false).unwrap();
assert_eq!(f.host.startup_frame(), None);
f.host.set_enabled("theme-one", true).unwrap();
assert_eq!(f.host.startup_frame().as_deref(), Some("#21252b"));
f.host.set_enabled("theme-pair", true).unwrap();
assert_eq!(f.host.startup_frame().as_deref(), Some("#111111"));
// A new selection drops the old theme's colour until the UI reports the new one.
f.host.set_theme(Some(("theme-one", "one-light"))).unwrap();
f.host.set_theme(Some(("theme-pair", "day"))).unwrap();
assert_eq!(f.host.startup_frame(), None);
f.host.set_theme_frame(Some("#eaeaeb")).unwrap();
f.host.set_theme_frame(Some("#eeeeee")).unwrap();
f.host.set_theme(None).unwrap();
assert_eq!(f.host.startup_frame(), None);
}
@@ -327,14 +352,14 @@ fn theme_with_broken_editor_scheme_fails_to_load() {
fn state_persists_across_restarts() {
let f = fixture(Limits::default());
f.host.install_from_dir(&sample("hello")).unwrap();
f.host.install_from_dir(&sample("theme-solarized")).unwrap();
f.host.install_from_dir(&theme_template()).unwrap();
f.host.set_enabled("hello", false).unwrap();
f.host.set_theme(Some(("theme-solarized", "solarized-dark"))).unwrap();
f.host.set_theme_frame(Some("#073642")).unwrap();
f.host.set_theme(Some(("theme-harbor-night", "harbor-night"))).unwrap();
f.host.set_theme_frame(Some("#1f252e")).unwrap();
let again = PluginHost::new(f.plugins_dir.clone(), Arc::new(|_: Option<&str>, _| {}), Limits::default()).unwrap();
// Known before the plugins are loaded: new windows open in the theme's frame colour.
assert_eq!(again.startup_frame().as_deref(), Some("#073642"));
assert_eq!(again.startup_frame().as_deref(), Some("#1f252e"));
assert!(!again.wait_initial_load(Duration::from_millis(1)));
again.load_all();
assert!(again.wait_initial_load(Duration::ZERO));
@@ -342,7 +367,7 @@ fn state_persists_across_restarts() {
let hello = list.plugins.iter().find(|p| p.id == "hello").unwrap();
assert!(!hello.enabled);
assert_eq!(hello.status, PluginStatus::Disabled);
assert_eq!(list.active_theme.unwrap().theme.id, "solarized-dark");
assert_eq!(list.active_theme.unwrap().theme.id, "harbor-night");
assert!(
again
.run_command("hello", "insert-date", None, Invocation::default())
@@ -436,16 +461,16 @@ fn catalog_lists_sample_packages() {
std::fs::create_dir_all(tmp.path().join("junk")).unwrap();
std::fs::write(tmp.path().join("junk/plugin.toml"), "garbage = ").unwrap();
let catalog = scan_catalog(&[repo().join("plugins"), tmp.path().to_path_buf()]);
let catalog = scan_catalog(&[repo().join("plugins"), repo().join("templates"), tmp.path().to_path_buf()]);
let ids: Vec<&str> = catalog.iter().map(|c| c.id.as_str()).collect();
for id in ["hello", "theme-solarized", "theme-one", "language-toy", "x"] {
for id in ["hello", "theme-harbor-night", "language-toy", "x"] {
assert!(ids.contains(&id), "{id} missing from {ids:?}");
}
let hello = catalog.iter().find(|c| c.id == "hello").unwrap();
assert!(hello.has_code && hello.problem.is_none() && hello.change_notes.is_some());
assert_eq!(hello.commands.len(), 2);
let theme = catalog.iter().find(|c| c.id == "theme-solarized").unwrap();
assert_eq!((theme.theme_count, theme.has_code), (2, false));
let theme = catalog.iter().find(|c| c.id == "theme-harbor-night").unwrap();
assert_eq!((theme.theme_count, theme.has_code), (1, false));
let x = catalog.iter().find(|c| c.id == "x").unwrap();
assert!(x.problem.as_deref().unwrap().contains("plugin.wasm"));
}
Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 169 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 133 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 171 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 135 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 164 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 129 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 168 KiB

Бінарний файл не відображається.

Після

Ширина:  |  Висота:  |  Розмір: 131 KiB

+5 -30
Переглянути файл
@@ -1,7 +1,8 @@
# Theme plugins
OpenIDEA ships three built-in themes (Islands Light, Islands Dark, High
Contrast). Everything else comes from **theme plugins**, the same model as
OpenIDEA ships seven [built-in themes](../themes.md): Islands Light, Islands Dark,
High Contrast, Solarized Light, Solarized Dark, One Light and One Dark. Additional
themes come from **theme plugins**, the same model as
IntelliJ IDEA, where most themes are plugins rather than part of the IDE.
A theme plugin is data only. Like an IntelliJ theme (a `.theme.json` plus an
@@ -16,31 +17,6 @@ my-theme/
└─ my-theme.scheme.json # editor colours, optional (IntelliJ: .icls)
```
## Ready-made theme plugins
The repository's [`plugins/`](../../plugins) folder holds two complete theme
plugins, each with a light and a dark theme and an editor colour scheme
(syntax, editor and terminal ANSI colours):
| Package | Themes | Palette |
|---|---|---|
| [`plugins/theme-solarized`](../../plugins/theme-solarized) | Solarized Light, Solarized Dark | [Solarized](https://github.com/altercation/solarized) by Ethan Schoonover |
| [`plugins/theme-one`](../../plugins/theme-one) | One Light, One Dark | Atom [One Light](https://github.com/atom/one-light-syntax) / [One Dark](https://github.com/atom/one-dark-syntax) by GitHub |
Both palettes are MIT-licensed; each package carries the notice in its
`LICENSE`, which is installed with it. To use one:
1. *Settings › Plugins › ⚙ › Install Plugin from Disk…* and pick the package
folder (`plugins/theme-solarized` or `plugins/theme-one` of a checkout or a
downloaded copy). Development builds also list both on the *Available*
tab, where *Install* does the same.
2. Select the plugin and press *Apply* next to a theme.
The IDE remembers the theme: the next start-up opens its windows, splash and
workbench in the theme's colours before the plugin host has loaded.
## Writing a theme plugin
The quickest way to start is to copy [`templates/theme-plugin`](../../templates/theme-plugin).
It contains *Harbor Night*, a complete example dark theme that sets every
themeable token. The rest of this page covers the file format, the token
@@ -307,9 +283,8 @@ A `fontStyle: "italic"` on comments maps to `"--syn-comment-style": "italic"`.
- Text should keep at least WCAG AA contrast (4.5:1) on its surfaces. The
built-in themes are checked by `src/features/settings/__tests__/contrast.test.ts`.
- Repository checks (for changes to this repo): `pnpm vitest run scripts/theme`
validates the template and the theme plugins in `plugins/` against
tokens.css and keeps this reference current, and `cargo test -p ide-plugin`
loads them through the plugin host.
validates the template against tokens.css and keeps this reference current,
and `cargo test -p ide-plugin` loads the template through the plugin host.
## Token reference
+100
Переглянути файл
@@ -0,0 +1,100 @@
# Built-in theme sources
OpenIDEA includes Islands Light, Islands Dark, High Contrast, Solarized Light,
Solarized Dark, One Light and One Dark. Select them in Settings › Appearance,
Welcome › Customize or the existing quick theme menu. The same themes are
available as explicit editor schemes; “Theme default” follows the UI theme.
“Sync with OS” uses the existing preferred light/dark theme settings. Existing
ids (`light`, `dark`, `dark-hc`, `system`) and attribute overrides remain valid.
## Upstream palettes and licenses
All sources below are the original authors’ repositories, pinned for reproducibility.
Full MIT notices are preserved in [THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md).
| Theme | Original source revision | Palette / mapping source |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Solarized Light / Dark | [Ethan Schoonover, 62f656a](https://github.com/altercation/solarized/tree/62f656a02f93c5190a8753159e34b385588d5ff3) | README’s sixteen exact sRGB hex colours; `vim-colors-solarized/colors/solarized.vim` semantic roles; iTerm2 ANSI slots |
| One Light syntax | [GitHub / Atom, d845790](https://github.com/atom/one-light-syntax/tree/d84579027410c576086dfca14d934c4bd74b0438) | `styles/colors.less`, `styles/syntax-variables.less` |
| One Dark syntax | [GitHub / Atom, 9c96f44](https://github.com/atom/one-dark-syntax/tree/9c96f4454362267ac45322063e193ccf9d2debb1) | `styles/colors.less`, `styles/syntax-variables.less` |
| One Light UI | [GitHub / Atom, a27bce3](https://github.com/atom/one-light-ui/tree/a27bce35088ecd323e44f5c2510bcf2d4e9efcf8) | `styles/ui-variables.less`, `styles/ui-variables-custom.less` |
| One Dark UI | [GitHub / Atom, 18c2143](https://github.com/atom/one-dark-ui/tree/18c2143d41e17b337fff6dc7024562c4d999be17) | `styles/ui-variables.less`, `styles/ui-variables-custom.less` |
## Mapping into OpenIDEA
`src/platform/themes.ts` registers the stable ids. `data-theme` retains the
light/dark kind for icons, native controls and existing selectors;
`data-color-theme` selects the additional palette. High Contrast continues to
use `data-contrast="high"`. Plugin themes clear the built-in palette attribute
and retain their existing light/dark fallback; removing a plugin restores the
chosen built-in palette.
`src/styles/tokens.css` contains the shared mapping for all colour tokens and
four palette blocks. No new dependency is required. Solarized uses base3/base2
for light backgrounds and base03/base02 for dark backgrounds, base00/base0 for
text, and the original eight accents. Vim’s statements are green, constants
and strings cyan, identifiers blue, types yellow, and comments the original
subdued base1/base01. Solarized’s original ANSI slot order is preserved (the
bright slots intentionally include orange, violet and neutrals).
One retains the exact HSL definitions from Atom, avoiding rounded palette
approximations: 230° / 1% / 98% light and 220° / 13% / 18% dark editor backgrounds;
purple keywords, green strings, blue functions, red variables/tags, orange
numbers and attributes, yellow classes, and the original mono-3 comments.
UI frame/popup levels use Atom’s relative background lightness formulas with
the matching syntax theme. OpenIDEA’s shared UI text uses the syntax foreground
and mono-2 for readable secondary labels. Selection and cursor-line values
follow Atom’s syntax variables. Atom does not define a terminal ANSI palette:
One’s ANSI slots reuse its exact semantic syntax hues, repeating them for bright
colours rather than inventing a different palette. One Light maps ANSI white
and bright white to its mono-2 and mono-1 text colours so they remain visible
on the light terminal background.
Components without an upstream equivalent (feedback banners, tool window states,
search fills and focus decoration) use the original semantic hues, neutrals,
or explicit `color-mix()` tints. This adapts colours to the existing layout;
it does not recreate Atom’s or Vim’s UI. Platform traffic-light colours stay
platform colours. Original subdued comments and some syntax accents have lower
contrast than a high-contrast accessibility theme; High Contrast remains available.
Native windows open in the theme's frame colour before their page loads:
`src-tauri/src/theme_frame.rs` reads `--bg-frame` of each built-in theme from
`tokens.css` itself (resolving `var()` and `hsl()`), so there is no second
colour table to keep in sync. Elsewhere the frontend resolves HSL and
`color-mix()` tokens to RGB (`src/platform/cssColor.ts`) for Tauri's window
API, the native Settings window, Mermaid and the terminal.
`public/boot.js` restores the palette (`data-color-theme`) and the splash's
resolved frame, text and accent colours before the application stylesheet
loads, so a restart in One Dark or Solarized shows no flash of another theme.
The backend's settings persistence remains authoritative. Terminal, Mermaid
and editor-scheme observers also watch palette changes within the same
light/dark kind. Custom accents, explicit editor schemes, per-scheme attribute
overrides, compact mode and accessibility options retain their precedence.
`src/features/settings/__tests__/contrast.test.ts` requires every palette theme
to define every colour token of the base themes (in the shared mapping or its
own block), so a token added to `tokens.css` later can't silently fall back to
an Islands colour.
## Browser evidence
These approved screenshots are reused from the original theme implementation on
0.1.15; regression checks are rerun against the upstream contribution base.
Captured at 1400 × 900 in Linux Chromium 154 using OpenIDEA's public in-memory
`acme-api` project / Kotlin fixture and synthetic ANSI terminal output. The
terminal is a real xterm renderer, without a native PTY or shell. These images
cover the shell, sidebar, editor, gutter, find matches, terminal, Settings,
focused controls, disabled settings and the selected theme popup.
| Theme | Workbench, editor and terminal | Settings and theme popup |
|---|---|---|
| Solarized Light | [Screenshot](media/themes/solarized-light-workbench.png) | [Screenshot](media/themes/solarized-light-popup.png) |
| Solarized Dark | [Screenshot](media/themes/solarized-dark-workbench.png) | [Screenshot](media/themes/solarized-dark-popup.png) |
| One Light | [Screenshot](media/themes/one-light-workbench.png) | [Screenshot](media/themes/one-light-popup.png) |
| One Dark | [Screenshot](media/themes/one-dark-workbench.png) | [Screenshot](media/themes/one-dark-popup.png) |
The browser regression also checks refresh / startup cache restoration,
selection visibility, same-kind switches, the quick menu, existing themes,
explicit editor schemes and preferred OS themes. Native startup colours are
covered by Rust unit tests.
+188
Переглянути файл
@@ -0,0 +1,188 @@
import { expect, openFromTree, test } from "./fixtures";
import type { Page } from "@playwright/test";
test.setTimeout(60_000);
const palettes = [
{
id: "solarized-light",
label: "Solarized Light",
kind: "light",
bg: "rgb(253, 246, 227)",
fg: "rgb(101, 123, 131)",
frame: [238, 232, 213, 255],
},
{
id: "solarized-dark",
label: "Solarized Dark",
kind: "dark",
bg: "rgb(0, 43, 54)",
fg: "rgb(131, 148, 150)",
frame: [7, 54, 66, 255],
},
{
id: "one-light",
label: "One Light",
kind: "light",
bg: "rgb(250, 250, 250)",
fg: "rgb(56, 58, 66)",
frame: [234, 234, 235, 255],
},
{
id: "one-dark",
label: "One Dark",
kind: "dark",
bg: "rgb(40, 44, 52)",
fg: "rgb(171, 178, 191)",
frame: [33, 37, 43, 255],
},
];
async function choose(page: Page, control: string, label: string) {
await page.getByRole("button", { name: control, exact: true }).click();
await page.getByRole("option", { name: label, exact: true }).click();
}
async function settings(page: Page) {
await page.keyboard.press("Meta+,");
const dialog = page.getByRole("dialog", { name: "Settings", exact: true });
await expect(dialog).toBeVisible();
await dialog
.locator('[role="treeitem"][aria-level="2"]')
.filter({ hasText: /^Appearance$/ })
.click();
return dialog;
}
for (const palette of palettes) {
test(`${palette.label}: settings, editor, terminal, popup and reload`, async ({ workbench: page }, info) => {
const editor = await openFromTree(page, "build.gradle.kts");
await page.keyboard.press("Alt+F12");
await expect(page.locator(".xterm")).toBeVisible();
// Use only synthetic ANSI output; no shell, network or native PTY is involved.
await page.evaluate(async () => {
const path = "/src/features/terminal/store.ts";
const { activeSession } = await import(/* @vite-ignore */ path);
const session = activeSession();
session.term.clear();
session.term.writeln("Synthetic theme regression — no PTY");
session.term.writeln("\x1b[31merror\x1b[0m \x1b[33mwarning\x1b[0m \x1b[32msuccess\x1b[0m \x1b[34minfo\x1b[0m");
});
const dialog = await settings(page);
await choose(page, "Theme", palette.label);
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
await expect(page.locator("html")).toHaveAttribute("data-theme", palette.kind);
expect(
await page.evaluate(async () => {
const path = "/src/workbench/theme.ts";
const { windowBackgroundColor, windowBackgroundHex } = await import(/* @vite-ignore */ path);
return { rgba: windowBackgroundColor(), hex: windowBackgroundHex() };
}),
).toEqual({
rgba: palette.frame,
hex:
"#" +
palette.frame
.slice(0, 3)
.map((n) => n.toString(16).padStart(2, "0"))
.join(""),
});
await page.screenshot({ path: info.outputPath(`${palette.id}-settings.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await expect(editor).toHaveCSS("background-color", palette.bg);
await expect(editor).toHaveCSS("color", palette.fg);
await expect(editor.locator(".cm-gutters")).toHaveCSS("background-color", palette.bg);
await expect
.poll(() =>
page.evaluate(async () => {
const path = "/src/features/terminal/store.ts";
const { activeSession } = await import(/* @vite-ignore */ path);
return activeSession().term.options.theme.background;
}),
)
.toBe(
palette.id === "solarized-light"
? "#fdf6e3"
: palette.id === "solarized-dark"
? "#002b36"
: palette.id === "one-light"
? "#fafafa"
: "#282c34",
);
await editor.locator(".cm-content").click();
await page.keyboard.press("Meta+a");
await expect(editor.locator(".cm-selectionBackground").first()).toBeVisible();
await page.keyboard.press("ArrowRight");
await page.keyboard.press("Meta+f");
await page.getByRole("textbox", { name: "Find", exact: true }).fill("Order");
await expect(editor.locator(".cm-openidea-findMatch").first()).toBeVisible();
await page.screenshot({ path: info.outputPath(`${palette.id}-workbench.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await settings(page);
await page.getByRole("button", { name: "Theme", exact: true }).click();
await expect(page.getByRole("listbox", { name: "Theme", exact: true })).toBeVisible();
await page.screenshot({ path: info.outputPath(`${palette.id}-popup.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await page.keyboard.press("Escape");
await page.reload();
await expect(page.getByRole("tree", { name: "Project" })).toBeVisible();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
const cached = await page.evaluate(() => JSON.parse(localStorage.getItem("openidea.theme")!));
expect(cached).toMatchObject({ theme: palette.kind, colorTheme: palette.id });
});
}
test("same-kind switching, legacy themes, system mode and explicit editor schemes", async ({ workbench: page }) => {
await openFromTree(page, "build.gradle.kts");
const dialog = await settings(page);
for (const label of [
"Solarized Light",
"One Light",
"Solarized Dark",
"One Dark",
"Islands Light",
"Islands Dark",
"High Contrast",
]) {
await choose(page, "Theme", label);
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
if (label === "High Contrast") await expect(page.locator("html")).toHaveAttribute("data-contrast", "high");
else await expect(page.locator("html")).not.toHaveAttribute("data-contrast");
const added = palettes.find((p) => p.label === label);
if (added) await expect(page.locator("html")).toHaveAttribute("data-color-theme", added.id);
else await expect(page.locator("html")).not.toHaveAttribute("data-color-theme");
const bg =
added?.bg ??
(label === "Islands Light"
? "rgb(255, 255, 255)"
: label === "Islands Dark"
? "rgb(25, 26, 28)"
: "rgb(0, 0, 0)");
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", bg);
}
await choose(page, "Theme", "One Light");
await choose(page, "Editor color scheme", "Solarized Dark");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", "rgb(0, 43, 54)");
await choose(page, "Editor color scheme", "Theme default");
await dialog.getByLabel("Sync with OS", { exact: true }).check();
await dialog.getByRole("button", { name: "Preferred themes", exact: true }).click();
await choose(page, "Preferred light theme", "Solarized Light");
await choose(page, "Preferred dark theme", "One Dark");
await page.keyboard.press("Escape");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await page.emulateMedia({ colorScheme: "light" });
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "solarized-light");
await page.emulateMedia({ colorScheme: "dark" });
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "one-dark");
});
test("quick theme menu offers all four palettes and applies their kind", async ({ workbench: page }) => {
for (const palette of palettes) {
await page.getByRole("button", { name: "IDE and Project Settings", exact: true }).click();
await page.getByText("Theme", { exact: true }).click();
await page.getByText(palette.label, { exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
await expect(page.locator("html")).toHaveAttribute("data-theme", palette.kind);
}
});
+3 -5
Переглянути файл
@@ -11,8 +11,6 @@ code at all.
plugins/
├─ build.sh # builds Rust plugins into components
├─ hello/ # Rust plugin: two commands
├─ theme-solarized/ # manifest-only: Solarized Light / Dark themes
├─ theme-one/ # manifest-only: One Light / Dark themes
└─ language-toy/ # manifest-only: a language definition
```
@@ -171,10 +169,10 @@ never affected.
```json
{
"id": "solarized-dark",
"name": "Solarized Dark",
"id": "harbor-night",
"name": "Harbor Night",
"kind": "dark",
"tokens": { "--bg-app": "#002b36", "--accent": "#268bd2", "--syn-keyword": "#859900" }
"tokens": { "--bg-app": "#14171c", "--accent": "#2b9e93", "--syn-keyword": "#e0a46e" }
}
```
-60
Переглянути файл
@@ -1,60 +0,0 @@
The One Light and One Dark palettes are adapted from GitHub's Atom One themes
(styles/colors.less, styles/syntax-variables.less, styles/ui-variables.less):
- https://github.com/atom/one-light-syntax (revision d84579027410c576086dfca14d934c4bd74b0438)
- https://github.com/atom/one-dark-syntax (revision 9c96f4454362267ac45322063e193ccf9d2debb1)
- https://github.com/atom/one-light-ui (revision a27bce35088ecd323e44f5c2510bcf2d4e9efcf8)
- https://github.com/atom/one-dark-ui (revision 18c2143d41e17b337fff6dc7024562c4d999be17)
The exact HSL definitions are kept. Atom defines no terminal palette, so the
ANSI slots reuse One's syntax hues. Adapted to OpenIDEA's design tokens by bghtyu.
## one-light-syntax, one-dark-syntax
MIT License
Copyright (c) 2016 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## one-light-ui, one-dark-ui
MIT License
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-17
Переглянути файл
@@ -1,17 +0,0 @@
# Contribution-only theme plugin: no code, just theme and editor scheme files.
id = "theme-one"
name = "One Themes"
version = "1.0.0"
description = "One Light and One Dark: the Atom One palettes by GitHub for the UI, the editor and the terminal."
authors = ["bghtyu <[email protected]>", "GitHub Inc. (Atom One palettes)"]
homepage = "https://github.com/atom/one-dark-syntax"
change-notes = "1.0.0 — One Light and One Dark: UI, editor colour scheme and terminal ANSI colours."
openidea-api = "0.1"
[[contributes.themes]]
path = "themes/one-light.theme.json"
editor-scheme = "themes/one-light.scheme.json"
[[contributes.themes]]
path = "themes/one-dark.theme.json"
editor-scheme = "themes/one-dark.scheme.json"
-103
Переглянути файл
@@ -1,103 +0,0 @@
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
{
"name": "One Dark",
"tokens": {
// Syntax
"--syn-keyword": "hsl(286, 60%, 67%)",
"--syn-string": "hsl(95, 38%, 62%)",
"--syn-type": "hsl(39, 67%, 69%)",
"--syn-function": "hsl(207, 82%, 66%)",
"--syn-function-call": "hsl(207, 82%, 66%)",
"--syn-comment": "hsl(220, 10%, 40%)",
"--syn-comment-style": "italic",
"--syn-doc-comment": "hsl(220, 10%, 40%)",
"--syn-docstring": "hsl(95, 38%, 62%)",
"--syn-annotation": "hsl(29, 54%, 61%)",
"--syn-decorator": "hsl(29, 54%, 61%)",
"--syn-number": "hsl(29, 54%, 61%)",
"--syn-punct": "hsl(220, 14%, 71%)",
"--syn-variable": "hsl(355, 65%, 65%)",
"--syn-param": "hsl(220, 14%, 71%)",
"--syn-property": "hsl(220, 14%, 71%)",
"--syn-constant": "hsl(29, 54%, 61%)",
"--syn-builtin": "hsl(187, 47%, 55%)",
"--syn-self": "hsl(355, 65%, 65%)",
"--syn-keyword-arg": "hsl(355, 65%, 65%)",
"--syn-predefined": "hsl(187, 47%, 55%)",
"--syn-escape": "hsl(187, 47%, 55%)",
"--syn-regexp": "hsl(187, 47%, 55%)",
"--syn-tag": "hsl(355, 65%, 65%)",
"--syn-attribute": "hsl(29, 54%, 61%)",
"--syn-todo": "hsl(286, 60%, 67%)",
"--syn-link": "hsl(207, 82%, 66%)",
"--syn-markup-heading": "hsl(207, 82%, 66%)",
"--syn-markup-code": "hsl(95, 38%, 62%)",
"--syn-invalid": "hsl(355, 65%, 65%)",
"--syn-inline-value": "hsl(220, 9%, 55%)",
"--syn-rainbow-1": "hsl(187, 47%, 55%)",
"--syn-rainbow-2": "hsl(29, 54%, 61%)",
"--syn-rainbow-3": "hsl(95, 38%, 62%)",
"--syn-rainbow-4": "hsl(286, 60%, 67%)",
"--syn-rainbow-5": "hsl(207, 82%, 66%)",
"--syn-bracket-1": "hsl(39, 67%, 69%)",
"--syn-bracket-2": "hsl(286, 60%, 67%)",
"--syn-bracket-3": "hsl(207, 82%, 66%)",
// Editor
"--fg-editor": "hsl(220, 14%, 71%)",
"--caret": "hsl(220, 100%, 66%)",
"--fg-line-number": "hsl(220, 9%, 55%)",
"--fg-line-number-current": "hsl(220, 14%, 71%)",
"--border-gutter": "hsl(220, 13%, 10.8%)",
"--border-indent-guide": "hsl(220, 13%, 10.8%)",
"--border-indent-guide-active": "hsl(220, 100%, 66%)",
"--border-right-margin": "hsl(220, 13%, 10.8%)",
"--bg-editor-selection": "hsl(220, 13%, 28%)",
"--bg-editor-selection-inactive": "hsl(220, 13%, 28%)",
"--bg-text-search-result": "color-mix(in srgb, hsl(39, 67%, 69%) 22%, hsl(220, 13%, 18%))",
"--bg-text-search-result-current": "color-mix(in srgb, hsl(39, 67%, 69%) 36%, hsl(220, 13%, 18%))",
"--bg-matched-brace": "hsl(220, 13%, 28%)",
"--matched-brace-weight": "var(--fw-code-bold)",
"--bg-matched-tag": "hsl(220, 13%, 28%)",
"--bg-fold-placeholder": "hsl(220, 13%, 15%)",
"--fg-fold-placeholder": "hsl(220, 14%, 71%)",
"--fg-fold-marker": "hsl(220, 9%, 55%)",
"--fg-whitespace": "hsl(220, 9%, 55%)",
"--fg-unused": "hsl(220, 9%, 55%)",
"--bg-identifier-under-caret": "hsl(220, 13%, 28%)",
"--bg-identifier-under-caret-write": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
"--fg-inlay": "hsl(220, 9%, 55%)",
"--bg-inlay": "hsl(220, 13%, 15%)",
"--wave-error": "hsl(355, 65%, 65%)",
"--wave-warning": "hsl(39, 67%, 69%)",
"--editor-error-decoration": "underline wavy var(--wave-error)",
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
"--editor-warning-background": "color-mix(in srgb, hsl(39, 67%, 69%) 12%, hsl(220, 13%, 18%))",
"--bg-breakpoint-line": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
"--fg-completion-match": "hsl(207, 82%, 66%)",
"--bg-editor-inline": "hsl(220, 13%, 15%)",
"--bg-editor-banner": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
"--fg-weak-warning": "hsl(220, 9%, 55%)",
"--fg-bulb": "hsl(39, 67%, 69%)",
// ANSI palette
"--ansi-black": "hsl(220, 13%, 15%)",
"--ansi-red": "hsl(355, 65%, 65%)",
"--ansi-green": "hsl(95, 38%, 62%)",
"--ansi-yellow": "hsl(39, 67%, 69%)",
"--ansi-blue": "hsl(207, 82%, 66%)",
"--ansi-magenta": "hsl(286, 60%, 67%)",
"--ansi-cyan": "hsl(187, 47%, 55%)",
"--ansi-white": "hsl(220, 14%, 71%)",
"--ansi-bright-black": "hsl(220, 9%, 55%)",
"--ansi-bright-red": "hsl(355, 65%, 65%)",
"--ansi-bright-green": "hsl(95, 38%, 62%)",
"--ansi-bright-yellow": "hsl(39, 67%, 69%)",
"--ansi-bright-blue": "hsl(207, 82%, 66%)",
"--ansi-bright-magenta": "hsl(286, 60%, 67%)",
"--ansi-bright-cyan": "hsl(187, 47%, 55%)",
"--ansi-bright-white": "#ffffff"
}
}
-142
Переглянути файл
@@ -1,142 +0,0 @@
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// UI theme; the editor colour scheme is in one-dark.scheme.json.
{
"id": "one-dark",
"name": "One Dark",
"kind": "dark",
"tokens": {
// Surfaces
"--bg-app": "hsl(220, 13%, 18%)",
"--bg-elev": "hsl(220, 13%, 15%)",
"--bg-popup": "hsl(220, 13%, 16.5%)",
"--bg-hover": "hsl(220, 13%, 22%)",
"--bg-selected": "hsl(220, 13%, 28%)",
"--bg-selected-strong": "hsl(220, 13%, 28%)",
"--bg-current-line": "color-mix(in srgb, hsl(220, 100%, 80%) 4%, hsl(220, 13%, 18%))",
"--bg-debug-line": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
"--bg-backdrop": "hsl(220, 13%, 15%)",
"--bg-input": "hsl(220, 13%, 18%)",
// Lines
"--border": "hsl(220, 13%, 10.8%)",
"--border-popup": "hsl(220, 13%, 10.8%)",
"--border-strong": "hsl(220, 9%, 55%)",
"--border-window": "hsl(220, 13%, 10.8%)",
"--control-off": "hsl(220, 9%, 55%)",
// Text
"--fg-strong": "hsl(220, 14%, 71%)",
"--fg-emphasis": "hsl(220, 14%, 71%)",
"--fg": "hsl(220, 14%, 71%)",
"--fg-secondary": "hsl(220, 9%, 55%)",
"--fg-muted": "hsl(220, 9%, 55%)",
"--fg-subtle": "hsl(220, 9%, 55%)",
"--fg-faint": "hsl(220, 9%, 55%)",
"--fg-gutter": "hsl(220, 9%, 55%)",
"--fg-icon": "hsl(220, 14%, 71%)",
// Accent
"--accent": "hsl(220, 100%, 66%)",
"--link": "hsl(207, 82%, 66%)",
"--link-hover": "hsl(187, 47%, 55%)",
// Charts
"--chart-series": "hsl(355, 65%, 65%)",
// Status
"--danger": "hsl(355, 65%, 65%)",
"--warning": "hsl(39, 67%, 69%)",
"--success": "hsl(95, 38%, 62%)",
"--db-connected": "hsl(95, 38%, 62%)",
"--db-connected-ring": "hsl(95, 38%, 62%)",
"--info": "hsl(207, 82%, 66%)",
// Vcs
"--vcs-modified": "hsl(207, 82%, 66%)",
"--vcs-added": "hsl(95, 38%, 62%)",
"--vcs-deleted": "hsl(220, 9%, 55%)",
"--vcs-untracked": "hsl(355, 65%, 65%)",
"--vcs-ignored": "hsl(39, 67%, 69%)",
"--vcs-conflict": "hsl(355, 65%, 65%)",
"--gutter-modified": "hsl(207, 82%, 66%)",
"--gutter-added": "hsl(95, 38%, 62%)",
"--gutter-deleted": "hsl(355, 65%, 65%)",
// Shadows and window controls
"--shadow-popup": "0 4px 16px #00000024",
"--shadow-dialog": "0 10px 36px #00000033",
"--traffic-close": "#ff5f57",
"--traffic-close-border": "#e0443e",
"--traffic-close-pressed": "#bf4942",
"--traffic-glyph": "#4d0000",
"--traffic-off": "hsl(220, 13%, 22%)",
"--traffic-off-border": "hsl(220, 13%, 10.8%)",
// Additive
"--bg-tooltip": "hsl(220, 13%, 16.5%)",
"--fg-tooltip": "hsl(220, 14%, 71%)",
"--border-tooltip": "hsl(220, 13%, 10.8%)",
"--bg-control": "hsl(220, 13%, 18%)",
"--bg-control-hover": "hsl(220, 13%, 22%)",
"--bg-active": "hsl(220, 13%, 22%)",
"--bg-scrim": "color-mix(in srgb, hsl(220, 13%, 18%) 68%, transparent)",
"--fg-on-accent": "#000000",
"--shadow-tooltip": "0 4px 16px #00000024",
"--shadow-balloon": "0 10px 36px #00000033",
// Islands contract
"--bg-frame": "hsl(220, 13%, 15%)",
"--bg-island": "hsl(220, 13%, 18%)",
"--bg-stripe-hover": "hsl(220, 13%, 22%)",
"--bg-stripe-selected": "hsl(220, 13%, 28%)",
"--fg-stripe-selected-active": "#000000",
"--fg-stripe-label": "hsl(220, 14%, 71%)",
"--bg-tab-selected": "hsl(220, 13%, 28%)",
"--border-tab-selected": "hsl(220, 100%, 66%)",
"--bg-tab-selected-inactive": "hsl(220, 13%, 15%)",
"--border-tab-selected-inactive": "hsl(220, 13%, 10.8%)",
"--bg-tab-hover": "hsl(220, 13%, 22%)",
"--border-editor-tabs": "hsl(220, 13%, 10.8%)",
"--bg-tree-selected": "hsl(220, 13%, 28%)",
"--bg-tree-selected-inactive": "hsl(220, 13%, 22%)",
"--bg-tree-hover": "hsl(220, 13%, 22%)",
"--bg-toolbar-hover": "hsl(220, 13%, 22%)",
"--bg-toolbar-pressed": "hsl(220, 13%, 28%)",
"--bg-toolbar-selected": "hsl(220, 13%, 28%)",
"--bg-toolbar-selected-hover": "hsl(220, 13%, 28%)",
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, hsl(220, 14%, 71%))",
"--fg-button-primary": "#000000",
"--bg-button": "hsl(220, 13%, 18%)",
"--border-button": "hsl(220, 13%, 10.8%)",
"--bg-field": "hsl(220, 13%, 18%)",
"--bg-checkbox": "hsl(220, 13%, 18%)",
"--border-checkbox": "hsl(220, 9%, 55%)",
"--bg-toggle-on": "hsl(95, 38%, 62%)",
"--fg-toggle-on": "#000000",
"--bg-toggle-off": "hsl(220, 13%, 15%)",
"--fg-toggle-off": "hsl(220, 14%, 71%)",
"--border-toggle": "hsl(220, 9%, 55%)",
"--bg-toggle-knob": "hsl(220, 9%, 55%)",
"--focus-ring-soft": "hsl(220, 100%, 66%)",
"--scrollbar-thumb": "color-mix(in srgb, hsl(220, 14%, 71%) 24%, transparent)",
"--scrollbar-thumb-hover": "color-mix(in srgb, hsl(220, 14%, 71%) 42%, transparent)",
"--bg-search-match": "color-mix(in srgb, hsl(39, 67%, 69%) 22%, hsl(220, 13%, 18%))",
"--bg-search-match-current": "color-mix(in srgb, hsl(39, 67%, 69%) 36%, hsl(220, 13%, 18%))",
"--fg-search-match": "hsl(220, 14%, 71%)",
"--bg-dialog": "hsl(220, 13%, 18%)",
"--bg-popup-inline": "hsl(220, 13%, 15%)",
"--bg-popup-advertiser": "hsl(220, 13%, 15%)",
"--bg-feedback-info": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
"--border-feedback-info": "color-mix(in srgb, hsl(207, 82%, 66%) 55%, hsl(220, 13%, 18%))",
"--bg-feedback-error": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
"--border-feedback-error": "color-mix(in srgb, hsl(355, 65%, 65%) 55%, hsl(220, 13%, 18%))",
"--bg-feedback-warning": "color-mix(in srgb, hsl(39, 67%, 69%) 12%, hsl(220, 13%, 18%))",
"--border-feedback-warning": "color-mix(in srgb, hsl(39, 67%, 69%) 55%, hsl(220, 13%, 18%))",
"--bg-feedback-success": "color-mix(in srgb, hsl(95, 38%, 62%) 12%, hsl(220, 13%, 18%))",
"--border-feedback-success": "color-mix(in srgb, hsl(95, 38%, 62%) 55%, hsl(220, 13%, 18%))",
"--bg-counter": "hsl(220, 100%, 66%)",
"--fg-counter": "#000000",
"--bg-progress-track": "hsl(220, 13%, 22%)"
}
}
-103
Переглянути файл
@@ -1,103 +0,0 @@
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
{
"name": "One Light",
"tokens": {
// Syntax
"--syn-keyword": "hsl(301, 63%, 40%)",
"--syn-string": "hsl(119, 34%, 47%)",
"--syn-type": "hsl(41, 99%, 38%)",
"--syn-function": "hsl(221, 87%, 60%)",
"--syn-function-call": "hsl(221, 87%, 60%)",
"--syn-comment": "hsl(230, 4%, 64%)",
"--syn-comment-style": "italic",
"--syn-doc-comment": "hsl(230, 4%, 64%)",
"--syn-docstring": "hsl(119, 34%, 47%)",
"--syn-annotation": "hsl(41, 99%, 30%)",
"--syn-decorator": "hsl(41, 99%, 30%)",
"--syn-number": "hsl(41, 99%, 30%)",
"--syn-punct": "hsl(230, 8%, 24%)",
"--syn-variable": "hsl(5, 74%, 59%)",
"--syn-param": "hsl(230, 8%, 24%)",
"--syn-property": "hsl(230, 8%, 24%)",
"--syn-constant": "hsl(41, 99%, 30%)",
"--syn-builtin": "hsl(198, 99%, 37%)",
"--syn-self": "hsl(5, 74%, 59%)",
"--syn-keyword-arg": "hsl(5, 74%, 59%)",
"--syn-predefined": "hsl(198, 99%, 37%)",
"--syn-escape": "hsl(198, 99%, 37%)",
"--syn-regexp": "hsl(198, 99%, 37%)",
"--syn-tag": "hsl(5, 74%, 59%)",
"--syn-attribute": "hsl(41, 99%, 30%)",
"--syn-todo": "hsl(301, 63%, 40%)",
"--syn-link": "hsl(221, 87%, 60%)",
"--syn-markup-heading": "hsl(221, 87%, 60%)",
"--syn-markup-code": "hsl(119, 34%, 47%)",
"--syn-invalid": "hsl(5, 74%, 59%)",
"--syn-inline-value": "hsl(230, 6%, 44%)",
"--syn-rainbow-1": "hsl(198, 99%, 37%)",
"--syn-rainbow-2": "hsl(41, 99%, 30%)",
"--syn-rainbow-3": "hsl(119, 34%, 47%)",
"--syn-rainbow-4": "hsl(301, 63%, 40%)",
"--syn-rainbow-5": "hsl(221, 87%, 60%)",
"--syn-bracket-1": "hsl(41, 99%, 38%)",
"--syn-bracket-2": "hsl(301, 63%, 40%)",
"--syn-bracket-3": "hsl(221, 87%, 60%)",
// Editor
"--fg-editor": "hsl(230, 8%, 24%)",
"--caret": "hsl(230, 100%, 66%)",
"--fg-line-number": "hsl(230, 6%, 44%)",
"--fg-line-number-current": "hsl(230, 8%, 24%)",
"--border-gutter": "hsl(230, 1%, 86%)",
"--border-indent-guide": "hsl(230, 1%, 86%)",
"--border-indent-guide-active": "hsl(230, 100%, 66%)",
"--border-right-margin": "hsl(230, 1%, 86%)",
"--bg-editor-selection": "hsl(230, 1%, 90%)",
"--bg-editor-selection-inactive": "hsl(230, 1%, 90%)",
"--bg-text-search-result": "color-mix(in srgb, hsl(41, 99%, 38%) 22%, hsl(230, 1%, 98%))",
"--bg-text-search-result-current": "color-mix(in srgb, hsl(41, 99%, 38%) 36%, hsl(230, 1%, 98%))",
"--bg-matched-brace": "hsl(230, 1%, 90%)",
"--matched-brace-weight": "var(--fw-code-bold)",
"--bg-matched-tag": "hsl(230, 1%, 90%)",
"--bg-fold-placeholder": "hsl(230, 1%, 92%)",
"--fg-fold-placeholder": "hsl(230, 8%, 24%)",
"--fg-fold-marker": "hsl(230, 6%, 44%)",
"--fg-whitespace": "hsl(230, 6%, 44%)",
"--fg-unused": "hsl(230, 6%, 44%)",
"--bg-identifier-under-caret": "hsl(230, 1%, 90%)",
"--bg-identifier-under-caret-write": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
"--fg-inlay": "hsl(230, 6%, 44%)",
"--bg-inlay": "hsl(230, 1%, 92%)",
"--wave-error": "hsl(5, 74%, 59%)",
"--wave-warning": "hsl(41, 99%, 38%)",
"--editor-error-decoration": "underline wavy var(--wave-error)",
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
"--editor-warning-background": "color-mix(in srgb, hsl(41, 99%, 38%) 12%, hsl(230, 1%, 98%))",
"--bg-breakpoint-line": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
"--fg-completion-match": "hsl(221, 87%, 60%)",
"--bg-editor-inline": "hsl(230, 1%, 92%)",
"--bg-editor-banner": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
"--fg-weak-warning": "hsl(230, 6%, 44%)",
"--fg-bulb": "hsl(41, 99%, 38%)",
// ANSI palette
"--ansi-black": "hsl(230, 8%, 24%)",
"--ansi-red": "hsl(5, 74%, 59%)",
"--ansi-green": "hsl(119, 34%, 47%)",
"--ansi-yellow": "hsl(41, 99%, 38%)",
"--ansi-blue": "hsl(221, 87%, 60%)",
"--ansi-magenta": "hsl(301, 63%, 40%)",
"--ansi-cyan": "hsl(198, 99%, 37%)",
"--ansi-white": "hsl(230, 6%, 44%)",
"--ansi-bright-black": "hsl(230, 6%, 44%)",
"--ansi-bright-red": "hsl(5, 74%, 59%)",
"--ansi-bright-green": "hsl(119, 34%, 47%)",
"--ansi-bright-yellow": "hsl(41, 99%, 38%)",
"--ansi-bright-blue": "hsl(221, 87%, 60%)",
"--ansi-bright-magenta": "hsl(301, 63%, 40%)",
"--ansi-bright-cyan": "hsl(198, 99%, 37%)",
"--ansi-bright-white": "hsl(230, 8%, 24%)"
}
}
-142
Переглянути файл
@@ -1,142 +0,0 @@
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// UI theme; the editor colour scheme is in one-light.scheme.json.
{
"id": "one-light",
"name": "One Light",
"kind": "light",
"tokens": {
// Surfaces
"--bg-app": "hsl(230, 1%, 98%)",
"--bg-elev": "hsl(230, 1%, 92%)",
"--bg-popup": "hsl(230, 1%, 95%)",
"--bg-hover": "hsl(230, 1%, 90%)",
"--bg-selected": "hsl(230, 1%, 90%)",
"--bg-selected-strong": "hsl(230, 1%, 90%)",
"--bg-current-line": "color-mix(in srgb, hsl(230, 8%, 24%) 5%, hsl(230, 1%, 98%))",
"--bg-debug-line": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
"--bg-backdrop": "hsl(230, 1%, 92%)",
"--bg-input": "hsl(230, 1%, 98%)",
// Lines
"--border": "hsl(230, 1%, 86%)",
"--border-popup": "hsl(230, 1%, 86%)",
"--border-strong": "hsl(230, 6%, 44%)",
"--border-window": "hsl(230, 1%, 86%)",
"--control-off": "hsl(230, 6%, 44%)",
// Text
"--fg-strong": "hsl(230, 8%, 24%)",
"--fg-emphasis": "hsl(230, 8%, 24%)",
"--fg": "hsl(230, 8%, 24%)",
"--fg-secondary": "hsl(230, 6%, 44%)",
"--fg-muted": "hsl(230, 6%, 44%)",
"--fg-subtle": "hsl(230, 6%, 44%)",
"--fg-faint": "hsl(230, 6%, 44%)",
"--fg-gutter": "hsl(230, 6%, 44%)",
"--fg-icon": "hsl(230, 8%, 24%)",
// Accent
"--accent": "hsl(230, 100%, 66%)",
"--link": "hsl(221, 87%, 60%)",
"--link-hover": "hsl(198, 99%, 37%)",
// Charts
"--chart-series": "hsl(5, 74%, 59%)",
// Status
"--danger": "hsl(5, 74%, 59%)",
"--warning": "hsl(41, 99%, 38%)",
"--success": "hsl(119, 34%, 47%)",
"--db-connected": "hsl(119, 34%, 47%)",
"--db-connected-ring": "hsl(119, 34%, 47%)",
"--info": "hsl(221, 87%, 60%)",
// Vcs
"--vcs-modified": "hsl(221, 87%, 60%)",
"--vcs-added": "hsl(119, 34%, 47%)",
"--vcs-deleted": "hsl(230, 6%, 44%)",
"--vcs-untracked": "hsl(5, 74%, 59%)",
"--vcs-ignored": "hsl(41, 99%, 38%)",
"--vcs-conflict": "hsl(5, 74%, 59%)",
"--gutter-modified": "hsl(221, 87%, 60%)",
"--gutter-added": "hsl(119, 34%, 47%)",
"--gutter-deleted": "hsl(5, 74%, 59%)",
// Shadows and window controls
"--shadow-popup": "0 4px 16px #00000024",
"--shadow-dialog": "0 10px 36px #00000033",
"--traffic-close": "#ff5f57",
"--traffic-close-border": "#e0443e",
"--traffic-close-pressed": "#bf4942",
"--traffic-glyph": "#4d0000",
"--traffic-off": "hsl(230, 1%, 90%)",
"--traffic-off-border": "hsl(230, 1%, 86%)",
// Additive
"--bg-tooltip": "hsl(230, 1%, 95%)",
"--fg-tooltip": "hsl(230, 8%, 24%)",
"--border-tooltip": "hsl(230, 1%, 86%)",
"--bg-control": "hsl(230, 1%, 98%)",
"--bg-control-hover": "hsl(230, 1%, 90%)",
"--bg-active": "hsl(230, 1%, 90%)",
"--bg-scrim": "color-mix(in srgb, hsl(230, 1%, 98%) 68%, transparent)",
"--fg-on-accent": "#ffffff",
"--shadow-tooltip": "0 4px 16px #00000024",
"--shadow-balloon": "0 10px 36px #00000033",
// Islands contract
"--bg-frame": "hsl(230, 1%, 92%)",
"--bg-island": "hsl(230, 1%, 98%)",
"--bg-stripe-hover": "hsl(230, 1%, 90%)",
"--bg-stripe-selected": "hsl(230, 1%, 90%)",
"--fg-stripe-selected-active": "#ffffff",
"--fg-stripe-label": "hsl(230, 8%, 24%)",
"--bg-tab-selected": "hsl(230, 1%, 90%)",
"--border-tab-selected": "hsl(230, 100%, 66%)",
"--bg-tab-selected-inactive": "hsl(230, 1%, 92%)",
"--border-tab-selected-inactive": "hsl(230, 1%, 86%)",
"--bg-tab-hover": "hsl(230, 1%, 90%)",
"--border-editor-tabs": "hsl(230, 1%, 86%)",
"--bg-tree-selected": "hsl(230, 1%, 90%)",
"--bg-tree-selected-inactive": "hsl(230, 1%, 90%)",
"--bg-tree-hover": "hsl(230, 1%, 90%)",
"--bg-toolbar-hover": "hsl(230, 1%, 90%)",
"--bg-toolbar-pressed": "hsl(230, 1%, 90%)",
"--bg-toolbar-selected": "hsl(230, 1%, 90%)",
"--bg-toolbar-selected-hover": "hsl(230, 1%, 90%)",
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, hsl(230, 8%, 24%))",
"--fg-button-primary": "#ffffff",
"--bg-button": "hsl(230, 1%, 98%)",
"--border-button": "hsl(230, 1%, 86%)",
"--bg-field": "hsl(230, 1%, 98%)",
"--bg-checkbox": "hsl(230, 1%, 98%)",
"--border-checkbox": "hsl(230, 6%, 44%)",
"--bg-toggle-on": "hsl(119, 34%, 47%)",
"--fg-toggle-on": "#ffffff",
"--bg-toggle-off": "hsl(230, 1%, 92%)",
"--fg-toggle-off": "hsl(230, 8%, 24%)",
"--border-toggle": "hsl(230, 6%, 44%)",
"--bg-toggle-knob": "hsl(230, 6%, 44%)",
"--focus-ring-soft": "hsl(230, 100%, 66%)",
"--scrollbar-thumb": "color-mix(in srgb, hsl(230, 8%, 24%) 24%, transparent)",
"--scrollbar-thumb-hover": "color-mix(in srgb, hsl(230, 8%, 24%) 42%, transparent)",
"--bg-search-match": "color-mix(in srgb, hsl(41, 99%, 38%) 22%, hsl(230, 1%, 98%))",
"--bg-search-match-current": "color-mix(in srgb, hsl(41, 99%, 38%) 36%, hsl(230, 1%, 98%))",
"--fg-search-match": "hsl(230, 8%, 24%)",
"--bg-dialog": "hsl(230, 1%, 98%)",
"--bg-popup-inline": "hsl(230, 1%, 92%)",
"--bg-popup-advertiser": "hsl(230, 1%, 92%)",
"--bg-feedback-info": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
"--border-feedback-info": "color-mix(in srgb, hsl(221, 87%, 60%) 55%, hsl(230, 1%, 98%))",
"--bg-feedback-error": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
"--border-feedback-error": "color-mix(in srgb, hsl(5, 74%, 59%) 55%, hsl(230, 1%, 98%))",
"--bg-feedback-warning": "color-mix(in srgb, hsl(41, 99%, 38%) 12%, hsl(230, 1%, 98%))",
"--border-feedback-warning": "color-mix(in srgb, hsl(41, 99%, 38%) 55%, hsl(230, 1%, 98%))",
"--bg-feedback-success": "color-mix(in srgb, hsl(119, 34%, 47%) 12%, hsl(230, 1%, 98%))",
"--border-feedback-success": "color-mix(in srgb, hsl(119, 34%, 47%) 55%, hsl(230, 1%, 98%))",
"--bg-counter": "hsl(230, 100%, 66%)",
"--fg-counter": "#ffffff",
"--bg-progress-track": "hsl(230, 1%, 90%)"
}
}
-27
Переглянути файл
@@ -1,27 +0,0 @@
The Solarized palette is adapted from Ethan Schoonover's Solarized,
https://github.com/altercation/solarized (revision 62f656a02f93c5190a8753159e34b385588d5ff3):
the sixteen sRGB colours of its README, the semantic roles of
vim-colors-solarized/colors/solarized.vim and its iTerm2 ANSI slots.
Adapted to OpenIDEA's design tokens by bghtyu.
MIT License
Copyright (c) 2011 Ethan Schoonover
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-17
Переглянути файл
@@ -1,17 +0,0 @@
# Contribution-only theme plugin: no code, just theme and editor scheme files.
id = "theme-solarized"
name = "Solarized Themes"
version = "1.0.0"
description = "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette for the UI, the editor and the terminal."
authors = ["bghtyu <[email protected]>", "Ethan Schoonover (Solarized palette)"]
homepage = "https://github.com/altercation/solarized"
change-notes = "1.0.0 — Solarized Light and Solarized Dark: UI, editor colour scheme and terminal ANSI colours."
openidea-api = "0.1"
[[contributes.themes]]
path = "themes/solarized-light.theme.json"
editor-scheme = "themes/solarized-light.scheme.json"
[[contributes.themes]]
path = "themes/solarized-dark.theme.json"
editor-scheme = "themes/solarized-dark.scheme.json"
-103
Переглянути файл
@@ -1,103 +0,0 @@
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
{
"name": "Solarized Dark",
"tokens": {
// Syntax
"--syn-keyword": "#859900",
"--syn-string": "#2aa198",
"--syn-type": "#b58900",
"--syn-function": "#268bd2",
"--syn-function-call": "#268bd2",
"--syn-comment": "#586e75",
"--syn-comment-style": "italic",
"--syn-doc-comment": "#586e75",
"--syn-docstring": "#2aa198",
"--syn-annotation": "#cb4b16",
"--syn-decorator": "#cb4b16",
"--syn-number": "#2aa198",
"--syn-punct": "#839496",
"--syn-variable": "#268bd2",
"--syn-param": "#839496",
"--syn-property": "#268bd2",
"--syn-constant": "#2aa198",
"--syn-builtin": "#2aa198",
"--syn-self": "#dc322f",
"--syn-keyword-arg": "#dc322f",
"--syn-predefined": "#2aa198",
"--syn-escape": "#dc322f",
"--syn-regexp": "#2aa198",
"--syn-tag": "#268bd2",
"--syn-attribute": "#cb4b16",
"--syn-todo": "#d33682",
"--syn-link": "#268bd2",
"--syn-markup-heading": "#268bd2",
"--syn-markup-code": "#2aa198",
"--syn-invalid": "#dc322f",
"--syn-inline-value": "#839496",
"--syn-rainbow-1": "#2aa198",
"--syn-rainbow-2": "#cb4b16",
"--syn-rainbow-3": "#859900",
"--syn-rainbow-4": "#d33682",
"--syn-rainbow-5": "#268bd2",
"--syn-bracket-1": "#b58900",
"--syn-bracket-2": "#d33682",
"--syn-bracket-3": "#268bd2",
// Editor
"--fg-editor": "#839496",
"--caret": "#268bd2",
"--fg-line-number": "#839496",
"--fg-line-number-current": "#839496",
"--border-gutter": "#073642",
"--border-indent-guide": "#073642",
"--border-indent-guide-active": "#268bd2",
"--border-right-margin": "#073642",
"--bg-editor-selection": "#073642",
"--bg-editor-selection-inactive": "#073642",
"--bg-text-search-result": "color-mix(in srgb, #b58900 22%, #002b36)",
"--bg-text-search-result-current": "color-mix(in srgb, #b58900 36%, #002b36)",
"--bg-matched-brace": "#073642",
"--matched-brace-weight": "var(--fw-code-bold)",
"--bg-matched-tag": "#073642",
"--bg-fold-placeholder": "#073642",
"--fg-fold-placeholder": "#839496",
"--fg-fold-marker": "#839496",
"--fg-whitespace": "#839496",
"--fg-unused": "#839496",
"--bg-identifier-under-caret": "#073642",
"--bg-identifier-under-caret-write": "color-mix(in srgb, #dc322f 12%, #002b36)",
"--fg-inlay": "#839496",
"--bg-inlay": "#073642",
"--wave-error": "#dc322f",
"--wave-warning": "#b58900",
"--editor-error-decoration": "underline wavy var(--wave-error)",
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
"--editor-warning-background": "color-mix(in srgb, #b58900 12%, #002b36)",
"--bg-breakpoint-line": "color-mix(in srgb, #dc322f 12%, #002b36)",
"--fg-completion-match": "#268bd2",
"--bg-editor-inline": "#073642",
"--bg-editor-banner": "color-mix(in srgb, #268bd2 12%, #002b36)",
"--fg-weak-warning": "#839496",
"--fg-bulb": "#b58900",
// ANSI palette
"--ansi-black": "#073642",
"--ansi-red": "#dc322f",
"--ansi-green": "#859900",
"--ansi-yellow": "#b58900",
"--ansi-blue": "#268bd2",
"--ansi-magenta": "#d33682",
"--ansi-cyan": "#2aa198",
"--ansi-white": "#eee8d5",
"--ansi-bright-black": "#002b36",
"--ansi-bright-red": "#cb4b16",
"--ansi-bright-green": "#586e75",
"--ansi-bright-yellow": "#657b83",
"--ansi-bright-blue": "#839496",
"--ansi-bright-magenta": "#6c71c4",
"--ansi-bright-cyan": "#93a1a1",
"--ansi-bright-white": "#fdf6e3"
}
}
-142
Переглянути файл
@@ -1,142 +0,0 @@
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// UI theme; the editor colour scheme is in solarized-dark.scheme.json.
{
"id": "solarized-dark",
"name": "Solarized Dark",
"kind": "dark",
"tokens": {
// Surfaces
"--bg-app": "#002b36",
"--bg-elev": "#073642",
"--bg-popup": "#073642",
"--bg-hover": "#073642",
"--bg-selected": "#073642",
"--bg-selected-strong": "#073642",
"--bg-current-line": "#073642",
"--bg-debug-line": "color-mix(in srgb, #268bd2 12%, #002b36)",
"--bg-backdrop": "#073642",
"--bg-input": "#002b36",
// Lines
"--border": "#073642",
"--border-popup": "#073642",
"--border-strong": "#839496",
"--border-window": "#073642",
"--control-off": "#839496",
// Text
"--fg-strong": "#839496",
"--fg-emphasis": "#839496",
"--fg": "#839496",
"--fg-secondary": "#839496",
"--fg-muted": "#839496",
"--fg-subtle": "#839496",
"--fg-faint": "#839496",
"--fg-gutter": "#839496",
"--fg-icon": "#839496",
// Accent
"--accent": "#268bd2",
"--link": "#268bd2",
"--link-hover": "#2aa198",
// Charts
"--chart-series": "#dc322f",
// Status
"--danger": "#dc322f",
"--warning": "#b58900",
"--success": "#859900",
"--db-connected": "#859900",
"--db-connected-ring": "#859900",
"--info": "#268bd2",
// Vcs
"--vcs-modified": "#268bd2",
"--vcs-added": "#859900",
"--vcs-deleted": "#839496",
"--vcs-untracked": "#dc322f",
"--vcs-ignored": "#b58900",
"--vcs-conflict": "#dc322f",
"--gutter-modified": "#268bd2",
"--gutter-added": "#859900",
"--gutter-deleted": "#dc322f",
// Shadows and window controls
"--shadow-popup": "0 4px 16px #00000024",
"--shadow-dialog": "0 10px 36px #00000033",
"--traffic-close": "#ff5f57",
"--traffic-close-border": "#e0443e",
"--traffic-close-pressed": "#bf4942",
"--traffic-glyph": "#4d0000",
"--traffic-off": "#073642",
"--traffic-off-border": "#073642",
// Additive
"--bg-tooltip": "#073642",
"--fg-tooltip": "#839496",
"--border-tooltip": "#073642",
"--bg-control": "#002b36",
"--bg-control-hover": "#073642",
"--bg-active": "#073642",
"--bg-scrim": "color-mix(in srgb, #002b36 68%, transparent)",
"--fg-on-accent": "#002b36",
"--shadow-tooltip": "0 4px 16px #00000024",
"--shadow-balloon": "0 10px 36px #00000033",
// Islands contract
"--bg-frame": "#073642",
"--bg-island": "#002b36",
"--bg-stripe-hover": "#073642",
"--bg-stripe-selected": "#073642",
"--fg-stripe-selected-active": "#002b36",
"--fg-stripe-label": "#839496",
"--bg-tab-selected": "#073642",
"--border-tab-selected": "#268bd2",
"--bg-tab-selected-inactive": "#073642",
"--border-tab-selected-inactive": "#073642",
"--bg-tab-hover": "#073642",
"--border-editor-tabs": "#073642",
"--bg-tree-selected": "#073642",
"--bg-tree-selected-inactive": "#073642",
"--bg-tree-hover": "#073642",
"--bg-toolbar-hover": "#073642",
"--bg-toolbar-pressed": "#073642",
"--bg-toolbar-selected": "#073642",
"--bg-toolbar-selected-hover": "#073642",
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, #839496)",
"--fg-button-primary": "#002b36",
"--bg-button": "#002b36",
"--border-button": "#073642",
"--bg-field": "#002b36",
"--bg-checkbox": "#002b36",
"--border-checkbox": "#839496",
"--bg-toggle-on": "#859900",
"--fg-toggle-on": "#002b36",
"--bg-toggle-off": "#073642",
"--fg-toggle-off": "#839496",
"--border-toggle": "#839496",
"--bg-toggle-knob": "#839496",
"--focus-ring-soft": "#268bd2",
"--scrollbar-thumb": "color-mix(in srgb, #839496 24%, transparent)",
"--scrollbar-thumb-hover": "color-mix(in srgb, #839496 42%, transparent)",
"--bg-search-match": "color-mix(in srgb, #b58900 22%, #002b36)",
"--bg-search-match-current": "color-mix(in srgb, #b58900 36%, #002b36)",
"--fg-search-match": "#839496",
"--bg-dialog": "#002b36",
"--bg-popup-inline": "#073642",
"--bg-popup-advertiser": "#073642",
"--bg-feedback-info": "color-mix(in srgb, #268bd2 12%, #002b36)",
"--border-feedback-info": "color-mix(in srgb, #268bd2 55%, #002b36)",
"--bg-feedback-error": "color-mix(in srgb, #dc322f 12%, #002b36)",
"--border-feedback-error": "color-mix(in srgb, #dc322f 55%, #002b36)",
"--bg-feedback-warning": "color-mix(in srgb, #b58900 12%, #002b36)",
"--border-feedback-warning": "color-mix(in srgb, #b58900 55%, #002b36)",
"--bg-feedback-success": "color-mix(in srgb, #859900 12%, #002b36)",
"--border-feedback-success": "color-mix(in srgb, #859900 55%, #002b36)",
"--bg-counter": "#268bd2",
"--fg-counter": "#002b36",
"--bg-progress-track": "#073642"
}
}
-103
Переглянути файл
@@ -1,103 +0,0 @@
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
{
"name": "Solarized Light",
"tokens": {
// Syntax
"--syn-keyword": "#859900",
"--syn-string": "#2aa198",
"--syn-type": "#b58900",
"--syn-function": "#268bd2",
"--syn-function-call": "#268bd2",
"--syn-comment": "#93a1a1",
"--syn-comment-style": "italic",
"--syn-doc-comment": "#93a1a1",
"--syn-docstring": "#2aa198",
"--syn-annotation": "#cb4b16",
"--syn-decorator": "#cb4b16",
"--syn-number": "#2aa198",
"--syn-punct": "#657b83",
"--syn-variable": "#268bd2",
"--syn-param": "#657b83",
"--syn-property": "#268bd2",
"--syn-constant": "#2aa198",
"--syn-builtin": "#2aa198",
"--syn-self": "#dc322f",
"--syn-keyword-arg": "#dc322f",
"--syn-predefined": "#2aa198",
"--syn-escape": "#dc322f",
"--syn-regexp": "#2aa198",
"--syn-tag": "#268bd2",
"--syn-attribute": "#cb4b16",
"--syn-todo": "#d33682",
"--syn-link": "#268bd2",
"--syn-markup-heading": "#268bd2",
"--syn-markup-code": "#2aa198",
"--syn-invalid": "#dc322f",
"--syn-inline-value": "#657b83",
"--syn-rainbow-1": "#2aa198",
"--syn-rainbow-2": "#cb4b16",
"--syn-rainbow-3": "#859900",
"--syn-rainbow-4": "#d33682",
"--syn-rainbow-5": "#268bd2",
"--syn-bracket-1": "#b58900",
"--syn-bracket-2": "#d33682",
"--syn-bracket-3": "#268bd2",
// Editor
"--fg-editor": "#657b83",
"--caret": "#268bd2",
"--fg-line-number": "#657b83",
"--fg-line-number-current": "#657b83",
"--border-gutter": "#eee8d5",
"--border-indent-guide": "#eee8d5",
"--border-indent-guide-active": "#268bd2",
"--border-right-margin": "#eee8d5",
"--bg-editor-selection": "#eee8d5",
"--bg-editor-selection-inactive": "#eee8d5",
"--bg-text-search-result": "color-mix(in srgb, #b58900 22%, #fdf6e3)",
"--bg-text-search-result-current": "color-mix(in srgb, #b58900 36%, #fdf6e3)",
"--bg-matched-brace": "#eee8d5",
"--matched-brace-weight": "var(--fw-code-bold)",
"--bg-matched-tag": "#eee8d5",
"--bg-fold-placeholder": "#eee8d5",
"--fg-fold-placeholder": "#657b83",
"--fg-fold-marker": "#657b83",
"--fg-whitespace": "#657b83",
"--fg-unused": "#657b83",
"--bg-identifier-under-caret": "#eee8d5",
"--bg-identifier-under-caret-write": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
"--fg-inlay": "#657b83",
"--bg-inlay": "#eee8d5",
"--wave-error": "#dc322f",
"--wave-warning": "#b58900",
"--editor-error-decoration": "underline wavy var(--wave-error)",
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
"--editor-warning-background": "color-mix(in srgb, #b58900 12%, #fdf6e3)",
"--bg-breakpoint-line": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
"--fg-completion-match": "#268bd2",
"--bg-editor-inline": "#eee8d5",
"--bg-editor-banner": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
"--fg-weak-warning": "#657b83",
"--fg-bulb": "#b58900",
// ANSI palette
"--ansi-black": "#073642",
"--ansi-red": "#dc322f",
"--ansi-green": "#859900",
"--ansi-yellow": "#b58900",
"--ansi-blue": "#268bd2",
"--ansi-magenta": "#d33682",
"--ansi-cyan": "#2aa198",
"--ansi-white": "#eee8d5",
"--ansi-bright-black": "#002b36",
"--ansi-bright-red": "#cb4b16",
"--ansi-bright-green": "#586e75",
"--ansi-bright-yellow": "#657b83",
"--ansi-bright-blue": "#839496",
"--ansi-bright-magenta": "#6c71c4",
"--ansi-bright-cyan": "#93a1a1",
"--ansi-bright-white": "#fdf6e3"
}
}
-142
Переглянути файл
@@ -1,142 +0,0 @@
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
// UI theme; the editor colour scheme is in solarized-light.scheme.json.
{
"id": "solarized-light",
"name": "Solarized Light",
"kind": "light",
"tokens": {
// Surfaces
"--bg-app": "#fdf6e3",
"--bg-elev": "#eee8d5",
"--bg-popup": "#fdf6e3",
"--bg-hover": "#eee8d5",
"--bg-selected": "#eee8d5",
"--bg-selected-strong": "#eee8d5",
"--bg-current-line": "#eee8d5",
"--bg-debug-line": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
"--bg-backdrop": "#eee8d5",
"--bg-input": "#fdf6e3",
// Lines
"--border": "#eee8d5",
"--border-popup": "#eee8d5",
"--border-strong": "#657b83",
"--border-window": "#eee8d5",
"--control-off": "#657b83",
// Text
"--fg-strong": "#657b83",
"--fg-emphasis": "#657b83",
"--fg": "#657b83",
"--fg-secondary": "#657b83",
"--fg-muted": "#657b83",
"--fg-subtle": "#657b83",
"--fg-faint": "#657b83",
"--fg-gutter": "#657b83",
"--fg-icon": "#657b83",
// Accent
"--accent": "#268bd2",
"--link": "#268bd2",
"--link-hover": "#2aa198",
// Charts
"--chart-series": "#dc322f",
// Status
"--danger": "#dc322f",
"--warning": "#b58900",
"--success": "#859900",
"--db-connected": "#859900",
"--db-connected-ring": "#859900",
"--info": "#268bd2",
// Vcs
"--vcs-modified": "#268bd2",
"--vcs-added": "#859900",
"--vcs-deleted": "#657b83",
"--vcs-untracked": "#dc322f",
"--vcs-ignored": "#b58900",
"--vcs-conflict": "#dc322f",
"--gutter-modified": "#268bd2",
"--gutter-added": "#859900",
"--gutter-deleted": "#dc322f",
// Shadows and window controls
"--shadow-popup": "0 4px 16px #00000024",
"--shadow-dialog": "0 10px 36px #00000033",
"--traffic-close": "#ff5f57",
"--traffic-close-border": "#e0443e",
"--traffic-close-pressed": "#bf4942",
"--traffic-glyph": "#4d0000",
"--traffic-off": "#eee8d5",
"--traffic-off-border": "#eee8d5",
// Additive
"--bg-tooltip": "#fdf6e3",
"--fg-tooltip": "#657b83",
"--border-tooltip": "#eee8d5",
"--bg-control": "#fdf6e3",
"--bg-control-hover": "#eee8d5",
"--bg-active": "#eee8d5",
"--bg-scrim": "color-mix(in srgb, #fdf6e3 68%, transparent)",
"--fg-on-accent": "#002b36",
"--shadow-tooltip": "0 4px 16px #00000024",
"--shadow-balloon": "0 10px 36px #00000033",
// Islands contract
"--bg-frame": "#eee8d5",
"--bg-island": "#fdf6e3",
"--bg-stripe-hover": "#eee8d5",
"--bg-stripe-selected": "#eee8d5",
"--fg-stripe-selected-active": "#002b36",
"--fg-stripe-label": "#657b83",
"--bg-tab-selected": "#eee8d5",
"--border-tab-selected": "#268bd2",
"--bg-tab-selected-inactive": "#eee8d5",
"--border-tab-selected-inactive": "#eee8d5",
"--bg-tab-hover": "#eee8d5",
"--border-editor-tabs": "#eee8d5",
"--bg-tree-selected": "#eee8d5",
"--bg-tree-selected-inactive": "#eee8d5",
"--bg-tree-hover": "#eee8d5",
"--bg-toolbar-hover": "#eee8d5",
"--bg-toolbar-pressed": "#eee8d5",
"--bg-toolbar-selected": "#eee8d5",
"--bg-toolbar-selected-hover": "#eee8d5",
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, #657b83)",
"--fg-button-primary": "#002b36",
"--bg-button": "#fdf6e3",
"--border-button": "#eee8d5",
"--bg-field": "#fdf6e3",
"--bg-checkbox": "#fdf6e3",
"--border-checkbox": "#657b83",
"--bg-toggle-on": "#859900",
"--fg-toggle-on": "#002b36",
"--bg-toggle-off": "#eee8d5",
"--fg-toggle-off": "#657b83",
"--border-toggle": "#657b83",
"--bg-toggle-knob": "#657b83",
"--focus-ring-soft": "#268bd2",
"--scrollbar-thumb": "color-mix(in srgb, #657b83 24%, transparent)",
"--scrollbar-thumb-hover": "color-mix(in srgb, #657b83 42%, transparent)",
"--bg-search-match": "color-mix(in srgb, #b58900 22%, #fdf6e3)",
"--bg-search-match-current": "color-mix(in srgb, #b58900 36%, #fdf6e3)",
"--fg-search-match": "#657b83",
"--bg-dialog": "#fdf6e3",
"--bg-popup-inline": "#eee8d5",
"--bg-popup-advertiser": "#eee8d5",
"--bg-feedback-info": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
"--border-feedback-info": "color-mix(in srgb, #268bd2 55%, #fdf6e3)",
"--bg-feedback-error": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
"--border-feedback-error": "color-mix(in srgb, #dc322f 55%, #fdf6e3)",
"--bg-feedback-warning": "color-mix(in srgb, #b58900 12%, #fdf6e3)",
"--border-feedback-warning": "color-mix(in srgb, #b58900 55%, #fdf6e3)",
"--bg-feedback-success": "color-mix(in srgb, #859900 12%, #fdf6e3)",
"--border-feedback-success": "color-mix(in srgb, #859900 55%, #fdf6e3)",
"--bg-counter": "#268bd2",
"--fg-counter": "#002b36",
"--bg-progress-track": "#eee8d5"
}
}
+4
Переглянути файл
@@ -13,6 +13,9 @@ try {
const last = JSON.parse(localStorage.getItem("openidea.theme") ?? "null");
if (last?.theme) root.dataset.theme = last.theme;
if (last?.contrast) root.dataset.contrast = last.contrast;
// A built-in palette theme (Solarized, One) over its light / dark base (tokens.css).
if (typeof last?.colorTheme === "string" && /^[a-z][a-z0-9-]*$/.test(last.colorTheme))
root.dataset.colorTheme = last.colorTheme;
// A plugin theme's stylesheet (plugins/bridge.ts), so the workbench doesn't
// show the built-in theme until the plugin host answers.
const plugin = JSON.parse(localStorage.getItem("openidea.pluginTheme") ?? "null");
@@ -21,6 +24,7 @@ try {
root.dataset.pluginTheme = plugin.id;
root.dataset.pluginThemeKind = root.dataset.theme = plugin.kind === "light" ? "light" : "dark";
delete root.dataset.contrast;
delete root.dataset.colorTheme;
}
// The splash's frame, text and accent colours, resolved by theme.ts (splash.css).
const colors = [
+1 -68
Переглянути файл
@@ -1,4 +1,4 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
convertIcls,
@@ -77,73 +77,6 @@ describe("theme plugin template", () => {
});
});
/** Theme packages in plugins/: every theme they contribute, with its editor scheme. */
function pluginThemes() {
return readdirSync("plugins", { withFileTypes: true })
.filter((d) => d.isDirectory() && existsSync(`plugins/${d.name}/plugin.toml`))
.flatMap((d) => {
const dir = `plugins/${d.name}`;
const manifest = read(`${dir}/plugin.toml`);
return manifest
.split("[[contributes.themes]]")
.slice(1)
.map((entry) => ({
dir,
manifest,
path: /^path\s*=\s*"([^"]+)"/m.exec(entry)?.[1],
scheme: /^editor-scheme\s*=\s*"([^"]+)"/m.exec(entry)?.[1],
}));
});
}
describe("theme plugins in plugins/", () => {
const themes = pluginThemes();
it("include Solarized and One, light and dark", () => {
expect(themes.map((t) => `${t.dir}/${t.path}`)).toEqual(
expect.arrayContaining([
"plugins/theme-solarized/themes/solarized-light.theme.json",
"plugins/theme-solarized/themes/solarized-dark.theme.json",
"plugins/theme-one/themes/one-light.theme.json",
"plugins/theme-one/themes/one-dark.theme.json",
]),
);
});
it.each(themes.map((t) => [`${t.dir}/${t.path}`, t] as const))(
"%s sets every UI token and its scheme every editor token",
(_name, t) => {
// A failure here means tokens.css gained (or lost) a themeable token: add it to the theme too.
expect(t.path && t.scheme, "theme and editor-scheme paths").toBeTruthy();
expect(t.manifest).not.toMatch(/^wasm\s*=/m);
const ui = JSON.parse(stripJsonComments(read(`${t.dir}/${t.path}`))) as {
id: string;
name: string;
kind: string;
tokens: Record<string, string>;
};
const ed = JSON.parse(stripJsonComments(read(`${t.dir}/${t.scheme}`))) as { tokens: Record<string, string> };
expect(ui.id).toMatch(/^[a-z0-9][a-z0-9._-]*$/);
expect(ui.name.trim()).not.toBe("");
expect(["light", "dark"]).toContain(ui.kind);
expect(sorted(Object.keys(ui.tokens))).toEqual(sorted(sets.ui));
expect(sorted(Object.keys(ed.tokens))).toEqual(sorted(sets.scheme));
for (const [k, v] of Object.entries({ ...ui.tokens, ...ed.tokens })) {
expect(/[;{}<>\\\n]|url\(|image\(/i.test(v), `${k}: ${v}`).toBe(false);
// var() may only point at tokens the IDE defines.
for (const [, ref] of v.matchAll(/var\((--[a-z0-9-]+)/g)) expect(css, `${k} → ${ref}`).toContain(`${ref}:`);
}
},
);
it("ship the licence of the palettes they port", () => {
for (const dir of new Set(themes.map((t) => t.dir))) {
if (dir === "plugins/theme-solarized" || dir === "plugins/theme-one")
expect(read(`${dir}/LICENSE`), dir).toContain("MIT License");
}
});
});
describe("IntelliJ theme import", () => {
const themeJson = `{
// comment
+1
Переглянути файл
@@ -9,6 +9,7 @@ mod logging;
mod menu;
mod shell_env;
mod state;
mod theme_frame;
mod windows;
use state::AppState;
+159
Переглянути файл
@@ -0,0 +1,159 @@
//! Built-in themes' window frame colour (`--bg-frame`), read from the UI's own
//! `src/styles/tokens.css` so a native window opens in the colour its page paints.
use std::collections::HashMap;
use std::sync::LazyLock;
use tauri::utils::config::Color;
const TOKENS_CSS: &str = include_str!("../../src/styles/tokens.css");
/// A top-level rule: its selector list and custom-property declarations.
type Block = (Vec<String>, HashMap<String, String>);
/// The rule blocks of tokens.css, in source order.
static BLOCKS: LazyLock<Vec<Block>> = LazyLock::new(|| blocks(TOKENS_CSS));
/// `--bg-frame` of built-in theme `id` (`light`, `dark`, `dark-hc` or a palette
/// theme with a `:root[data-color-theme="<id>"]` block); `None` for any other id.
pub fn builtin_frame(id: &str) -> Option<Color> {
builtin_frame_in(&BLOCKS, id)
}
fn builtin_frame_in(blocks: &[Block], id: &str) -> Option<Color> {
let palette = format!(":root[data-color-theme=\"{id}\"]");
let mut selectors = vec![":root".to_owned()];
match id {
"dark" => {}
"light" => selectors.push(":root[data-theme=\"light\"]".into()),
"dark-hc" => selectors.push(":root[data-theme=\"dark-hc\"]".into()),
_ if blocks.iter().any(|(sel, _)| sel.contains(&palette)) => {
if id.ends_with("light") {
selectors.push(":root[data-theme=\"light\"]".into());
}
selectors.push(":root[data-color-theme]".into());
selectors.push(palette);
}
_ => return None,
}
// Later matching blocks win, as in the page (the selectors involved share a specificity order).
let mut vars: HashMap<&str, &str> = HashMap::new();
for (sel, decls) in blocks {
if sel.iter().any(|s| selectors.contains(s)) {
vars.extend(decls.iter().map(|(k, v)| (k.as_str(), v.as_str())));
}
}
let mut value = *vars.get("--bg-frame")?;
for _ in 0..8 {
match value.strip_prefix("var(").and_then(|v| v.strip_suffix(')')) {
Some(name) => value = vars.get(name.split(',').next()?.trim())?,
None => return parse_color(value),
}
}
None
}
fn blocks(css: &str) -> Vec<Block> {
let mut text = String::with_capacity(css.len());
let mut rest = css;
while let Some(start) = rest.find("/*") {
text.push_str(&rest[..start]);
rest = rest[start + 2..].find("*/").map_or("", |end| &rest[start + 2 + end + 2..]);
}
text.push_str(rest);
let mut out = Vec::new();
let mut rest = text.as_str();
while let Some(open) = rest.find('{') {
let selector = rest[..open].trim();
// Matching close brace (nested at-rule blocks are skipped whole).
let mut depth = 0;
let Some(close) = rest[open..].char_indices().find_map(|(i, c)| {
match c {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
(depth == 0).then_some(open + i)
}) else {
break;
};
if !selector.starts_with('@') {
let decls = rest[open + 1..close]
.split(';')
.filter_map(|d| d.split_once(':'))
.filter(|(k, _)| k.trim().starts_with("--"))
.map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
.collect();
out.push((selector.split(',').map(|s| s.trim().to_owned()).collect(), decls));
}
rest = &rest[close + 1..];
}
out
}
/// `#hex` or `hsl(h, s%, l%)`, rounded to 8-bit channels as browsers do.
fn parse_color(value: &str) -> Option<Color> {
if value.starts_with('#') {
return serde_json::from_value(serde_json::Value::String(value.to_owned())).ok();
}
let args = value.strip_prefix("hsl(")?.strip_suffix(')')?;
let nums: Vec<f64> = args
.split([',', ' '])
.filter(|s| !s.is_empty())
.map(|s| s.trim_end_matches("deg").trim_end_matches('%').parse())
.collect::<Result<_, _>>()
.ok()?;
let [h, s, l] = nums[..] else { return None };
let (s, l) = (s / 100.0, l / 100.0);
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let hp = (h.rem_euclid(360.0)) / 60.0;
let x = c * (1.0 - (hp % 2.0 - 1.0).abs());
let (r, g, b) = match hp as u8 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = l - c / 2.0;
let byte = |v: f64| ((v + m) * 255.0).round().clamp(0.0, 255.0) as u8;
Some(Color(byte(r), byte(g), byte(b), 0xff))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_every_builtin_frame_from_tokens_css() {
for (id, color) in [
("light", Color(0xe9, 0xea, 0xee, 0xff)),
("dark", Color(0x26, 0x28, 0x2c, 0xff)),
("dark-hc", Color(0x1a, 0x1b, 0x1e, 0xff)),
("solarized-light", Color(0xee, 0xe8, 0xd5, 0xff)),
("solarized-dark", Color(0x07, 0x36, 0x42, 0xff)),
("one-light", Color(0xea, 0xea, 0xeb, 0xff)),
("one-dark", Color(0x21, 0x25, 0x2b, 0xff)),
] {
assert_eq!(builtin_frame(id), Some(color), "{id}");
}
assert_eq!(builtin_frame("system"), None);
assert_eq!(builtin_frame("nope"), None);
}
#[test]
fn resolves_var_chains_and_hsl() {
let css = r#"
/* a { comment } */
:root { --bg-frame: #000; --p: hsl(0, 100%, 50%); }
@media (x) { :root { --bg-frame: #fff; } }
:root[data-color-theme] { --bg-frame: var(--q); }
:root[data-color-theme="t-dark"] { --q: var(--p, #123); }
"#;
let b = blocks(css);
assert_eq!(builtin_frame_in(&b, "dark"), Some(Color(0, 0, 0, 0xff)));
assert_eq!(builtin_frame_in(&b, "t-dark"), Some(Color(0xff, 0, 0, 0xff)));
assert_eq!(builtin_frame_in(&b, "u-dark"), None);
}
}
+23 -10
Переглянути файл
@@ -11,6 +11,7 @@
//! [`Launch::settings_page`] tells it which page to show.
use crate::state::AppState;
use crate::theme_frame::builtin_frame;
use ide_core::settings::AppearanceSettings;
use ide_core::{IdeError, IdeResult};
use parking_lot::Mutex;
@@ -212,22 +213,15 @@ fn window_config(app: &AppHandle) -> IdeResult<WindowConfig> {
/// The theme's `--bg-frame` (tokens.css), mirroring `resolveThemeId` in theme.ts.
pub fn frame_color(appearance: &AppearanceSettings, os_dark: bool) -> Color {
const LIGHT: Color = Color(0xe9, 0xea, 0xee, 0xff);
const DARK: Color = Color(0x26, 0x28, 0x2c, 0xff);
const DARK_HC: Color = Color(0x1a, 0x1b, 0x1e, 0xff);
let sync = appearance.sync_with_os || appearance.theme == "system";
let theme = match (sync, os_dark) {
(false, _) => &appearance.theme,
(true, true) => &appearance.preferred_dark_theme,
(true, false) => &appearance.preferred_light_theme,
};
match theme.as_str() {
"dark-hc" => DARK_HC,
"dark" => DARK,
"light" => LIGHT,
_ if sync && os_dark => DARK,
_ => LIGHT,
}
builtin_frame(theme)
.or_else(|| builtin_frame(if sync && os_dark { "dark" } else { "light" }))
.unwrap_or(Color(0xe9, 0xea, 0xee, 0xff))
}
/// A `#rgb` / `#rrggbb` / `#rrggbbaa` colour sent by the UI (Tauri's own parser).
@@ -475,6 +469,25 @@ mod tests {
assert_eq!(frame_color(&a, true), Color(0x26, 0x28, 0x2c, 0xff));
}
#[test]
fn additional_palettes_keep_native_startup_and_os_preferences_in_sync() {
for (id, color) in [
("solarized-light", Color(0xee, 0xe8, 0xd5, 0xff)),
("solarized-dark", Color(0x07, 0x36, 0x42, 0xff)),
("one-light", Color(0xea, 0xea, 0xeb, 0xff)),
("one-dark", Color(0x21, 0x25, 0x2b, 0xff)),
] {
let mut a = AppearanceSettings { theme: id.into(), ..Default::default() };
assert_eq!(frame_color(&a, false), color);
assert_eq!(frame_color(&a, true), color);
a.sync_with_os = true;
a.preferred_light_theme = id.into();
a.preferred_dark_theme = id.into();
assert_eq!(frame_color(&a, false), color);
assert_eq!(frame_color(&a, true), color);
}
}
#[test]
fn frame_colours_from_the_ui_parse_as_hex() {
for (hex, expected) in [
+1 -1
Переглянути файл
@@ -28,7 +28,7 @@ export function installColorScheme(): () => void {
const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(apply) : null;
observer?.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme"],
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme"],
});
return () => {
unsubscribe();
+5 -5
Переглянути файл
@@ -144,8 +144,8 @@ describe("store", () => {
it("selects and clears a plugin theme", async () => {
await refreshPlugins();
await selectPluginTheme("theme-solarized", "solarized-dark");
expect(usePlugins.getState().activeTheme?.theme.id).toBe("solarized-dark");
await selectPluginTheme("theme-harbor-night", "harbor-night");
expect(usePlugins.getState().activeTheme?.theme.id).toBe("harbor-night");
await selectPluginTheme(null, null);
expect(usePlugins.getState().activeTheme).toBeNull();
});
@@ -238,8 +238,8 @@ describe("PluginsPage", () => {
await waitFor(() => expect(screen.getByRole("checkbox", { name: "Enable Hello" })).toBeInTheDocument());
expect(screen.getByRole("button", { name: "Run Hello: Insert Date" })).toBeDisabled();
await user.click(within(list).getByText("Solarized Themes"));
await user.click(screen.getByRole("button", { name: "Apply theme Solarized Dark" }));
await user.click(within(list).getByText("Harbor Night Theme"));
await user.click(screen.getByRole("button", { name: "Apply theme Harbor Night" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Use Default Theme" })).toBeInTheDocument());
await user.type(screen.getByLabelText("Filter plugins"), "toy");
@@ -309,7 +309,7 @@ describe("catalog helpers", () => {
expect(catalogState(entry, [plugin({ id: "hello", version: "0.0.9" })])).toBe("update");
expect(catalogState(entry, [plugin({ id: "hello", version: "0.1.0" })])).toBe("installed");
expect(catalogState(entry, [plugin({ id: "hello", source: "dev" })])).toBe("dev");
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized"]);
expect(filterCatalog(sampleCatalog(), "harbor").map((c) => c.id)).toEqual(["theme-harbor-night"]);
});
it("disables and re-enables all downloaded plugins, leaving dev plugins alone", async () => {
+18 -32
Переглянути файл
@@ -76,35 +76,21 @@ function ensureSubscribed() {
// ---------------------------------------------------------------------------
// Browser simulation: the bundled sample plugins, with `hello` implemented in JS.
const SOLARIZED: ActiveTheme["theme"][] = [
// templates/theme-plugin, abridged.
const SAMPLE_THEMES: ActiveTheme["theme"][] = [
{
id: "solarized-light",
name: "Solarized Light",
kind: "light",
tokens: {
"--bg-app": "#fdf6e3",
"--bg-island": "#fdf6e3",
"--bg-frame": "#eee8d5",
"--bg-elev": "#eee8d5",
"--fg": "#657b83",
"--accent": "#268bd2",
"--syn-keyword": "#859900",
"--syn-string": "#2aa198",
},
},
{
id: "solarized-dark",
name: "Solarized Dark",
id: "harbor-night",
name: "Harbor Night",
kind: "dark",
tokens: {
"--bg-app": "#002b36",
"--bg-island": "#002b36",
"--bg-frame": "#073642",
"--bg-elev": "#073642",
"--fg": "#839496",
"--accent": "#268bd2",
"--syn-keyword": "#859900",
"--syn-string": "#2aa198",
"--bg-app": "#14171c",
"--bg-island": "#14171c",
"--bg-frame": "#1f252e",
"--bg-elev": "#1b2027",
"--fg": "#c9d1dc",
"--accent": "#2b9e93",
"--syn-keyword": "#e0a46e",
"--syn-string": "#9ccc7a",
},
},
];
@@ -140,14 +126,14 @@ function samplePlugins(): PluginInfo[] {
},
{
...base,
id: "theme-solarized",
name: "Solarized Themes",
id: "theme-harbor-night",
name: "Harbor Night Theme",
version: "1.0.0",
description: "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette.",
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized",
description: "Example dark theme: slate surfaces, a teal accent and a warm syntax palette.",
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-harbor-night",
status: "static",
hasCode: false,
themes: SOLARIZED.map(({ id, name, kind }) => ({ id, name, kind })),
themes: SAMPLE_THEMES.map(({ id, name, kind }) => ({ id, name, kind })),
},
{
...base,
@@ -201,7 +187,7 @@ export function createMemoryPluginsBackend(
const activeTheme = (): ActiveTheme | null => {
if (!theme) return null;
const p = plugins.find((x) => x.id === theme?.pluginId);
const t = SOLARIZED.find((x) => x.id === theme?.themeId);
const t = SAMPLE_THEMES.find((x) => x.id === theme?.themeId);
if (!p?.enabled || !t) return null;
return { pluginId: p.id, theme: t };
};
+47
Переглянути файл
@@ -2,6 +2,7 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { contrastRatio, isColorValue, readTokens } from "../theme/contrast";
import { BUILTIN_THEMES } from "@/platform/themes";
const css = readFileSync(`${process.cwd()}/src/styles/tokens.css`, "utf8");
const dark = readTokens(css, ":root");
@@ -116,3 +117,49 @@ describe("theme tokens", () => {
expect(contrastRatio("#000", "#fff")).toBeCloseTo(21, 5);
});
});
/**
* Tokens the palette themes (Solarized, One) take from their light / dark base
* on purpose: colours the user assigns (project and data source colours), the
* platform's caption close button, and the tint strength.
*/
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|tint-strength$)/;
describe("palette themes", () => {
const ISLANDS = ["light", "dark", "dark-hc"];
const palettes = BUILTIN_THEMES.filter((t) => !ISLANDS.includes(t.id));
const shared = readTokens(css, ":root[data-color-theme]");
// Every themeable token (the Islands Light block lists them all) and every plain colour of :root.
const required = [
...new Set([
...Object.keys(readTokens(css, ':root[data-theme="light"]')),
...Object.entries(dark)
.filter(([, v]) => isColorValue(v) && !v.includes("var("))
.map(([k]) => k),
]),
];
it("have a tokens.css block for every palette theme id, and no other", () => {
const blocks = [...css.matchAll(/:root\[data-color-theme="([a-z0-9-]+)"\]/g)].map((m) => m[1]);
expect([...new Set(blocks)].sort()).toEqual(palettes.map((t) => t.id).sort());
expect(palettes.length).toBeGreaterThanOrEqual(4);
});
it.each(palettes)("$id defines every token or inherits it from its $kind base on purpose", ({ id, kind }) => {
// A failure here means tokens.css gained a token: map it in the :root[data-color-theme]
// block (or the theme's own), or add it to INHERITED_FROM_BASE if the base's value is right.
const own = { ...shared, ...readTokens(css, `:root[data-color-theme="${id}"]`) };
expect(required.length).toBeGreaterThan(200);
expect(required.filter((k) => !(k in own) && !INHERITED_FROM_BASE.test(k))).toEqual([]);
const base = kind === "light" ? light : dark;
expect(required.filter((k) => INHERITED_FROM_BASE.test(k) && !(k in base))).toEqual([]);
// Every palette role the shared mapping uses is set by the theme.
const refs = Object.values(own)
.join(" ")
.matchAll(/var\((--palette-[a-z0-9-]+)/g);
const roles = new Set([...refs].map((m) => m[1]!));
expect([...roles].filter((r) => !(r in own))).toEqual([]);
for (const t of ["--chart-series", "--bg-frame", "--fg-muted", "--accent", "--ansi-red", "--syn-keyword"])
expect(own[t], `${id} ${t}`).toBeTruthy();
});
});
+4 -2
Переглянути файл
@@ -4,11 +4,13 @@
* `ide-core::settings` defaults; validation never throws.
*/
import { BUILTIN_THEME_IDS, type BuiltinThemeId } from "@/platform/themes";
export type Antialiasing = "subpixel" | "greyscale" | "none";
export type OpenProjectIn = "ask" | "new" | "same";
export type NotificationDisplay = "balloon" | "sticky" | "none";
/** Themes selectable as "preferred" when syncing with the OS. */
export type ConcreteTheme = "light" | "dark" | "dark-hc";
export type ConcreteTheme = BuiltinThemeId;
export interface AppearanceExtras {
syncWithOs: boolean;
@@ -102,7 +104,7 @@ const int = (v: unknown, d: number, { min, max }: { min: number; max: number })
typeof v === "number" && Number.isFinite(v) ? Math.round(Math.min(max, Math.max(min, v))) : d;
const AA: readonly Antialiasing[] = ["subpixel", "greyscale", "none"];
const CONCRETE: readonly ConcreteTheme[] = ["light", "dark", "dark-hc"];
const CONCRETE = BUILTIN_THEME_IDS;
const DISPLAYS: readonly NotificationDisplay[] = ["balloon", "sticky", "none"];
export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
+3 -2
Переглянути файл
@@ -33,8 +33,9 @@ import {
type GeneralExtras,
type NotificationsValid,
} from "./behaviorSchema";
import { BUILTIN_THEME_IDS, type BuiltinThemeId } from "@/platform/themes";
export type ThemeId = "dark" | "light" | "system" | "dark-hc";
export type ThemeId = BuiltinThemeId | "system";
export type LocaleId = "en" | "zh";
/** A user keymap (Keymap › Duplicate…): its own overrides on top of a bundled preset. */
@@ -155,7 +156,7 @@ export const LIMITS = {
localHistoryMaxMb: { min: 16, max: 16384 },
} as const;
const THEMES: readonly ThemeId[] = ["dark", "light", "system", "dark-hc"];
const THEMES: readonly ThemeId[] = [...BUILTIN_THEME_IDS, "system"];
const LOCALES: readonly LocaleId[] = ["en", "zh"];
export const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
+4 -6
Переглянути файл
@@ -29,13 +29,11 @@ import type { PageSpec } from "./spec";
import styles from "./Behavior.module.css";
import { useT } from "@/platform/i18n";
import { BUILTIN_THEMES } from "@/platform/themes";
export const APPEARANCE_GROUP = "Appearance & Behavior";
const THEMES: SelectOption<ConcreteTheme>[] = [
{ value: "light", label: "Islands Light" },
{ value: "dark", label: "Islands Dark" },
{ value: "dark-hc", label: "High Contrast" },
];
const THEMES: SelectOption<ConcreteTheme>[] = BUILTIN_THEMES.map((t) => ({ value: t.id, label: t.label }));
/** Checkbox bound to a key, optionally disabled. */
function BoundToggle({ k, label, hint, disabled }: { k: string; label: string; hint?: ReactNode; disabled?: boolean }) {
@@ -500,7 +498,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
{
kind: "custom",
label: "Theme",
keywords: ["Sync with OS", "Islands Light", "Islands Dark", "High Contrast", "preferred theme"],
keywords: ["Sync with OS", ...BUILTIN_THEMES.map((t) => t.label), "preferred theme"],
component: ThemeRow,
},
{
+2 -2
Переглянути файл
@@ -195,10 +195,10 @@ export function activate(): () => void {
terminalRuntime()?.applyThemeToAll();
terminalRuntime()?.applyThemeToEmbedded();
});
// A plugin theme switch keeps data-theme when both themes share a light / dark base.
// A palette or plugin theme switch keeps data-theme when both themes share a light / dark base.
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme", "style"],
});
disposers.push(() => observer.disconnect());
}
+24 -2
Переглянути файл
@@ -108,16 +108,38 @@ describe("mermaid in the markdown preview", () => {
expect(ids.size).toBe(2);
});
it("re-renders when switching between built-in palette themes of the same light / dark base", async () => {
const root = document.documentElement;
const previous = root.dataset.colorTheme;
const changed = vi.fn();
const off = subscribeTheme(changed);
try {
root.dataset.colorTheme = "one-dark";
const firstKey = themeKey();
await renderMermaidIn(preview(SEQ)).done;
root.dataset.colorTheme = "solarized-dark";
await Promise.resolve();
expect(changed).toHaveBeenCalled();
expect(themeKey()).not.toBe(firstKey);
await renderMermaidIn(preview(SEQ)).done;
expect(m.render).toHaveBeenCalledTimes(2);
} finally {
off();
if (previous === undefined) delete root.dataset.colorTheme;
else root.dataset.colorTheme = previous;
}
});
it("re-renders when switching between plugin themes of the same light / dark base", async () => {
const root = document.documentElement;
const changed = vi.fn();
const off = subscribeTheme(changed);
try {
root.dataset.theme = "dark";
root.dataset.pluginTheme = "theme-one/one-dark";
root.dataset.pluginTheme = "acme-themes/night";
const firstKey = themeKey();
await renderMermaidIn(preview(SEQ)).done;
root.dataset.pluginTheme = "theme-solarized/solarized-dark";
root.dataset.pluginTheme = "acme-themes/dusk";
await Promise.resolve();
expect(changed).toHaveBeenCalled();
expect(themeKey()).not.toBe(firstKey);
+3 -3
Переглянути файл
@@ -30,8 +30,8 @@ function loadMermaid(): Promise<Mermaid> {
/** Theme identity: anything that changes the colours read below. */
export function themeKey(): string {
const root = document.documentElement;
const { theme = "", contrast = "", pluginTheme = "" } = root.dataset;
return `${theme}|${contrast}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
const { theme = "", contrast = "", colorTheme = "", pluginTheme = "" } = root.dataset;
return `${theme}|${contrast}|${colorTheme}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
}
/** Calls `cb` when the theme attributes (a plugin theme's included) or the accent on the root change. */
@@ -40,7 +40,7 @@ export function subscribeTheme(cb: () => void): () => void {
const observer = new MutationObserver(cb);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme", "style"],
});
return () => observer.disconnect();
}
+4 -7
Переглянути файл
@@ -13,14 +13,11 @@ import { notifications } from "@/workbench/contributions";
import { ACCENT_PRESETS, DEFAULT_ACCENT } from "@/workbench/theme";
import { reopenEnabled, setReopenPreference, useSession } from "./session";
import styles from "./Welcome.module.css";
import { BUILTIN_THEMES, isBuiltinTheme, type BuiltinThemeId } from "@/platform/themes";
type Theme = "light" | "dark" | "dark-hc";
type Theme = BuiltinThemeId;
export const THEME_OPTIONS: SelectOption<Theme>[] = [
{ value: "light", label: "Islands Light" },
{ value: "dark", label: "Islands Dark" },
{ value: "dark-hc", label: "High Contrast" },
];
export const THEME_OPTIONS: SelectOption<Theme>[] = BUILTIN_THEMES.map((t) => ({ value: t.id, label: t.label }));
const DEFAULT_UI_FONT_SIZE = 13;
const DEFAULT_UI_FONT = "Inter";
@@ -52,7 +49,7 @@ interface AppearanceView {
/** The values Customize shows, from (possibly partial / legacy) settings. */
export function customizeView(settings: ReturnType<typeof useProject.getState>["settings"]): AppearanceView {
const a = (settings?.appearance ?? {}) as Record<string, unknown>;
const theme = a.theme === "dark" || a.theme === "dark-hc" ? a.theme : "light";
const theme = isBuiltinTheme(a.theme) ? a.theme : "light";
const custom = a.useCustomFont === true;
const size = typeof a.uiFontSize === "number" ? a.uiFontSize : DEFAULT_UI_FONT_SIZE;
return {
+7 -8
Переглянути файл
@@ -9,14 +9,11 @@
*/
import tokensCss from "@/styles/tokens.css?raw";
import type { TextAttributesOverride } from "./editorSettings";
import { BUILTIN_THEMES, isBuiltinTheme, themeKind, type BuiltinThemeId } from "./themes";
export type SchemeId = "light" | "dark" | "dark-hc";
export type SchemeId = BuiltinThemeId;
export const SCHEMES: ReadonlyArray<{ id: SchemeId; name: string }> = [
{ id: "light", name: "Islands Light" },
{ id: "dark", name: "Islands Dark" },
{ id: "dark-hc", name: "High Contrast" },
];
export const SCHEMES: ReadonlyArray<{ id: SchemeId; name: string }> = BUILTIN_THEMES.map((t) => ({ id: t.id, name: t.label }));
export interface AttributeDef {
id: string;
@@ -167,7 +164,8 @@ function blocks(css: string): Array<{ selectors: string[]; decls: Vars }> {
function themeSelectors(id: SchemeId): string[] {
if (id === "light") return [':root[data-theme="light"]'];
if (id === "dark-hc") return [':root[data-theme="dark-hc"]', ':root[data-theme="dark"][data-contrast="high"]'];
return [':root[data-theme="dark"]'];
if (id === "dark") return [':root[data-theme="dark"]'];
return [`:root[data-theme="${themeKind(id)}"]`, ":root[data-color-theme]", `:root[data-color-theme="${id}"]`];
}
let source: string = tokensCss;
@@ -205,13 +203,14 @@ export function schemeTokens(id: SchemeId, css: string = source): Vars {
/** The scheme the editor shows for `setting` under the current theme. */
export function effectiveScheme(setting: string | undefined, root: HTMLElement | null = typeof document !== "undefined" ? document.documentElement : null): SchemeId {
if (setting === "light" || setting === "dark" || setting === "dark-hc") return setting;
if (isBuiltinTheme(setting)) return setting;
return themeScheme(root);
}
/** Scheme belonging to the active IDE theme (`data-theme` / `data-contrast`). */
export function themeScheme(root: HTMLElement | null = typeof document !== "undefined" ? document.documentElement : null): SchemeId {
if (!root) return "light";
if (isBuiltinTheme(root.dataset.colorTheme)) return root.dataset.colorTheme;
if (root.dataset.theme === "light") return "light";
return root.dataset.contrast === "high" || root.dataset.theme === "dark-hc" ? "dark-hc" : "dark";
}
+5 -1
Переглянути файл
@@ -3681,5 +3681,9 @@
"omitted for cycle": "因循环依赖而省略",
"version managed from {version}": "版本由 {version} 经依赖管理调整",
"Cannot find the declaration": "找不到声明",
"Cannot exclude {name}": "无法排除 {name}"
"Cannot exclude {name}": "无法排除 {name}",
"Solarized Light": "Solarized 浅色",
"Solarized Dark": "Solarized 深色",
"One Light": "One 浅色",
"One Dark": "One 深色"
}
+76
Переглянути файл
@@ -0,0 +1,76 @@
import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import { BUILTIN_THEMES, isBuiltinTheme, themeKind } from "./themes";
import { _setSchemeSource, effectiveScheme, schemeCss, schemeTokens, themeScheme } from "./colorScheme";
import { applyThemeAttributes, resolveTheme, resolveThemeId } from "@/workbench/theme";
import { validateSettings } from "@/features/settings/model/schema";
import { applyPluginTheme } from "@/features/plugins/bridge";
const css = readFileSync("src/styles/tokens.css", "utf8");
_setSchemeSource(css);
afterEach(() => {
applyPluginTheme(null);
delete document.documentElement.dataset.colorTheme;
delete document.documentElement.dataset.contrast;
});
describe("built-in palettes", () => {
it.each(BUILTIN_THEMES)("preserves $id through validation and system resolution", ({ id, kind }) => {
const a = { theme: id, preferredLightTheme: id, preferredDarkTheme: id };
expect(validateSettings({ appearance: a }).appearance).toMatchObject(a);
expect(resolveThemeId(a, false)).toBe(id);
expect(resolveTheme(id, false)).toBe(kind);
expect(resolveThemeId({ ...a, syncWithOs: true }, true)).toBe(id);
expect(resolveThemeId({ ...a, theme: "system" }, false)).toBe(id);
applyThemeAttributes(id);
expect(document.documentElement.dataset.theme).toBe(kind);
expect(themeScheme()).toBe(id);
expect(effectiveScheme(id)).toBe(id);
expect(schemeCss("theme-default", undefined)).toBe("");
expect(schemeTokens(id).get("--bg-editor")).not.toContain("var(");
});
it("rejects unknown ids and retains legacy fallbacks", () => {
expect(isBuiltinTheme("not-a-theme")).toBe(false);
expect(resolveThemeId({ theme: "bad" }, true)).toBe("light");
expect(resolveThemeId({ theme: "system", preferredDarkTheme: "bad" }, false)).toBe("dark");
expect(themeKind("solarized-light")).toBe("light");
});
// Token coverage of every palette theme: settings/__tests__/contrast.test.ts.
it("resolves editor scheme tokens from the palettes", () => {
expect(schemeTokens("solarized-light").get("--bg-editor")).toBe("#fdf6e3");
expect(schemeTokens("solarized-dark").get("--bg-editor")).toBe("#002b36");
expect(schemeTokens("solarized-dark").get("--syn-string")).toBe("#2aa198");
expect(schemeTokens("solarized-light").get("--ansi-bright-blue")).toBe("#839496");
expect(schemeTokens("one-light").get("--syn-keyword")).toBe("hsl(301, 63%, 40%)");
expect(schemeTokens("one-dark").get("--syn-string")).toBe("hsl(95, 38%, 62%)");
});
it("keeps explicit editor schemes and per-scheme edits independent of UI palettes", () => {
applyThemeAttributes("solarized-light");
expect(schemeCss("one-dark", undefined)).toContain("--bg-editor: hsl(220, 13%, 18%) !important");
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toContain(
"--syn-keyword: #123456 !important",
);
applyThemeAttributes("one-light");
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toBe("");
});
it("clears the palette for plugins and restores it when the plugin is removed", () => {
const root = document.documentElement;
applyThemeAttributes("one-dark", root);
root.dataset.pluginThemeKind = "light";
applyThemeAttributes("one-dark", root);
expect(root.dataset.colorTheme).toBeUndefined();
expect(root.dataset.theme).toBe("light");
delete root.dataset.pluginThemeKind;
applyThemeAttributes("one-dark", root);
expect(root.dataset.colorTheme).toBe("one-dark");
applyThemeAttributes("dark-hc", root);
expect(root.dataset.colorTheme).toBeUndefined();
expect(root.dataset.contrast).toBe("high");
applyThemeAttributes("solarized-dark", root);
expect(root.dataset.contrast).toBeUndefined();
});
});
+19
Переглянути файл
@@ -0,0 +1,19 @@
/** Stable built-in ids shared by settings, quick switch and editor schemes. */
export const BUILTIN_THEMES = [
{ id: "light", label: "Islands Light", kind: "light" },
{ id: "dark", label: "Islands Dark", kind: "dark" },
{ id: "dark-hc", label: "High Contrast", kind: "dark" },
{ id: "solarized-light", label: "Solarized Light", kind: "light" },
{ id: "solarized-dark", label: "Solarized Dark", kind: "dark" },
{ id: "one-light", label: "One Light", kind: "light" },
{ id: "one-dark", label: "One Dark", kind: "dark" },
] as const;
export type BuiltinThemeId = (typeof BUILTIN_THEMES)[number]["id"];
export const BUILTIN_THEME_IDS: readonly BuiltinThemeId[] = BUILTIN_THEMES.map((t) => t.id);
export function isBuiltinTheme(value: unknown): value is BuiltinThemeId {
return typeof value === "string" && (BUILTIN_THEME_IDS as readonly string[]).includes(value);
}
export function themeKind(id: BuiltinThemeId): "light" | "dark" {
return BUILTIN_THEMES.find((t) => t.id === id)!.kind;
}
+396
Переглянути файл
@@ -824,6 +824,402 @@ html {
--bg-progress-track: #55585f;
}
/* Solarized and Atom One adaptations. Exact upstream palettes and MIT notices:
* docs/themes.md and THIRD_PARTY_LICENSES.md. The shared mapping covers every
* colour token; only palette roles below differ. UI-only states use explicit
* mixes of original colours where the source has no equivalent component.
* Keep before appearance options so accessibility preferences still win.
*/
:root[data-color-theme] {
--bg-app: var(--palette-bg);
--bg-island: var(--palette-bg);
--bg-input: var(--palette-bg);
--bg-field: var(--palette-bg);
--bg-checkbox: var(--palette-bg);
--bg-control: var(--palette-bg);
--bg-button: var(--palette-bg);
--bg-dialog: var(--palette-bg);
--bg-frame: var(--palette-panel);
--bg-elev: var(--palette-panel);
--bg-backdrop: var(--palette-panel);
--bg-popup-inline: var(--palette-panel);
--bg-popup-advertiser: var(--palette-panel);
--bg-tab-selected-inactive: var(--palette-panel);
--bg-toggle-off: var(--palette-panel);
--bg-editor-inline: var(--palette-panel);
--bg-inlay: var(--palette-panel);
--bg-fold-placeholder: var(--palette-panel);
--bg-popup: var(--palette-popup);
--bg-tooltip: var(--palette-popup);
--bg-hover: var(--palette-hover);
--bg-control-hover: var(--palette-hover);
--bg-active: var(--palette-hover);
--bg-tree-selected-inactive: var(--palette-hover);
--bg-progress-track: var(--palette-hover);
--bg-tab-hover: var(--palette-hover);
--bg-tree-hover: var(--palette-hover);
--bg-toolbar-hover: var(--palette-hover);
--bg-stripe-hover: var(--palette-hover);
--bg-selected: var(--palette-selection);
--bg-selected-strong: var(--palette-selection);
--bg-tree-selected: var(--palette-selection);
--bg-tab-selected: var(--palette-selection);
--bg-toolbar-selected: var(--palette-selection);
--bg-toolbar-selected-hover: var(--palette-selection);
--bg-toolbar-pressed: var(--palette-selection);
--bg-stripe-selected: var(--palette-selection);
--bg-editor-selection: var(--palette-selection);
--bg-editor-selection-inactive: var(--palette-selection);
--bg-matched-brace: var(--palette-selection);
--bg-matched-tag: var(--palette-selection);
--bg-identifier-under-caret: var(--palette-selection);
--bg-current-line: var(--palette-line);
--border: var(--palette-border);
--border-gutter: var(--palette-border);
--border-indent-guide: var(--palette-border);
--border-right-margin: var(--palette-border);
--border-editor-tabs: var(--palette-border);
--border-tab-selected-inactive: var(--palette-border);
--border-button: var(--palette-border);
--border-window: var(--palette-border);
--border-popup: var(--palette-border);
--border-tooltip: var(--palette-border);
--border-strong: var(--palette-muted);
--control-off: var(--palette-muted);
--border-checkbox: var(--palette-muted);
--border-toggle: var(--palette-muted);
--bg-toggle-knob: var(--palette-muted);
--fg: var(--palette-fg);
--fg-emphasis: var(--palette-fg);
--fg-strong: var(--palette-fg);
--fg-icon: var(--palette-fg);
--fg-tooltip: var(--palette-fg);
--fg-editor: var(--palette-fg);
--fg-fold-placeholder: var(--palette-fg);
--fg-line-number-current: var(--palette-fg);
--fg-stripe-label: var(--palette-fg);
--fg-toggle-off: var(--palette-fg);
--fg-secondary: var(--palette-muted);
--fg-muted: var(--palette-muted);
--fg-subtle: var(--palette-muted);
--fg-faint: var(--palette-muted);
--fg-gutter: var(--palette-muted);
--fg-line-number: var(--palette-muted);
--fg-fold-marker: var(--palette-muted);
--fg-whitespace: var(--palette-muted);
--fg-unused: var(--palette-muted);
--fg-inlay: var(--palette-muted);
--fg-weak-warning: var(--palette-muted);
--syn-inline-value: var(--palette-muted);
--accent: var(--palette-accent);
--caret: var(--palette-accent);
--fg-on-accent: var(--palette-on-accent);
--fg-button-primary: var(--palette-on-accent);
--fg-stripe-selected-active: var(--palette-on-accent);
--fg-toggle-on: var(--palette-on-accent);
--fg-counter: var(--palette-on-accent);
--link: var(--palette-blue);
--fg-completion-match: var(--palette-blue);
--info: var(--palette-blue);
--vcs-modified: var(--palette-blue);
--gutter-modified: var(--palette-blue);
--syn-link: var(--palette-blue);
--link-hover: var(--palette-cyan);
--danger: var(--palette-red);
--vcs-untracked: var(--palette-red);
--vcs-conflict: var(--palette-red);
--gutter-deleted: var(--palette-red);
--wave-error: var(--palette-red);
--syn-invalid: var(--palette-red);
--chart-series: var(--palette-red);
--warning: var(--palette-yellow);
--wave-warning: var(--palette-yellow);
--fg-bulb: var(--palette-yellow);
--vcs-ignored: var(--palette-yellow);
--success: var(--palette-green);
--vcs-added: var(--palette-green);
--gutter-added: var(--palette-green);
--db-connected: var(--palette-green);
--bg-toggle-on: var(--palette-green);
--db-connected-ring: var(--palette-green);
--vcs-deleted: var(--palette-muted);
--bg-search-match: color-mix(in srgb, var(--palette-yellow) 22%, var(--palette-bg));
--bg-text-search-result: color-mix(in srgb, var(--palette-yellow) 22%, var(--palette-bg));
--bg-search-match-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
--bg-text-search-result-current: color-mix(in srgb, var(--palette-yellow) 36%, var(--palette-bg));
--fg-search-match: var(--palette-fg);
--border-tab-selected: var(--palette-accent);
--border-indent-guide-active: var(--palette-accent);
--focus-ring-soft: var(--palette-accent);
--bg-counter: var(--palette-accent);
--bg-button-primary-hover: color-mix(in srgb, var(--accent) 86%, var(--palette-fg));
--scrollbar-thumb: color-mix(in srgb, var(--palette-fg) 24%, transparent);
--scrollbar-thumb-hover: color-mix(in srgb, var(--palette-fg) 42%, transparent);
--bg-scrim: color-mix(in srgb, var(--palette-bg) 68%, transparent);
--bg-debug-line: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
--bg-editor-banner: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
--bg-feedback-info: color-mix(in srgb, var(--palette-blue) 12%, var(--palette-bg));
--bg-breakpoint-line: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
--bg-identifier-under-caret-write: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
--bg-feedback-error: color-mix(in srgb, var(--palette-red) 12%, var(--palette-bg));
--bg-feedback-warning: color-mix(in srgb, var(--palette-yellow) 12%, var(--palette-bg));
--editor-warning-background: color-mix(in srgb, var(--palette-yellow) 12%, var(--palette-bg));
--bg-feedback-success: color-mix(in srgb, var(--palette-green) 12%, var(--palette-bg));
--border-feedback-info: color-mix(in srgb, var(--palette-blue) 55%, var(--palette-bg));
--border-feedback-error: color-mix(in srgb, var(--palette-red) 55%, var(--palette-bg));
--border-feedback-warning: color-mix(in srgb, var(--palette-yellow) 55%, var(--palette-bg));
--border-feedback-success: color-mix(in srgb, var(--palette-green) 55%, var(--palette-bg));
--editor-error-decoration: underline wavy var(--wave-error);
--editor-warning-decoration: underline wavy var(--wave-warning);
--matched-brace-weight: var(--fw-code-bold);
--shadow-popup: 0 4px 16px #00000024;
--shadow-tooltip: 0 4px 16px #00000024;
--shadow-dialog: 0 10px 36px #00000033;
--shadow-balloon: 0 10px 36px #00000033;
--traffic-off: var(--palette-hover);
--traffic-off-border: var(--palette-border);
--traffic-close: #ff5f57;
--traffic-close-border: #e0443e;
--traffic-close-pressed: #bf4942;
--traffic-glyph: #4d0000;
--syn-keyword: var(--palette-keyword);
--syn-string: var(--palette-green);
--syn-type: var(--palette-type);
--syn-function: var(--palette-blue);
--syn-function-call: var(--palette-blue);
--syn-comment: var(--palette-comment);
--syn-doc-comment: var(--palette-comment);
--syn-docstring: var(--palette-green);
--syn-annotation: var(--palette-orange);
--syn-decorator: var(--palette-orange);
--syn-number: var(--palette-number);
--syn-punct: var(--palette-fg);
--syn-variable: var(--palette-variable);
--syn-param: var(--palette-fg);
--syn-property: var(--palette-property);
--syn-constant: var(--palette-number);
--syn-builtin: var(--palette-cyan);
--syn-self: var(--palette-red);
--syn-keyword-arg: var(--palette-red);
--syn-predefined: var(--palette-cyan);
--syn-escape: var(--palette-cyan);
--syn-regexp: var(--palette-cyan);
--syn-tag: var(--palette-red);
--syn-attribute: var(--palette-orange);
--syn-todo: var(--palette-magenta);
--syn-markup-heading: var(--palette-blue);
--syn-markup-code: var(--palette-green);
--syn-comment-style: italic;
--syn-rainbow-1: var(--palette-cyan);
--syn-rainbow-2: var(--palette-orange);
--syn-rainbow-3: var(--palette-green);
--syn-rainbow-4: var(--palette-magenta);
--syn-rainbow-5: var(--palette-blue);
--syn-bracket-1: var(--palette-yellow);
--syn-bracket-2: var(--palette-magenta);
--syn-bracket-3: var(--palette-blue);
--ansi-black: var(--palette-ansi-black);
--ansi-bright-black: var(--palette-ansi-bright-black);
--ansi-red: var(--palette-ansi-red);
--ansi-bright-red: var(--palette-ansi-bright-red);
--ansi-green: var(--palette-ansi-green);
--ansi-bright-green: var(--palette-ansi-bright-green);
--ansi-yellow: var(--palette-ansi-yellow);
--ansi-bright-yellow: var(--palette-ansi-bright-yellow);
--ansi-blue: var(--palette-ansi-blue);
--ansi-bright-blue: var(--palette-ansi-bright-blue);
--ansi-magenta: var(--palette-ansi-magenta);
--ansi-bright-magenta: var(--palette-ansi-bright-magenta);
--ansi-cyan: var(--palette-ansi-cyan);
--ansi-bright-cyan: var(--palette-ansi-bright-cyan);
--ansi-white: var(--palette-ansi-white);
--ansi-bright-white: var(--palette-ansi-bright-white);
}
:root[data-color-theme="solarized-light"] {
--palette-yellow: #b58900;
--palette-orange: #cb4b16;
--palette-red: #dc322f;
--palette-magenta: #d33682;
--palette-violet: #6c71c4;
--palette-blue: #268bd2;
--palette-cyan: #2aa198;
--palette-green: #859900;
--palette-bg: #fdf6e3;
--palette-panel: #eee8d5;
--palette-popup: #fdf6e3;
--palette-fg: #657b83;
--palette-muted: #657b83;
--palette-border: #eee8d5;
--palette-hover: #eee8d5;
--palette-selection: #eee8d5;
--palette-line: #eee8d5;
--palette-accent: #268bd2;
--palette-on-accent: #002b36;
--palette-keyword: #859900;
--palette-type: #b58900;
--palette-number: #2aa198;
--palette-variable: #268bd2;
--palette-property: #268bd2;
--palette-comment: #93a1a1;
--palette-ansi-black: #073642;
--palette-ansi-red: #dc322f;
--palette-ansi-green: #859900;
--palette-ansi-yellow: #b58900;
--palette-ansi-blue: #268bd2;
--palette-ansi-magenta: #d33682;
--palette-ansi-cyan: #2aa198;
--palette-ansi-white: #eee8d5;
--palette-ansi-bright-black: #002b36;
--palette-ansi-bright-red: #cb4b16;
--palette-ansi-bright-green: #586e75;
--palette-ansi-bright-yellow: #657b83;
--palette-ansi-bright-blue: #839496;
--palette-ansi-bright-magenta: #6c71c4;
--palette-ansi-bright-cyan: #93a1a1;
--palette-ansi-bright-white: #fdf6e3;
--syn-string: #2aa198;
--syn-docstring: #2aa198;
--syn-markup-code: #2aa198;
--syn-tag: #268bd2;
--syn-escape: #dc322f;
}
:root[data-color-theme="solarized-dark"] {
--palette-yellow: #b58900;
--palette-orange: #cb4b16;
--palette-red: #dc322f;
--palette-magenta: #d33682;
--palette-violet: #6c71c4;
--palette-blue: #268bd2;
--palette-cyan: #2aa198;
--palette-green: #859900;
--palette-bg: #002b36;
--palette-panel: #073642;
--palette-popup: #073642;
--palette-fg: #839496;
--palette-muted: #839496;
--palette-border: #073642;
--palette-hover: #073642;
--palette-selection: #073642;
--palette-line: #073642;
--palette-accent: #268bd2;
--palette-on-accent: #002b36;
--palette-keyword: #859900;
--palette-type: #b58900;
--palette-number: #2aa198;
--palette-variable: #268bd2;
--palette-property: #268bd2;
--palette-comment: #586e75;
--palette-ansi-black: #073642;
--palette-ansi-red: #dc322f;
--palette-ansi-green: #859900;
--palette-ansi-yellow: #b58900;
--palette-ansi-blue: #268bd2;
--palette-ansi-magenta: #d33682;
--palette-ansi-cyan: #2aa198;
--palette-ansi-white: #eee8d5;
--palette-ansi-bright-black: #002b36;
--palette-ansi-bright-red: #cb4b16;
--palette-ansi-bright-green: #586e75;
--palette-ansi-bright-yellow: #657b83;
--palette-ansi-bright-blue: #839496;
--palette-ansi-bright-magenta: #6c71c4;
--palette-ansi-bright-cyan: #93a1a1;
--palette-ansi-bright-white: #fdf6e3;
--syn-string: #2aa198;
--syn-docstring: #2aa198;
--syn-markup-code: #2aa198;
--syn-tag: #268bd2;
--syn-escape: #dc322f;
}
:root[data-color-theme="one-light"] {
--palette-bg: hsl(230, 1%, 98%);
--palette-panel: hsl(230, 1%, 92%);
--palette-popup: hsl(230, 1%, 95%);
--palette-fg: hsl(230, 8%, 24%);
--palette-muted: hsl(230, 6%, 44%);
--palette-border: hsl(230, 1%, 86%);
--palette-hover: hsl(230, 1%, 90%);
--palette-selection: hsl(230, 1%, 90%);
--palette-line: color-mix(in srgb, hsl(230, 8%, 24%) 5%, hsl(230, 1%, 98%));
--palette-accent: hsl(230, 100%, 66%);
--palette-on-accent: #ffffff;
--palette-cyan: hsl(198, 99%, 37%);
--palette-blue: hsl(221, 87%, 60%);
--palette-magenta: hsl(301, 63%, 40%);
--palette-green: hsl(119, 34%, 47%);
--palette-red: hsl(5, 74%, 59%);
--palette-orange: hsl(41, 99%, 30%);
--palette-yellow: hsl(41, 99%, 38%);
--palette-comment: hsl(230, 4%, 64%);
--palette-keyword: hsl(301, 63%, 40%);
--palette-type: hsl(41, 99%, 38%);
--palette-number: hsl(41, 99%, 30%);
--palette-variable: hsl(5, 74%, 59%);
--palette-property: hsl(230, 8%, 24%);
--palette-ansi-black: hsl(230, 8%, 24%);
--palette-ansi-red: hsl(5, 74%, 59%);
--palette-ansi-green: hsl(119, 34%, 47%);
--palette-ansi-yellow: hsl(41, 99%, 38%);
--palette-ansi-blue: hsl(221, 87%, 60%);
--palette-ansi-magenta: hsl(301, 63%, 40%);
--palette-ansi-cyan: hsl(198, 99%, 37%);
--palette-ansi-white: hsl(230, 6%, 44%);
--palette-ansi-bright-black: hsl(230, 6%, 44%);
--palette-ansi-bright-red: hsl(5, 74%, 59%);
--palette-ansi-bright-green: hsl(119, 34%, 47%);
--palette-ansi-bright-yellow: hsl(41, 99%, 38%);
--palette-ansi-bright-blue: hsl(221, 87%, 60%);
--palette-ansi-bright-magenta: hsl(301, 63%, 40%);
--palette-ansi-bright-cyan: hsl(198, 99%, 37%);
--palette-ansi-bright-white: hsl(230, 8%, 24%);
}
:root[data-color-theme="one-dark"] {
--palette-bg: hsl(220, 13%, 18%);
--palette-panel: hsl(220, 13%, 15%);
--palette-popup: hsl(220, 13%, 16.5%);
--palette-fg: hsl(220, 14%, 71%);
--palette-muted: hsl(220, 9%, 55%);
--palette-border: hsl(220, 13%, 10.8%);
--palette-hover: hsl(220, 13%, 22%);
--palette-selection: hsl(220, 13%, 28%);
--palette-line: color-mix(in srgb, hsl(220, 100%, 80%) 4%, hsl(220, 13%, 18%));
--palette-accent: hsl(220, 100%, 66%);
--palette-on-accent: #000000;
--palette-cyan: hsl(187, 47%, 55%);
--palette-blue: hsl(207, 82%, 66%);
--palette-magenta: hsl(286, 60%, 67%);
--palette-green: hsl(95, 38%, 62%);
--palette-red: hsl(355, 65%, 65%);
--palette-orange: hsl(29, 54%, 61%);
--palette-yellow: hsl(39, 67%, 69%);
--palette-comment: hsl(220, 10%, 40%);
--palette-keyword: hsl(286, 60%, 67%);
--palette-type: hsl(39, 67%, 69%);
--palette-number: hsl(29, 54%, 61%);
--palette-variable: hsl(355, 65%, 65%);
--palette-property: hsl(220, 14%, 71%);
--palette-ansi-black: hsl(220, 13%, 15%);
--palette-ansi-red: hsl(355, 65%, 65%);
--palette-ansi-green: hsl(95, 38%, 62%);
--palette-ansi-yellow: hsl(39, 67%, 69%);
--palette-ansi-blue: hsl(207, 82%, 66%);
--palette-ansi-magenta: hsl(286, 60%, 67%);
--palette-ansi-cyan: hsl(187, 47%, 55%);
--palette-ansi-white: hsl(220, 14%, 71%);
--palette-ansi-bright-black: hsl(220, 9%, 55%);
--palette-ansi-bright-red: hsl(355, 65%, 65%);
--palette-ansi-bright-green: hsl(95, 38%, 62%);
--palette-ansi-bright-yellow: hsl(39, 67%, 69%);
--palette-ansi-bright-blue: hsl(207, 82%, 66%);
--palette-ansi-bright-magenta: hsl(286, 60%, 67%);
--palette-ansi-bright-cyan: hsl(187, 47%, 55%);
--palette-ansi-bright-white: #ffffff;
}
/*
* Appearance options (Settings › Appearance), applied as attributes on :root
* by the settings runtime.
+16 -14
Переглянути файл
@@ -4,9 +4,10 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { create } from "zustand";
import { call, commands, isTauri } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { BUILTIN_THEMES, isBuiltinTheme, themeKind, type BuiltinThemeId } from "@/platform/themes";
import { resolveTokenColor, toHex } from "@/platform/cssColor";
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
export type ThemeSetting = BuiltinThemeId | "system";
/** PyCharm's brand blue: the accent both Islands themes ship with. */
export const DEFAULT_ACCENT = "#3871e1";
/** Accent presets (Settings › Appearance, Welcome › Customize); the first is the Islands default. */
@@ -44,8 +45,8 @@ interface AppearanceLike {
* Concrete theme id: the chosen theme, or with "Sync with OS" (legacy
* `theme = "system"`) the preferred light / dark theme for the OS mode.
*/
export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boolean): "dark" | "light" | "dark-hc" {
const concrete = (v: string | undefined, d: "dark" | "light") => (v === "dark" || v === "light" || v === "dark-hc" ? v : d);
export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boolean): BuiltinThemeId {
const concrete = (v: string | undefined, d: "dark" | "light") => (isBuiltinTheme(v) ? v : d);
if (a?.syncWithOs || a?.theme === "system")
return systemLight ? concrete(a?.preferredLightTheme, "light") : concrete(a?.preferredDarkTheme, "dark");
return concrete(a?.theme, "light");
@@ -53,7 +54,7 @@ export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boole
/** `dark-hc` resolves to `dark` plus `data-contrast="high"`. */
export function resolveTheme(setting: string | undefined, systemLight: boolean): "dark" | "light" {
return resolveThemeId({ theme: setting }, systemLight) === "light" ? "light" : "dark";
return themeKind(resolveThemeId({ theme: setting }, systemLight));
}
const ACCENT_RE = /^#[0-9a-f]{3,8}$/i;
@@ -82,17 +83,21 @@ export function useApplyTheme() {
}
/**
* Sets `data-theme` / `data-contrast` for a built-in theme id. While a plugin
* Sets `data-theme` / `data-contrast` / `data-color-theme` (the palette themes
* over a light / dark base, tokens.css) for a built-in theme id. While a plugin
* theme is active (`data-plugin-theme-kind`), its light / dark base wins.
*/
export function applyThemeAttributes(id: "dark" | "light" | "dark-hc", root: HTMLElement = document.documentElement) {
export function applyThemeAttributes(id: BuiltinThemeId, root: HTMLElement = document.documentElement) {
const pluginKind = root.dataset.pluginThemeKind;
if (pluginKind === "light" || pluginKind === "dark") {
delete root.dataset.colorTheme;
root.dataset.theme = pluginKind;
delete root.dataset.contrast;
return;
}
root.dataset.theme = id === "light" ? "light" : "dark";
if (id === "light" || id === "dark" || id === "dark-hc") delete root.dataset.colorTheme;
else root.dataset.colorTheme = id;
root.dataset.theme = themeKind(id);
if (id === "dark-hc") root.dataset.contrast = "high";
else delete root.dataset.contrast;
}
@@ -135,6 +140,7 @@ export function rememberStartupTheme(root: HTMLElement = document.documentElemen
JSON.stringify({
theme: root.dataset.theme,
contrast: root.dataset.contrast,
colorTheme: root.dataset.colorTheme,
frame: hex("--bg-frame"),
fg: hex("--fg-muted"),
accent: hex("--accent"),
@@ -162,18 +168,14 @@ export function syncWindowBackground() {
}
export function currentTheme(): "dark" | "light" {
return currentThemeId() === "light" ? "light" : "dark";
return themeKind(currentThemeId());
}
/** Themes offered by quick switch menus, in IDEA's order. */
export const THEME_CHOICES: ReadonlyArray<{ id: "light" | "dark" | "dark-hc"; label: string }> = [
{ id: "light", label: "Islands Light" },
{ id: "dark", label: "Islands Dark" },
{ id: "dark-hc", label: "High Contrast" },
];
export const THEME_CHOICES = BUILTIN_THEMES;
/** The concrete theme in effect (`dark-hc` kept distinct). */
export function currentThemeId(): "dark" | "light" | "dark-hc" {
export function currentThemeId(): BuiltinThemeId {
const override = useThemeOverride.getState().theme;
return override
? resolveThemeId({ theme: override }, prefersLight())