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