mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
feat(themes): Solarized and One theme plugins (#92)
Solarized Light/Dark and One Light/Dark ship as theme plugins (plugins/theme-solarized, plugins/theme-one) with complete UI tokens, editor schemes and terminal colours, and the original palettes' MIT notices. Installing a plugin now also copies its LICENSE/NOTICE/COPYING. Theme handling that benefits every plugin theme: - The last plugin theme is restored before first paint (splash, workbench); the native window background follows the theme. - HSL / color-mix() / var() colours are resolved to RGB for native windows, the Settings window and Mermaid. - Mermaid, the terminal and editor schemes follow switches between two themes of the same kind. - An explicitly chosen editor scheme wins over a plugin theme's editor colours. Supersedes #85. Co-authored-by: bghtyu <3931591+bghtyu@users.noreply.github.com> Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
This commit is contained in:
@@ -100,6 +100,17 @@ 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
|
||||
|
||||
@@ -29,14 +29,18 @@ pub use types::*;
|
||||
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use ide_language::LanguageConfig;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::{Condvar, Mutex, RwLock};
|
||||
use runtime::{CallEffects, CallError, PluginEngine, Registrations, Runtime, RuntimeSetup};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
const STATE_FILE: &str = "state.json";
|
||||
/// Top-level files copied with a package on install besides those the manifest references.
|
||||
const LICENSE_FILES: &[&str] =
|
||||
&["LICENSE", "LICENSE.md", "LICENSE.txt", "NOTICE", "NOTICE.md", "NOTICE.txt", "COPYING"];
|
||||
/// Dev plugins live here, relative to a project root.
|
||||
pub const DEV_PLUGINS_DIR: &str = ".openidea/plugins";
|
||||
|
||||
@@ -52,6 +56,10 @@ struct PersistedState {
|
||||
struct ThemeRef {
|
||||
plugin: String,
|
||||
theme: String,
|
||||
/// The theme's window frame colour as the UI resolved it (`#rrggbb[aa]`), so
|
||||
/// new windows open in it before the plugin is loaded.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
frame: Option<String>,
|
||||
}
|
||||
|
||||
struct Meta {
|
||||
@@ -95,6 +103,8 @@ pub struct PluginHost {
|
||||
active_files: Arc<Mutex<HashMap<String, String>>>,
|
||||
/// Serializes loading / unloading so concurrent windows can't race.
|
||||
lifecycle: Mutex<()>,
|
||||
/// Set once the first [`load_all`](Self::load_all) has finished.
|
||||
initial_load: (Mutex<bool>, Condvar),
|
||||
}
|
||||
|
||||
impl PluginHost {
|
||||
@@ -114,6 +124,7 @@ impl PluginHost {
|
||||
state: Mutex::new(state),
|
||||
active_files: Arc::default(),
|
||||
lifecycle: Mutex::new(()),
|
||||
initial_load: (Mutex::new(false), Condvar::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,9 +140,24 @@ impl PluginHost {
|
||||
self.load_dir(&dir, PluginSource::Installed, None);
|
||||
}
|
||||
}
|
||||
let (done, cv) = &self.initial_load;
|
||||
*done.lock() = true;
|
||||
cv.notify_all();
|
||||
self.emit(None, PluginEvent::CommandsChanged);
|
||||
}
|
||||
|
||||
/// Waits (at most `timeout`) for the first [`load_all`](Self::load_all), so a
|
||||
/// listing made at startup already holds the installed plugins and the
|
||||
/// selected theme. Returns whether loading has finished.
|
||||
pub fn wait_initial_load(&self, timeout: Duration) -> bool {
|
||||
let (done, cv) = &self.initial_load;
|
||||
let mut done = done.lock();
|
||||
if !*done {
|
||||
cv.wait_while_for(&mut done, |d| !*d, timeout);
|
||||
}
|
||||
*done
|
||||
}
|
||||
|
||||
/// Loads new dev plugins from `<root>/.openidea/plugins/` and unloads vanished ones.
|
||||
pub fn sync_project(&self, root: &Path) {
|
||||
let _guard = self.lifecycle.lock();
|
||||
@@ -251,6 +277,11 @@ impl PluginHost {
|
||||
}
|
||||
std::fs::copy(src.join(file), to)?;
|
||||
}
|
||||
for name in LICENSE_FILES {
|
||||
if src.join(name).is_file() {
|
||||
std::fs::copy(src.join(name), staging.join(name))?;
|
||||
}
|
||||
}
|
||||
if target.exists() {
|
||||
std::fs::remove_dir_all(&target)?;
|
||||
}
|
||||
@@ -345,13 +376,45 @@ impl PluginHost {
|
||||
}
|
||||
}
|
||||
self.state.lock().theme =
|
||||
selection.map(|(plugin, theme)| ThemeRef { plugin: plugin.into(), theme: theme.into() });
|
||||
selection.map(|(plugin, theme)| ThemeRef { plugin: plugin.into(), theme: theme.into(), frame: None });
|
||||
self.save_state();
|
||||
let active = self.active_theme();
|
||||
self.emit(None, PluginEvent::ThemeChanged { theme: active.clone() });
|
||||
Ok(active)
|
||||
}
|
||||
|
||||
/// Records the selected theme's resolved window frame colour (`#rgb`,
|
||||
/// `#rrggbb` or `#rrggbbaa`); ignored without a selected theme.
|
||||
pub fn set_theme_frame(&self, frame: Option<&str>) -> IdeResult<()> {
|
||||
if let Some(f) = frame
|
||||
&& !is_hex_color(f)
|
||||
{
|
||||
return Err(IdeError::invalid(format!("not a hex colour: {f:?}")));
|
||||
}
|
||||
let changed = match self.state.lock().theme.as_mut() {
|
||||
Some(t) if t.frame.as_deref() != frame => {
|
||||
t.frame = frame.map(str::to_owned);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if changed {
|
||||
self.save_state();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Frame colour for a new window: the recorded one of the selected theme,
|
||||
/// unless its plugin is disabled. Available before plugins are loaded.
|
||||
pub fn startup_frame(&self) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
let t = state.theme.as_ref()?;
|
||||
if state.disabled.contains(&t.plugin) {
|
||||
return None;
|
||||
}
|
||||
t.frame.clone()
|
||||
}
|
||||
|
||||
/// The selected plugin theme, when its plugin is enabled and running.
|
||||
pub fn active_theme(&self) -> Option<ActiveTheme> {
|
||||
let sel = self.state.lock().theme.clone()?;
|
||||
@@ -568,6 +631,10 @@ impl Drop for PluginHost {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hex_color(s: &str) -> bool {
|
||||
s.strip_prefix('#').is_some_and(|h| matches!(h.len(), 3 | 6 | 8) && h.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
/// Plugin packages found in `dirs` (first occurrence of an id wins). Invalid
|
||||
/// manifests are skipped; packages with missing files carry a `problem`.
|
||||
pub fn scan_catalog(dirs: &[PathBuf]) -> Vec<CatalogPlugin> {
|
||||
|
||||
@@ -217,10 +217,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-dark")).unwrap();
|
||||
let theme = f.host.install_from_dir(&sample("theme-solarized")).unwrap();
|
||||
assert_eq!(theme.status, PluginStatus::Static);
|
||||
assert!(!theme.has_code);
|
||||
assert_eq!(theme.themes[0].id, "solarized-dark");
|
||||
let ids: Vec<&str> = theme.themes.iter().map(|t| t.id.as_str()).collect();
|
||||
assert_eq!(ids, ["solarized-light", "solarized-dark"]);
|
||||
let lang = f.host.install_from_dir(&sample("language-toy")).unwrap();
|
||||
assert_eq!(lang.status, PluginStatus::Static);
|
||||
assert_eq!(lang.languages[0].id, "toy");
|
||||
@@ -228,13 +229,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-dark", "solarized-dark"))).unwrap().unwrap();
|
||||
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");
|
||||
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-dark", false).unwrap();
|
||||
f.host.set_enabled("theme-solarized", 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();
|
||||
@@ -260,6 +262,49 @@ fn theme_template_installs_with_its_editor_scheme() {
|
||||
assert!(tokens.len() > 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_theme_plugins_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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_frame_follows_the_selection() {
|
||||
let f = fixture(Limits::default());
|
||||
f.host.install_from_dir(&sample("theme-one")).unwrap();
|
||||
// Nothing to attach it to without a selected theme.
|
||||
f.host.set_theme_frame(Some("#21252b")).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
f.host.set_theme(Some(("theme-one", "one-dark"))).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();
|
||||
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"));
|
||||
// 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();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
f.host.set_theme_frame(Some("#eaeaeb")).unwrap();
|
||||
f.host.set_theme(None).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_with_broken_editor_scheme_fails_to_load() {
|
||||
let f = fixture(Limits::default());
|
||||
@@ -282,12 +327,17 @@ 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-dark")).unwrap();
|
||||
f.host.install_from_dir(&sample("theme-solarized")).unwrap();
|
||||
f.host.set_enabled("hello", false).unwrap();
|
||||
f.host.set_theme(Some(("theme-solarized-dark", "solarized-dark"))).unwrap();
|
||||
f.host.set_theme(Some(("theme-solarized", "solarized-dark"))).unwrap();
|
||||
f.host.set_theme_frame(Some("#073642")).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!(!again.wait_initial_load(Duration::from_millis(1)));
|
||||
again.load_all();
|
||||
assert!(again.wait_initial_load(Duration::ZERO));
|
||||
let list = again.list(None);
|
||||
let hello = list.plugins.iter().find(|p| p.id == "hello").unwrap();
|
||||
assert!(!hello.enabled);
|
||||
@@ -388,14 +438,14 @@ fn catalog_lists_sample_packages() {
|
||||
|
||||
let catalog = scan_catalog(&[repo().join("plugins"), tmp.path().to_path_buf()]);
|
||||
let ids: Vec<&str> = catalog.iter().map(|c| c.id.as_str()).collect();
|
||||
for id in ["hello", "theme-solarized-dark", "language-toy", "x"] {
|
||||
for id in ["hello", "theme-solarized", "theme-one", "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-dark").unwrap();
|
||||
assert_eq!((theme.theme_count, theme.has_code), (1, false));
|
||||
let theme = catalog.iter().find(|c| c.id == "theme-solarized").unwrap();
|
||||
assert_eq!((theme.theme_count, theme.has_code), (2, false));
|
||||
let x = catalog.iter().find(|c| c.id == "x").unwrap();
|
||||
assert!(x.problem.as_deref().unwrap().contains("plugin.wasm"));
|
||||
}
|
||||
|
||||
+28
-2
@@ -16,6 +16,31 @@ 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
|
||||
@@ -282,8 +307,9 @@ 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 against tokens.css and keeps this reference current,
|
||||
and `cargo test -p ide-plugin` loads the template through the plugin host.
|
||||
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.
|
||||
|
||||
## Token reference
|
||||
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ code at all.
|
||||
plugins/
|
||||
├─ build.sh # builds Rust plugins into components
|
||||
├─ hello/ # Rust plugin: two commands
|
||||
├─ theme-solarized-dark/ # manifest-only: a theme
|
||||
├─ theme-solarized/ # manifest-only: Solarized Light / Dark themes
|
||||
├─ theme-one/ # manifest-only: One Light / Dark themes
|
||||
└─ language-toy/ # manifest-only: a language definition
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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 <3931591+bghtyu@users.noreply.github.com>", "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"
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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%)"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# Contribution-only plugin: no code, just a theme file.
|
||||
id = "theme-solarized-dark"
|
||||
name = "Solarized Dark Theme"
|
||||
version = "1.0.0"
|
||||
description = "Ethan Schoonover's Solarized palette, dark variant."
|
||||
authors = ["OpenIDEA contributors"]
|
||||
change-notes = "1.0.0 — Solarized Dark UI and syntax colours."
|
||||
openidea-api = "0.1"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/solarized-dark.json"
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"id": "solarized-dark",
|
||||
"name": "Solarized Dark",
|
||||
"kind": "dark",
|
||||
"tokens": {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-elev": "#03313c",
|
||||
"--bg-popup": "#073642",
|
||||
"--bg-hover": "#0a3d4a",
|
||||
"--bg-selected": "#0d4554",
|
||||
"--bg-selected-strong": "#104d5d",
|
||||
"--bg-current-line": "#04323d",
|
||||
"--bg-debug-line": "#0b3a50",
|
||||
"--bg-backdrop": "#00212a",
|
||||
"--bg-tooltip": "#073642",
|
||||
"--bg-control": "#073642",
|
||||
"--bg-control-hover": "#0a3d4a",
|
||||
"--bg-active": "#104d5d",
|
||||
"--border": "#06323d",
|
||||
"--border-popup": "#0a3d4a",
|
||||
"--border-strong": "#124b59",
|
||||
"--border-window": "#0a3d4a",
|
||||
"--control-off": "#2c5662",
|
||||
"--fg-strong": "#eee8d5",
|
||||
"--fg-emphasis": "#d9d4c1",
|
||||
"--fg": "#93a1a1",
|
||||
"--fg-secondary": "#839496",
|
||||
"--fg-muted": "#839496",
|
||||
"--fg-subtle": "#657b83",
|
||||
"--fg-faint": "#586e75",
|
||||
"--fg-gutter": "#4c636b",
|
||||
"--fg-tooltip": "#eee8d5",
|
||||
"--fg-on-accent": "#002b36",
|
||||
"--accent": "#268bd2",
|
||||
"--link": "#268bd2",
|
||||
"--link-hover": "#5fa9e0",
|
||||
"--danger": "#dc322f",
|
||||
"--warning": "#b58900",
|
||||
"--success": "#859900",
|
||||
"--info": "#268bd2",
|
||||
"--vcs-modified": "#b58900",
|
||||
"--vcs-added": "#859900",
|
||||
"--vcs-deleted": "#657b83",
|
||||
"--vcs-untracked": "#cb4b16",
|
||||
"--vcs-ignored": "#586e75",
|
||||
"--vcs-conflict": "#d33682",
|
||||
"--gutter-modified": "#268bd2",
|
||||
"--gutter-added": "#859900",
|
||||
"--gutter-deleted": "#dc322f",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
"--syn-type": "#b58900",
|
||||
"--syn-function": "#268bd2",
|
||||
"--syn-comment": "#586e75",
|
||||
"--syn-annotation": "#6c71c4",
|
||||
"--syn-number": "#d33682",
|
||||
"--syn-punct": "#839496",
|
||||
"--syn-variable": "#93a1a1",
|
||||
"--syn-property": "#93a1a1",
|
||||
"--syn-constant": "#cb4b16",
|
||||
"--syn-invalid": "#dc322f",
|
||||
"--syn-inline-value": "#657b83"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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 <3931591+bghtyu@users.noreply.github.com>", "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"
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
+25
-2
@@ -3,9 +3,32 @@
|
||||
// so a new window starts in it instead of flashing the light default.
|
||||
// An external file because the CSP forbids inline scripts.
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const addStyle = (id, css) => {
|
||||
const style = document.createElement("style");
|
||||
style.id = id;
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
const last = JSON.parse(localStorage.getItem("openidea.theme") ?? "null");
|
||||
if (last?.theme) document.documentElement.dataset.theme = last.theme;
|
||||
if (last?.contrast) document.documentElement.dataset.contrast = last.contrast;
|
||||
if (last?.theme) root.dataset.theme = last.theme;
|
||||
if (last?.contrast) root.dataset.contrast = last.contrast;
|
||||
// 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");
|
||||
if (typeof plugin?.id === "string" && typeof plugin.css === "string" && !/[<\\]|url\(/i.test(plugin.css)) {
|
||||
addStyle("openidea-plugin-theme", plugin.css);
|
||||
root.dataset.pluginTheme = plugin.id;
|
||||
root.dataset.pluginThemeKind = root.dataset.theme = plugin.kind === "light" ? "light" : "dark";
|
||||
delete root.dataset.contrast;
|
||||
}
|
||||
// The splash's frame, text and accent colours, resolved by theme.ts (splash.css).
|
||||
const colors = [
|
||||
["--boot-frame", last?.frame],
|
||||
["--boot-fg", last?.fg],
|
||||
["--boot-accent", last?.accent],
|
||||
].filter(([, v]) => typeof v === "string" && /^#[0-9a-f]{3,8}$/i.test(v));
|
||||
if (colors.length) addStyle("openidea-boot-colors", `:root { ${colors.map(([k, v]) => `${k}: ${v};`).join(" ")} }`);
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
|
||||
+10
-7
@@ -1,11 +1,14 @@
|
||||
/* Linked, not inlined in index.html: release builds hash inline <style> blocks into the CSP,
|
||||
which voids style-src 'unsafe-inline' and blocks the editor's runtime-injected styles. */
|
||||
/* Start-up splash over the page until the window's launch project is open (platform/splash.ts).
|
||||
Tokens aren't loaded yet, so the colours mirror --bg-frame / --fg-muted / --accent. */
|
||||
Until tokens load, the colours are the last theme's --bg-frame / --fg-muted / --accent that
|
||||
boot.js restored as --boot-*, else these built-in ones. */
|
||||
/* Not on html: the canvas then takes body's (themed) background, which a fast resize shows before relayout. */
|
||||
body, .splash { background: #e9eaee; }
|
||||
:root[data-theme="dark"] :is(body, .splash) { background: #26282c; }
|
||||
:root:is([data-theme="dark-hc"], [data-theme="dark"][data-contrast="high"]) :is(body, .splash) { background: #1a1b1e; }
|
||||
body, .splash { background: var(--bg-frame, var(--boot-frame, #e9eaee)); }
|
||||
:root[data-theme="dark"] :is(body, .splash) { background: var(--bg-frame, var(--boot-frame, #26282c)); }
|
||||
:root:is([data-theme="dark-hc"], [data-theme="dark"][data-contrast="high"]) :is(body, .splash) {
|
||||
background: var(--bg-frame, var(--boot-frame, #1a1b1e));
|
||||
}
|
||||
.splash {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -17,7 +20,7 @@ body, .splash { background: #e9eaee; }
|
||||
gap: 14px;
|
||||
padding-bottom: 38px;
|
||||
box-sizing: border-box;
|
||||
color: #6c707e;
|
||||
color: var(--fg-muted, var(--boot-fg, #6c707e));
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
@@ -25,7 +28,7 @@ body, .splash { background: #e9eaee; }
|
||||
transition: opacity 160ms ease-out;
|
||||
}
|
||||
.splash.splash-out { opacity: 0; pointer-events: none; }
|
||||
:root[data-theme^="dark"] .splash { color: #8c8f95; }
|
||||
:root[data-theme^="dark"] .splash { color: var(--fg-muted, var(--boot-fg, #8c8f95)); }
|
||||
.splash-logo { width: 56px; height: 56px; }
|
||||
/* 15px Inter: 2048 units per em, the line box from ascender to descender. */
|
||||
.splash-wordmark { width: 73.2px; height: 18.15px; }
|
||||
@@ -34,7 +37,7 @@ body, .splash { background: #e9eaee; }
|
||||
.splash-bar > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #3871e1;
|
||||
background: var(--accent, var(--boot-accent, #3871e1));
|
||||
transform-origin: left;
|
||||
animation: splash-fill 4s cubic-bezier(0.1, 0.7, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
convertIcls,
|
||||
@@ -77,6 +77,73 @@ 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
|
||||
|
||||
@@ -330,6 +330,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
plugin::plugin_run_command,
|
||||
plugin::plugin_set_active_file,
|
||||
plugin::plugin_set_theme,
|
||||
plugin::plugin_set_theme_frame,
|
||||
perf::app_metrics,
|
||||
perf::clear_caches,
|
||||
analysis::inspect_code,
|
||||
|
||||
@@ -11,9 +11,13 @@ use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{AppHandle, Manager, State, Window};
|
||||
|
||||
/// How long `plugin_list` waits for the startup load before answering with what it has.
|
||||
const INITIAL_LOAD_WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Window label → its event channel.
|
||||
static SUBSCRIBERS: LazyLock<Mutex<HashMap<String, Channel<PluginEvent>>>> = LazyLock::new(Default::default);
|
||||
|
||||
@@ -62,6 +66,8 @@ pub async fn plugin_list(window: Window, state: State<'_, AppState>) -> IdeResul
|
||||
let host = state.plugins.clone();
|
||||
let root = state.project(window.label()).ok().map(|p| p.root.clone());
|
||||
blocking(move || {
|
||||
// A window opened at startup would otherwise see no plugins and drop the selected theme.
|
||||
host.wait_initial_load(INITIAL_LOAD_WAIT);
|
||||
if let Some(root) = &root {
|
||||
host.sync_project(root);
|
||||
}
|
||||
@@ -156,3 +162,11 @@ pub fn plugin_set_theme(
|
||||
_ => state.plugins.set_theme(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The UI reports the selected plugin theme's resolved frame colour (`#rrggbb[aa]`),
|
||||
/// which new windows open in before their page loads.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn plugin_set_theme_frame(state: State<'_, AppState>, color: Option<String>) -> IdeResult<()> {
|
||||
state.plugins.set_theme_frame(color.as_deref())
|
||||
}
|
||||
|
||||
@@ -230,11 +230,21 @@ pub fn frame_color(appearance: &AppearanceSettings, os_dark: bool) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// A `#rgb` / `#rrggbb` / `#rrggbbaa` colour sent by the UI (Tauri's own parser).
|
||||
fn hex_color(hex: &str) -> Option<Color> {
|
||||
serde_json::from_value(serde_json::Value::String(hex.to_owned())).ok()
|
||||
}
|
||||
|
||||
/// Builds a project window painted in the saved theme's frame colour, so it
|
||||
/// doesn't flash the light default before its page (and splash) loads.
|
||||
fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<WebviewWindow> {
|
||||
let appearance = app.try_state::<AppState>().map(|s| s.global_settings.effective().appearance);
|
||||
if let Some(a) = &appearance {
|
||||
let state = app.try_state::<AppState>();
|
||||
let appearance = state.as_ref().map(|s| s.global_settings.effective().appearance);
|
||||
// A plugin theme replaces the built-in one in either OS mode; the UI records its resolved frame.
|
||||
let plugin_frame = state.as_ref().and_then(|s| s.plugins.startup_frame()).and_then(|c| hex_color(&c));
|
||||
if let Some(color) = plugin_frame {
|
||||
config.background_color = Some(color);
|
||||
} else if let Some(a) = &appearance {
|
||||
config.background_color = Some(frame_color(a, false));
|
||||
}
|
||||
let window = WebviewWindowBuilder::from_config(app, &config)
|
||||
@@ -242,7 +252,7 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<
|
||||
.map_err(|e| IdeError::internal(format!("cannot create window: {e}")))?;
|
||||
apply_zoom(app, &window);
|
||||
// "Sync with OS" needs the OS mode, known once the window exists.
|
||||
if let Some(a) = appearance.filter(|a| a.sync_with_os || a.theme == "system")
|
||||
if let Some(a) = appearance.filter(|a| plugin_frame.is_none() && (a.sync_with_os || a.theme == "system"))
|
||||
&& window.theme().is_ok_and(|t| t == Theme::Dark)
|
||||
{
|
||||
let _ = window.set_background_color(Some(frame_color(&a, true)));
|
||||
@@ -418,7 +428,7 @@ pub fn open_settings_window(
|
||||
config.fullscreen = false;
|
||||
config.drag_drop_enabled = false;
|
||||
// macOS shows a child window as soon as it is attached, before the page loads: paint the theme's frame colour.
|
||||
if let Some(color) = background.and_then(|c| serde_json::from_value(serde_json::Value::String(c)).ok()) {
|
||||
if let Some(color) = background.as_deref().and_then(hex_color) {
|
||||
config.background_color = Some(color);
|
||||
}
|
||||
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
|
||||
@@ -465,6 +475,19 @@ mod tests {
|
||||
assert_eq!(frame_color(&a, true), Color(0x26, 0x28, 0x2c, 0xff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_colours_from_the_ui_parse_as_hex() {
|
||||
for (hex, expected) in [
|
||||
("#eaeaeb", Color(0xea, 0xea, 0xeb, 0xff)),
|
||||
("#21252b", Color(0x21, 0x25, 0x2b, 0xff)),
|
||||
("#8040bfcc", Color(0x80, 0x40, 0xbf, 0xcc)),
|
||||
("#fff", Color(0xff, 0xff, 0xff, 0xff)),
|
||||
] {
|
||||
assert_eq!(hex_color(hex), Some(expected), "{hex}");
|
||||
}
|
||||
assert_eq!(hex_color("hsl(220, 13%, 15%)"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_sort_main_first_then_numerically() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -690,6 +690,11 @@ export const commands = {
|
||||
pluginId: string,
|
||||
theme: ThemeDef,
|
||||
} | null, IdeError>(__TAURI_INVOKE("plugin_set_theme", { pluginId, themeId })),
|
||||
/**
|
||||
* The UI reports the selected plugin theme's resolved frame colour (`#rrggbb[aa]`),
|
||||
* which new windows open in before their page loads.
|
||||
*/
|
||||
pluginSetThemeFrame: (color: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("plugin_set_theme_frame", { color })),
|
||||
/**
|
||||
* Snapshot of process memory and subsystem sizes, summed over all projects.
|
||||
* Async: the process walk runs on the blocking pool, not the main thread.
|
||||
|
||||
@@ -26,7 +26,10 @@ export function installColorScheme(): () => void {
|
||||
if (s.settings !== prev.settings) apply();
|
||||
});
|
||||
const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(apply) : null;
|
||||
observer?.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-contrast"] });
|
||||
observer?.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme"],
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
observer?.disconnect();
|
||||
|
||||
@@ -144,7 +144,7 @@ describe("store", () => {
|
||||
|
||||
it("selects and clears a plugin theme", async () => {
|
||||
await refreshPlugins();
|
||||
await selectPluginTheme("theme-solarized-dark", "solarized-dark");
|
||||
await selectPluginTheme("theme-solarized", "solarized-dark");
|
||||
expect(usePlugins.getState().activeTheme?.theme.id).toBe("solarized-dark");
|
||||
await selectPluginTheme(null, null);
|
||||
expect(usePlugins.getState().activeTheme).toBeNull();
|
||||
@@ -238,7 +238,7 @@ 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 Dark Theme"));
|
||||
await user.click(within(list).getByText("Solarized Themes"));
|
||||
await user.click(screen.getByRole("button", { name: "Apply theme Solarized Dark" }));
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Use Default Theme" })).toBeInTheDocument());
|
||||
|
||||
@@ -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-dark"]);
|
||||
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized"]);
|
||||
});
|
||||
|
||||
it("disables and re-enables all downloaded plugins, leaving dev plugins alone", async () => {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ActiveTheme } from "@/bindings";
|
||||
import { STARTUP_THEME_KEY, syncWindowBackground } from "@/workbench/theme";
|
||||
import { createMemoryPluginsBackend, setPluginsBackend, type PluginsBackend } from "../backend";
|
||||
import {
|
||||
applyPluginTheme,
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
PLUGIN_THEME_STYLE_ID,
|
||||
trackPluginTheme,
|
||||
type CachedPluginTheme,
|
||||
} from "../bridge";
|
||||
import { usePlugins } from "../store";
|
||||
|
||||
const BOOT_JS = readFileSync("public/boot.js", "utf8");
|
||||
/** Runs public/boot.js as the next window would, before any app code. */
|
||||
const boot = () => new Function(BOOT_JS)();
|
||||
|
||||
const ONE_DARK: ActiveTheme = {
|
||||
pluginId: "theme-one",
|
||||
theme: {
|
||||
id: "one-dark",
|
||||
name: "One Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-frame": "hsl(220, 13%, 15%)",
|
||||
"--fg-muted": "hsl(220, 9%, 55%)",
|
||||
"--accent": "hsl(220, 100%, 66%)",
|
||||
},
|
||||
},
|
||||
};
|
||||
const SOLARIZED_LIGHT: ActiveTheme = {
|
||||
pluginId: "theme-solarized",
|
||||
theme: { id: "solarized-light", name: "Solarized Light", kind: "light", tokens: { "--bg-frame": "#eee8d5" } },
|
||||
};
|
||||
|
||||
/** Computed style as a browser would resolve the active plugin theme. */
|
||||
function computedColors(values: Record<string, string>) {
|
||||
return vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getPropertyValue: (name: string) => values[name] ?? "",
|
||||
color: "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
function resetRoot() {
|
||||
const root = document.documentElement;
|
||||
for (const key of Object.keys(root.dataset)) delete root.dataset[key];
|
||||
document.getElementById(PLUGIN_THEME_STYLE_ID)?.remove();
|
||||
document.getElementById("openidea-boot-colors")?.remove();
|
||||
}
|
||||
|
||||
let backend: PluginsBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
backend = createMemoryPluginsBackend();
|
||||
setPluginsBackend(backend);
|
||||
usePlugins.setState({ plugins: [], activeTheme: null, loaded: false, error: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
applyPluginTheme(null);
|
||||
setPluginsBackend(null);
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
});
|
||||
|
||||
describe("plugin theme at start-up", () => {
|
||||
it("restores the last plugin theme and splash colours before the app loads", () => {
|
||||
const style = computedColors({ "--bg-frame": "#21252b", "--fg-muted": "#828997", "--accent": "#5c8aff" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
syncWindowBackground();
|
||||
stop();
|
||||
style.mockRestore();
|
||||
resetRoot();
|
||||
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(root.dataset.pluginThemeKind).toBe("dark");
|
||||
expect(root.dataset.theme).toBe("dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)?.textContent).toContain("--bg-frame: hsl(220, 13%, 15%);");
|
||||
expect(document.getElementById("openidea-boot-colors")?.textContent).toBe(
|
||||
":root { --boot-frame: #21252b; --boot-fg: #828997; --boot-accent: #5c8aff; }",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the restored theme until the plugin list arrives, then follows it", () => {
|
||||
const cached: CachedPluginTheme = {
|
||||
id: "theme-one/one-dark",
|
||||
kind: "dark",
|
||||
css: ':root[data-plugin-theme]:not([data-plugin-theme=""]) {\n --bg-frame: #21252b;\n}\n',
|
||||
};
|
||||
localStorage.setItem(PLUGIN_THEME_CACHE_KEY, JSON.stringify(cached));
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
const stop = trackPluginTheme();
|
||||
// Nothing listed yet: the boot theme stays.
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).not.toBeNull();
|
||||
// A failed listing doesn't drop it either.
|
||||
usePlugins.setState({ loaded: true, error: "backend unavailable" });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
|
||||
usePlugins.setState({ error: null, activeTheme: SOLARIZED_LIGHT });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-solarized/solarized-light");
|
||||
expect(root.dataset.theme).toBe("light");
|
||||
expect(JSON.parse(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)!)).toMatchObject({
|
||||
id: "theme-solarized/solarized-light",
|
||||
kind: "light",
|
||||
});
|
||||
|
||||
// The plugin was disabled or uninstalled meanwhile.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
expect(root.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)).toBeNull();
|
||||
stop();
|
||||
});
|
||||
|
||||
it("ignores a cached stylesheet that could load resources", () => {
|
||||
localStorage.setItem(
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
JSON.stringify({ id: "p/t", kind: "dark", css: ":root { --bg-app: url(https://example.com/x) }" }),
|
||||
);
|
||||
localStorage.setItem(STARTUP_THEME_KEY, JSON.stringify({ theme: "light", frame: "red; color: blue" }));
|
||||
boot();
|
||||
expect(document.documentElement.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.documentElement.dataset.theme).toBe("light");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(document.getElementById("openidea-boot-colors")).toBeNull();
|
||||
});
|
||||
|
||||
it("reports the resolved frame colour once per selected theme", () => {
|
||||
const setFrame = vi.spyOn(backend, "setThemeFrame");
|
||||
computedColors({ "--bg-frame": "rgb(33, 37, 43)" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
usePlugins.setState({ activeTheme: { ...ONE_DARK } });
|
||||
expect(setFrame.mock.calls).toEqual([["#21252b"]]);
|
||||
// Selecting it again after the default theme reports it again: the backend forgot it.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
usePlugins.setState({ activeTheme: ONE_DARK });
|
||||
expect(setFrame).toHaveBeenCalledTimes(2);
|
||||
stop();
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,8 @@ export interface PluginsBackend {
|
||||
runCommand(pluginId: string, commandId: string, arg: string | null): Promise<string | null>;
|
||||
setActiveFile(path: string | null): Promise<void>;
|
||||
setTheme(pluginId: string | null, themeId: string | null): Promise<ActiveTheme | null>;
|
||||
/** The selected theme's resolved frame colour (`#rrggbb[aa]`), which new windows open in. */
|
||||
setThemeFrame(color: string | null): Promise<void>;
|
||||
/** Folder picker; `null` when cancelled. */
|
||||
pickFolder(): Promise<string | null>;
|
||||
/** Subscribes to backend plugin events (this window only). */
|
||||
@@ -40,6 +42,9 @@ export const tauriPluginsBackend: PluginsBackend = {
|
||||
await commands.pluginSetActiveFile(path);
|
||||
},
|
||||
setTheme: (pluginId, themeId) => call(commands.pluginSetTheme(pluginId, themeId)),
|
||||
setThemeFrame: async (color) => {
|
||||
await call(commands.pluginSetThemeFrame(color));
|
||||
},
|
||||
pickFolder: async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({ directory: true, multiple: false, title: "Install Plugin from Disk" });
|
||||
@@ -71,21 +76,38 @@ function ensureSubscribed() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browser simulation: the bundled sample plugins, with `hello` implemented in JS.
|
||||
|
||||
const SOLARIZED: ActiveTheme["theme"] = {
|
||||
id: "solarized-dark",
|
||||
name: "Solarized Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-elev": "#03313c",
|
||||
"--bg-popup": "#073642",
|
||||
"--fg": "#93a1a1",
|
||||
"--fg-strong": "#eee8d5",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
const SOLARIZED: 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",
|
||||
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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function samplePlugins(): PluginInfo[] {
|
||||
const base = {
|
||||
@@ -118,14 +140,14 @@ function samplePlugins(): PluginInfo[] {
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: "theme-solarized-dark",
|
||||
name: "Solarized Dark Theme",
|
||||
id: "theme-solarized",
|
||||
name: "Solarized Themes",
|
||||
version: "1.0.0",
|
||||
description: "Ethan Schoonover's Solarized palette, dark variant.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized-dark",
|
||||
description: "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized",
|
||||
status: "static",
|
||||
hasCode: false,
|
||||
themes: [{ id: "solarized-dark", name: "Solarized Dark", kind: "dark" }],
|
||||
themes: SOLARIZED.map(({ id, name, kind }) => ({ id, name, kind })),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
@@ -179,8 +201,9 @@ export function createMemoryPluginsBackend(
|
||||
const activeTheme = (): ActiveTheme | null => {
|
||||
if (!theme) return null;
|
||||
const p = plugins.find((x) => x.id === theme?.pluginId);
|
||||
if (!p?.enabled || theme.themeId !== SOLARIZED.id) return null;
|
||||
return { pluginId: p.id, theme: SOLARIZED };
|
||||
const t = SOLARIZED.find((x) => x.id === theme?.themeId);
|
||||
if (!p?.enabled || !t) return null;
|
||||
return { pluginId: p.id, theme: t };
|
||||
};
|
||||
return {
|
||||
list: async () => ({ plugins: plugins.map((p) => ({ ...p })), activeTheme: activeTheme() }),
|
||||
@@ -237,6 +260,7 @@ export function createMemoryPluginsBackend(
|
||||
emit({ type: "themeChanged", theme: t });
|
||||
return t;
|
||||
},
|
||||
setThemeFrame: async () => {},
|
||||
pickFolder: async () => null,
|
||||
onEvent: (handler) => {
|
||||
listeners.add(handler);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ActiveTheme, PluginEvent, PluginInfo } from "@/bindings";
|
||||
import { activeEditor, useEditorRegistry } from "@/editor/api";
|
||||
import { registerAction, type Action } from "@/platform/actions";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { applyThemeAttributes, currentThemeId, syncWindowBackground } from "@/workbench/theme";
|
||||
import { applyThemeAttributes, currentThemeId, syncWindowBackground, windowBackgroundHex } from "@/workbench/theme";
|
||||
import { pluginsBackend } from "./backend";
|
||||
import { refreshPlugins, runPluginCommand, usePlugins } from "./store";
|
||||
|
||||
@@ -105,10 +105,67 @@ export function applyPluginTheme(theme: ActiveTheme | null, root: HTMLElement =
|
||||
if (had || theme) syncWindowBackground();
|
||||
}
|
||||
|
||||
/** localStorage key read by public/boot.js: the selected theme's stylesheet, applied before the app loads. */
|
||||
export const PLUGIN_THEME_CACHE_KEY = "openidea.pluginTheme";
|
||||
|
||||
export interface CachedPluginTheme {
|
||||
/** `data-plugin-theme`: `<pluginId>/<themeId>`. */
|
||||
id: string;
|
||||
kind: "light" | "dark";
|
||||
css: string;
|
||||
}
|
||||
|
||||
function cachePluginTheme(theme: ActiveTheme | null) {
|
||||
try {
|
||||
if (!theme) {
|
||||
localStorage.removeItem(PLUGIN_THEME_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
const cached: CachedPluginTheme = {
|
||||
id: `${theme.pluginId}/${theme.theme.id}`,
|
||||
kind: theme.theme.kind === "light" ? "light" : "dark",
|
||||
css: pluginThemeCss(theme.theme),
|
||||
};
|
||||
localStorage.setItem(PLUGIN_THEME_CACHE_KEY, JSON.stringify(cached));
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
let reportedFrame: string | undefined;
|
||||
|
||||
/**
|
||||
* Tells the backend the theme's resolved frame colour, which new windows open
|
||||
* in. Selecting a theme clears it there, so it is sent once per selection.
|
||||
*/
|
||||
function reportThemeFrame(theme: ActiveTheme | null) {
|
||||
const frame = theme ? windowBackgroundHex() : null;
|
||||
if (!theme || !frame) {
|
||||
reportedFrame = undefined;
|
||||
return;
|
||||
}
|
||||
const key = `${theme.pluginId}/${theme.theme.id}|${frame}`;
|
||||
if (key === reportedFrame) return;
|
||||
reportedFrame = key;
|
||||
void pluginsBackend()
|
||||
.setThemeFrame(frame)
|
||||
.catch(() => (reportedFrame = undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected plugin theme and keeps it for the next start-up. Until
|
||||
* the first plugin list arrives, the theme public/boot.js restored stays.
|
||||
*/
|
||||
export function trackPluginTheme(): () => void {
|
||||
applyPluginTheme(usePlugins.getState().activeTheme);
|
||||
const sync = (s: ReturnType<typeof usePlugins.getState>) => {
|
||||
if (!s.activeTheme && (!s.loaded || s.error)) return;
|
||||
applyPluginTheme(s.activeTheme);
|
||||
cachePluginTheme(s.activeTheme);
|
||||
reportThemeFrame(s.activeTheme);
|
||||
};
|
||||
sync(usePlugins.getState());
|
||||
const unsubscribe = usePlugins.subscribe((s, prev) => {
|
||||
if (s.activeTheme !== prev.activeTheme) applyPluginTheme(s.activeTheme);
|
||||
if (s.activeTheme !== prev.activeTheme || s.loaded !== prev.loaded) sync(s);
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import type { Settings } from "@/platform/ipc";
|
||||
import { commands, type Settings } from "@/platform/ipc";
|
||||
|
||||
const settingsSet = vi.fn();
|
||||
const settingsGet = vi.fn();
|
||||
@@ -282,3 +282,29 @@ describe("Settings dialog", () => {
|
||||
expect(within(dialog).getByLabelText("UI font size")).toHaveValue("13");
|
||||
});
|
||||
});
|
||||
|
||||
describe("native Settings window background", () => {
|
||||
it.each([
|
||||
["a hex frame", "#26282c", "", "#26282c"],
|
||||
["an hsl() frame", "hsl(220, 13%, 15%)", "rgb(33, 37, 43)", "#21252b"],
|
||||
[
|
||||
"a translucent color-mix() frame",
|
||||
"color-mix(in srgb, #8040bf 80%, transparent)",
|
||||
"color(srgb 0.5 0.25 0.75 / 0.8)",
|
||||
"#8040bfcc",
|
||||
],
|
||||
])("sends %s as hex, the colour format the window command parses", async (_name, token, resolved, hex) => {
|
||||
const native = vi.spyOn(commands, "windowOpenSettings").mockResolvedValue({ status: "ok", data: null });
|
||||
const computed = vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
|
||||
try {
|
||||
await executeAction("file.settings", "appearance");
|
||||
expect(native).toHaveBeenCalledWith("appearance", false, hex);
|
||||
expect(useSettingsDialog.getState().open).toBe(false);
|
||||
} finally {
|
||||
computed.mockRestore();
|
||||
native.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { isPopupOpen, lazyComponent } from "@/ui";
|
||||
import { loadSettings, useProject } from "@/platform/project";
|
||||
import { provideSettingsSearchIndex, registerOverlay } from "@/workbench/contributions";
|
||||
import { windowBackgroundHex } from "@/workbench/theme";
|
||||
import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dialog/LazySettingsDialog";
|
||||
import {
|
||||
closeSettings,
|
||||
@@ -85,8 +86,7 @@ function settingsActions(open: (page?: string, newProjects?: boolean) => void):
|
||||
async function showSettings(page?: string, newProjects = false) {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const frame = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
|
||||
await call(commands.windowOpenSettings(page ?? null, newProjects, frame || null));
|
||||
await call(commands.windowOpenSettings(page ?? null, newProjects, windowBackgroundHex()));
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn("settings window unavailable, using the dialog", e);
|
||||
|
||||
@@ -195,7 +195,11 @@ export function activate(): () => void {
|
||||
terminalRuntime()?.applyThemeToAll();
|
||||
terminalRuntime()?.applyThemeToEmbedded();
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
||||
// A 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"],
|
||||
});
|
||||
disposers.push(() => observer.disconnect());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,36 +7,9 @@
|
||||
import type { ITheme, Terminal } from "@xterm/xterm";
|
||||
import { fontWeightOf } from "@/platform/editorSettings";
|
||||
import { BUNDLED_MONO_FAMILY, fontsLoaded, monoFontFamily, monoStack } from "@/platform/fonts";
|
||||
import { parseColor, type Rgba } from "@/platform/cssColor";
|
||||
|
||||
export type Rgba = [number, number, number, number];
|
||||
|
||||
/** Parses `#rgb[a]`, `#rrggbb[aa]`, `rgb[a](…)` and `color(srgb …)`; `null` when unknown. */
|
||||
export function parseColor(input: string): Rgba | null {
|
||||
const s = input.trim().toLowerCase();
|
||||
if (s.startsWith("#")) {
|
||||
const h = s.slice(1);
|
||||
if (!/^[0-9a-f]+$/.test(h)) return null;
|
||||
if (h.length === 3 || h.length === 4) {
|
||||
const [r, g, b, a = "f"] = h.split("") as [string, string, string, string?];
|
||||
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255];
|
||||
}
|
||||
if (h.length === 6 || h.length === 8) {
|
||||
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
const alpha = m[4] === undefined ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3]), alpha];
|
||||
}
|
||||
m = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
return [Number(m[1]) * 255, Number(m[2]) * 255, Number(m[3]) * 255, m[4] === undefined ? 1 : Number(m[4])];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export { parseColor, type Rgba };
|
||||
|
||||
export function toHex([r, g, b]: Rgba): string {
|
||||
const h = (n: number) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderMarkdown } from "./render";
|
||||
import { hasMermaid, renderMermaidIn, resetMermaidForTests } from "./mermaid";
|
||||
import { hasMermaid, renderMermaidIn, resetMermaidForTests, subscribeTheme, themeKey } from "./mermaid";
|
||||
|
||||
const m = vi.hoisted(() => ({
|
||||
loads: 0,
|
||||
@@ -108,6 +108,66 @@ describe("mermaid in the markdown preview", () => {
|
||||
expect(ids.size).toBe(2);
|
||||
});
|
||||
|
||||
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";
|
||||
const firstKey = themeKey();
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
root.dataset.pluginTheme = "theme-solarized/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();
|
||||
delete root.dataset.pluginTheme;
|
||||
}
|
||||
});
|
||||
|
||||
it("passes colours a theme writes as hsl() or color-mix() to mermaid as RGB", async () => {
|
||||
const values: Record<string, string> = {
|
||||
"--bg-editor": "hsl(220, 13%, 18%)",
|
||||
"--fg": "color-mix(in srgb, hsl(220, 14%, 71%) 90%, black)",
|
||||
"--danger": "#e06c75",
|
||||
};
|
||||
const resolved: Record<string, string> = {
|
||||
"--bg-editor": "rgb(40, 44, 52)",
|
||||
"--fg": "color(srgb 0.6 0.62 0.66)",
|
||||
};
|
||||
let probe = "";
|
||||
const computed = vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockImplementation(
|
||||
(el) =>
|
||||
(el === document.documentElement
|
||||
? { getPropertyValue: (n: string) => values[n] ?? "" }
|
||||
: {
|
||||
color: resolved[(probe = (el as HTMLElement).style.color.slice(4, -1))] ?? "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
try {
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
expect(probe).not.toBe("");
|
||||
expect(m.initialize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
themeVariables: expect.objectContaining({
|
||||
background: "rgb(40, 44, 52)",
|
||||
textColor: "rgb(153, 158, 168)",
|
||||
errorTextColor: "#e06c75",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(document.querySelector("span[hidden]")).toBeNull();
|
||||
} finally {
|
||||
computed.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("skips superseded renders", async () => {
|
||||
const root = preview(SEQ.replace("test", "stale"));
|
||||
const pass = renderMermaidIn(root);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* turns placeholders into SVG. The library is imported on first use only.
|
||||
*/
|
||||
import type { Mermaid } from "mermaid";
|
||||
import { resolveTokenColor, toRgb } from "@/platform/cssColor";
|
||||
|
||||
export const MERMAID_SELECTOR = "[data-mermaid]";
|
||||
const OUTPUT_CLASS = "mermaid-output";
|
||||
@@ -29,27 +30,34 @@ function loadMermaid(): Promise<Mermaid> {
|
||||
/** Theme identity: anything that changes the colours read below. */
|
||||
export function themeKey(): string {
|
||||
const root = document.documentElement;
|
||||
return `${root.dataset.theme ?? ""}|${root.dataset.contrast ?? ""}|${root.style.getPropertyValue("--accent")}`;
|
||||
const { theme = "", contrast = "", pluginTheme = "" } = root.dataset;
|
||||
return `${theme}|${contrast}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
|
||||
}
|
||||
|
||||
/** Calls `cb` when the theme attributes or the accent on the root change. */
|
||||
/** Calls `cb` when the theme attributes (a plugin theme's included) or the accent on the root change. */
|
||||
export function subscribeTheme(cb: () => void): () => void {
|
||||
if (typeof MutationObserver === "undefined") return () => {};
|
||||
const observer = new MutationObserver(cb);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "style"],
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
/** Diagram colours from the IDE tokens; values mermaid can't parse (e.g. `color-mix`) are skipped. */
|
||||
/**
|
||||
* Diagram colours from the IDE tokens. Mermaid parses only hex and `rgb()`, so
|
||||
* tokens a theme writes as `hsl()`, `color-mix()` or `var()` are resolved to RGB.
|
||||
*/
|
||||
function themeVariables(): Record<string, string | boolean> {
|
||||
const css = getComputedStyle(document.documentElement);
|
||||
const root = document.documentElement;
|
||||
const css = getComputedStyle(root);
|
||||
const token = (...names: string[]) => {
|
||||
for (const n of names) {
|
||||
const v = css.getPropertyValue(n).trim();
|
||||
if (/^(#[0-9a-f]{3,8}|rgba?\([^)]*\))$/i.test(v)) return v;
|
||||
const resolved = v ? resolveTokenColor(n, root) : null;
|
||||
if (resolved) return toRgb(resolved);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,40 @@ describe("color schemes", () => {
|
||||
expect(pythonItalic).toContain("--syn-python-static-italic: italic !important");
|
||||
});
|
||||
|
||||
it("applies an explicit scheme over a plugin theme of the same base", () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "dark";
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
try {
|
||||
// "Theme default" keeps the plugin theme's own editor colours.
|
||||
expect(schemeCss("theme-default", undefined, root)).toBe("");
|
||||
expect(schemeCss("dark", undefined, root)).toContain("--syn-keyword: #cf8e6d !important");
|
||||
delete root.dataset.pluginTheme;
|
||||
expect(schemeCss("dark", undefined, root)).toBe("");
|
||||
} finally {
|
||||
delete root.dataset.pluginTheme;
|
||||
}
|
||||
});
|
||||
|
||||
it("re-applies the scheme when a plugin theme of the same base is selected", async () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "dark";
|
||||
const previous = useProject.getState().settings;
|
||||
useProject.setState({ settings: { editor: { colorScheme: "dark" } } as unknown as Settings });
|
||||
const dispose = installColorScheme();
|
||||
const sheet = () => document.getElementById("openidea-editor-color-scheme")?.textContent ?? "";
|
||||
try {
|
||||
expect(sheet()).toBe("");
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
await Promise.resolve();
|
||||
expect(sheet()).toContain("--syn-keyword: #cf8e6d !important");
|
||||
} finally {
|
||||
dispose();
|
||||
delete root.dataset.pluginTheme;
|
||||
useProject.setState({ settings: previous });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps editor weights out of the schemes", () => {
|
||||
for (const id of ["light", "dark", "dark-hc"] as const) {
|
||||
expect(schemeTokens(id).has("--fw-code")).toBe(false);
|
||||
|
||||
@@ -267,7 +267,10 @@ const SAFE = /^[^;{}<>]*$/;
|
||||
export function schemeCss(setting: string | undefined, overrides: Record<string, Record<string, TextAttributesOverride>> | undefined, root?: HTMLElement | null): string {
|
||||
const id = effectiveScheme(setting, root);
|
||||
const decls: Array<[string, string]> = [];
|
||||
if (id !== themeScheme(root)) {
|
||||
// A plugin theme brings its own editor colours: an explicit scheme replaces them even on the same base.
|
||||
const el = root === undefined ? (typeof document !== "undefined" ? document.documentElement : null) : root;
|
||||
const pluginTheme = !!el?.dataset.pluginTheme && id === setting;
|
||||
if (id !== themeScheme(root) || pluginTheme) {
|
||||
const tokens = schemeTokens(id);
|
||||
for (const [k, v] of tokens) if (SCHEME_VARS.has(k) || k.startsWith("--syn-")) decls.push([k, v]);
|
||||
// The editor surface follows the scheme, not the island colour of the UI theme.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseColor, resolveTokenColor, toHex, toRgb } from "./cssColor";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("css colours", () => {
|
||||
it("parses hex, rgb() and color(srgb) and formats them back", () => {
|
||||
expect(parseColor("#eee8d5")).toEqual([238, 232, 213, 1]);
|
||||
expect(parseColor("#fff8")).toEqual([255, 255, 255, 0x88 / 255]);
|
||||
expect(parseColor("rgb(33, 37, 43)")).toEqual([33, 37, 43, 1]);
|
||||
expect(parseColor("rgba(0 0 0 / 50%)")).toEqual([0, 0, 0, 0.5]);
|
||||
expect(parseColor("color(srgb 0.5 0.25 0.75 / 0.8)")).toEqual([127.5, 63.75, 191.25, 0.8]);
|
||||
expect(parseColor("hsl(220, 13%, 15%)")).toBeNull();
|
||||
expect(toHex([33, 37, 43, 1])).toBe("#21252b");
|
||||
expect(toHex([127.5, 63.75, 191.25, 0.8])).toBe("#8040bfcc");
|
||||
expect(toRgb([33, 37, 43, 1])).toBe("rgb(33, 37, 43)");
|
||||
expect(toRgb([0, 0, 0, 0.5])).toBe("rgba(0, 0, 0, 0.5)");
|
||||
});
|
||||
|
||||
it("resolves tokens written as hsl(), color-mix() or var() through the browser", () => {
|
||||
const root = document.documentElement;
|
||||
const tokens: Record<string, string> = { "--hex": "#073642", "--hsl": "hsl(220, 13%, 15%)", "--none": "" };
|
||||
vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
(el) =>
|
||||
(el === root
|
||||
? { getPropertyValue: (n: string) => tokens[n] ?? "" }
|
||||
: {
|
||||
color: (el as HTMLElement).style.color === "var(--hsl)" ? "rgb(33, 37, 43)" : "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
expect(resolveTokenColor("--hex", root)).toEqual([7, 54, 66, 1]);
|
||||
expect(resolveTokenColor("--hsl", root)).toEqual([33, 37, 43, 1]);
|
||||
expect(resolveTokenColor("--none", root)).toBeNull();
|
||||
// The probe element is removed again.
|
||||
expect(root.querySelector("span[hidden]")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Design tokens as concrete colours. Themes may write tokens as `hsl()`,
|
||||
* `color-mix()` or `var()`; consumers outside CSS (the native window, Mermaid,
|
||||
* xterm) need RGB, so a token is resolved by the browser and parsed here.
|
||||
*/
|
||||
|
||||
/** Red, green, blue (0–255) and alpha (0–1). */
|
||||
export type Rgba = [number, number, number, number];
|
||||
|
||||
/** Parses `#rgb[a]`, `#rrggbb[aa]`, `rgb[a](…)` and `color(srgb …)`; `null` when unknown. */
|
||||
export function parseColor(input: string): Rgba | null {
|
||||
const s = input.trim().toLowerCase();
|
||||
if (s.startsWith("#")) {
|
||||
const h = s.slice(1);
|
||||
if (!/^[0-9a-f]+$/.test(h)) return null;
|
||||
if (h.length === 3 || h.length === 4) {
|
||||
const [r, g, b, a = "f"] = h.split("") as [string, string, string, string?];
|
||||
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255];
|
||||
}
|
||||
if (h.length === 6 || h.length === 8) {
|
||||
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
const alpha = m[4] === undefined ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3]), alpha];
|
||||
}
|
||||
m = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
return [Number(m[1]) * 255, Number(m[2]) * 255, Number(m[3]) * 255, m[4] === undefined ? 1 : Number(m[4])];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The computed colour of token `name` on `root`, whatever syntax the theme wrote it in. */
|
||||
export function resolveTokenColor(name: string, root: HTMLElement = document.documentElement): Rgba | null {
|
||||
if (typeof getComputedStyle === "undefined") return null;
|
||||
const raw = getComputedStyle(root).getPropertyValue(name).trim();
|
||||
if (!raw) return null;
|
||||
const direct = parseColor(raw);
|
||||
if (direct) return direct;
|
||||
const probe = root.ownerDocument.createElement("span");
|
||||
probe.hidden = true;
|
||||
probe.style.color = `var(${name})`;
|
||||
root.appendChild(probe);
|
||||
const color = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
return parseColor(color);
|
||||
}
|
||||
|
||||
const byte = (v: number) => Math.min(255, Math.max(0, Math.round(v)));
|
||||
|
||||
/** `#rrggbb`, or `#rrggbbaa` when translucent. */
|
||||
export function toHex([r, g, b, a]: Rgba): string {
|
||||
const parts = [r, g, b].map(byte);
|
||||
if (byte(a * 255) !== 255) parts.push(byte(a * 255));
|
||||
return `#${parts.map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
||||
}
|
||||
|
||||
/** `rgb(r, g, b)`, or `rgba(r, g, b, a)` when translucent. */
|
||||
export function toRgb([r, g, b, a]: Rgba): string {
|
||||
const [rr, gg, bb] = [r, g, b].map(byte);
|
||||
return a < 1 ? `rgba(${rr}, ${gg}, ${bb}, ${Math.round(a * 1000) / 1000})` : `rgb(${rr}, ${gg}, ${bb})`;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { rememberStartupTheme, STARTUP_THEME_KEY, windowBackgroundColor, windowBackgroundHex } from "./theme";
|
||||
|
||||
const mockFrame = (token: string, resolved = "") =>
|
||||
vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("window background", () => {
|
||||
it("resolves the frame to Tauri's RGBA tuple instead of passing CSS hsl() or mixes", () => {
|
||||
mockFrame("#26282c");
|
||||
expect(windowBackgroundColor()).toEqual([38, 40, 44, 255]);
|
||||
mockFrame("hsl(230, 1%, 92%)", "rgb(234, 234, 235)");
|
||||
expect(windowBackgroundColor()).toEqual([234, 234, 235, 255]);
|
||||
expect(windowBackgroundHex()).toBe("#eaeaeb");
|
||||
mockFrame("color-mix(in srgb, #8040bf 80%, transparent)", "color(srgb 0.5 0.25 0.75 / 0.8)");
|
||||
expect(windowBackgroundColor()).toEqual([128, 64, 191, 204]);
|
||||
expect(windowBackgroundHex()).toBe("#8040bfcc");
|
||||
mockFrame("", "");
|
||||
expect(windowBackgroundColor()).toBeNull();
|
||||
expect(windowBackgroundHex()).toBeNull();
|
||||
});
|
||||
|
||||
it("remembers the base and the resolved splash colours for the next window", () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "light";
|
||||
const values: Record<string, string> = { "--bg-frame": "#eee8d5", "--fg-muted": "#657b83", "--accent": "#268bd2" };
|
||||
vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({
|
||||
getPropertyValue: (n: string) => values[n] ?? "",
|
||||
} as unknown as CSSStyleDeclaration);
|
||||
rememberStartupTheme(root);
|
||||
expect(JSON.parse(localStorage.getItem(STARTUP_THEME_KEY)!)).toEqual({
|
||||
theme: "light",
|
||||
frame: "#eee8d5",
|
||||
fg: "#657b83",
|
||||
accent: "#268bd2",
|
||||
});
|
||||
});
|
||||
});
|
||||
+54
-9
@@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { resolveTokenColor, toHex } from "@/platform/cssColor";
|
||||
|
||||
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
|
||||
/** PyCharm's brand blue: the accent both Islands themes ship with. */
|
||||
@@ -64,7 +65,6 @@ export function useApplyTheme() {
|
||||
const id = override
|
||||
? resolveThemeId({ theme: override }, systemLight)
|
||||
: resolveThemeId(appearance as AppearanceLike | undefined, systemLight);
|
||||
const theme = id === "light" ? "light" : "dark";
|
||||
const accent = appearance?.accent;
|
||||
|
||||
// Until settings arrive, keep the theme public/boot.js restored rather than the light default.
|
||||
@@ -77,13 +77,8 @@ export function useApplyTheme() {
|
||||
const custom = !!accent && ACCENT_RE.test(accent) && !THEME_ACCENTS.has(accent.toLowerCase());
|
||||
if (custom) root.style.setProperty("--accent", accent);
|
||||
else root.style.removeProperty("--accent");
|
||||
try {
|
||||
localStorage.setItem("openidea.theme", JSON.stringify({ theme, contrast: root.dataset.contrast }));
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
syncWindowBackground();
|
||||
}, [pending, theme, id, accent]);
|
||||
}, [pending, id, accent]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,14 +97,64 @@ export function applyThemeAttributes(id: "dark" | "light" | "dark-hc", root: HTM
|
||||
else delete root.dataset.contrast;
|
||||
}
|
||||
|
||||
/**
|
||||
* The frame colour as Tauri's `[r, g, b, a]` (0–255). Tauri parses neither
|
||||
* `hsl()` nor `color-mix()`, which themes may use, so it is resolved here.
|
||||
*/
|
||||
export function windowBackgroundColor(
|
||||
root: HTMLElement = document.documentElement,
|
||||
): [number, number, number, number] | null {
|
||||
const c = resolveTokenColor("--bg-frame", root);
|
||||
if (!c) return null;
|
||||
const [r, g, b, a] = c;
|
||||
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a * 255)];
|
||||
}
|
||||
|
||||
/** The frame colour as `#rrggbb[aa]`, for native window commands that take a string. */
|
||||
export function windowBackgroundHex(root: HTMLElement = document.documentElement): string | null {
|
||||
const c = resolveTokenColor("--bg-frame", root);
|
||||
return c ? toHex(c) : null;
|
||||
}
|
||||
|
||||
/** localStorage key read by public/boot.js. */
|
||||
export const STARTUP_THEME_KEY = "openidea.theme";
|
||||
|
||||
/**
|
||||
* Remembers the theme for public/boot.js, which applies it to the next window
|
||||
* before any stylesheet loads: the light / dark base, and the resolved frame,
|
||||
* text and accent colours its splash is painted in.
|
||||
*/
|
||||
export function rememberStartupTheme(root: HTMLElement = document.documentElement) {
|
||||
const hex = (name: string) => {
|
||||
const c = resolveTokenColor(name, root);
|
||||
return c ? toHex(c) : undefined;
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STARTUP_THEME_KEY,
|
||||
JSON.stringify({
|
||||
theme: root.dataset.theme,
|
||||
contrast: root.dataset.contrast,
|
||||
frame: hex("--bg-frame"),
|
||||
fg: hex("--fg-muted"),
|
||||
accent: hex("--accent"),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a live resize outruns the page, the native window and the webview (WebView2's default
|
||||
* background) show their own colours; match both to the frame. The webview one is only set at
|
||||
* window creation otherwise, so a theme switched later left it light.
|
||||
* window creation otherwise, so a theme switched later left it light. Also remembers the theme
|
||||
* for the next window's start-up.
|
||||
*/
|
||||
export function syncWindowBackground() {
|
||||
rememberStartupTheme();
|
||||
if (!isTauri()) return;
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
|
||||
const color = windowBackgroundColor();
|
||||
if (!color) return;
|
||||
getCurrentWebviewWindow()
|
||||
.setBackgroundColor(color)
|
||||
|
||||
Reference in New Issue
Block a user