Merge branch 'main' into screen-sharing

Antonio Scandurra created

# Conflicts:
#	crates/collab/src/integration_tests.rs
#	crates/collab/src/main.rs
#	styles/src/styleTree/workspace.ts

Change summary

Cargo.lock                                        | 152 +++--
crates/chat_panel/Cargo.toml                      |  20 
crates/chat_panel/src/chat_panel.rs               | 433 -----------------
crates/client/src/client.rs                       | 117 +++-
crates/collab/.env.toml                           |   2 
crates/collab/k8s/manifest.template.yml           |  32 -
crates/collab/src/api.rs                          |  20 
crates/collab/src/auth.rs                         |  25 
crates/collab/src/db.rs                           |  15 
crates/collab/src/db_tests.rs                     |  32 
crates/collab/src/integration_tests.rs            |   3 
crates/collab/src/main.rs                         |  14 
crates/collab/src/rpc.rs                          |   9 
crates/db/src/items.rs                            | 325 +++++++----
crates/editor/src/editor.rs                       |  86 +++
crates/editor/src/element.rs                      |  21 
crates/gpui/src/keymap.rs                         |   4 
crates/terminal/Cargo.toml                        |   1 
crates/terminal/src/mappings/colors.rs            | 130 ++--
crates/terminal/src/terminal.rs                   |   2 
crates/terminal/src/terminal_container_view.rs    |  21 
crates/terminal/src/terminal_element.rs           |  25 
crates/terminal/src/terminal_view.rs              |   1 
crates/theme/src/theme.rs                         |  86 ++
crates/theme_testbench/Cargo.toml                 |  18 
crates/theme_testbench/src/theme_testbench.rs     | 357 ++++++++++++++
crates/zed/Cargo.toml                             |   2 
crates/zed/build.rs                               |   4 
crates/zed/src/main.rs                            |   6 
script/zed-with-local-servers                     |   2 
styles/package.json                               |  33 
styles/src/buildThemes.ts                         |  25 
styles/src/colorSchemes.ts                        |  35 +
styles/src/styleTree/app.ts                       |  65 +-
styles/src/styleTree/chatPanel.ts                 | 108 ----
styles/src/styleTree/commandPalette.ts            |  28 
styles/src/styleTree/components.ts                | 250 ++++++---
styles/src/styleTree/contactFinder.ts             |  32 
styles/src/styleTree/contactList.ts               |  85 +-
styles/src/styleTree/contactNotification.ts       |  21 
styles/src/styleTree/contactsPopover.ts           |  19 
styles/src/styleTree/contextMenu.ts               |  43 
styles/src/styleTree/editor.ts                    | 255 ++++++---
styles/src/styleTree/hoverPopover.ts              |  40 
styles/src/styleTree/incomingCallNotification.ts  |  25 
styles/src/styleTree/picker.ts                    |  68 +-
styles/src/styleTree/projectDiagnostics.ts        |  11 
styles/src/styleTree/projectPanel.ts              |  30 
styles/src/styleTree/projectSharedNotification.ts |  26 
styles/src/styleTree/search.ts                    |  50 +
styles/src/styleTree/statusBar.ts                 |  79 +-
styles/src/styleTree/tabBar.ts                    |  92 +-
styles/src/styleTree/terminal.ts                  |  91 +--
styles/src/styleTree/tooltip.ts                   |  19 
styles/src/styleTree/updateNotification.ts        |  17 
styles/src/styleTree/workspace.ts                 | 153 ++---
styles/src/themes.ts                              |  31 -
styles/src/themes/abruzzo.ts                      |  28 -
styles/src/themes/andromeda.ts                    |  26 
styles/src/themes/brushtrees.ts                   |  28 -
styles/src/themes/cave.ts                         |  51 +
styles/src/themes/common/colorScheme.ts           |  78 +++
styles/src/themes/common/ramps.ts                 | 202 +++++++
styles/src/themes/internal/zed-pro.ts             |  30 +
styles/src/themes/one-dark.ts                     |  17 
styles/src/themes/one-light.ts                    |  17 
styles/src/themes/rose-pine-dawn.ts               |  26 
styles/src/themes/rose-pine-moon.ts               |  26 
styles/src/themes/rose-pine.ts                    |   4 
styles/src/themes/sandcastle.ts                   |   8 
styles/src/themes/solarized.ts                    |  28 
styles/src/themes/sulphurpool.ts                  |  28 
styles/src/themes/summercamp.ts                   |  26 
styles/src/themes/summerfruit.ts                  |  28 -
styles/src/themes/template.ts                     |  10 
75 files changed, 2,446 insertions(+), 1,911 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -840,22 +840,6 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
-[[package]]
-name = "chat_panel"
-version = "0.1.0"
-dependencies = [
- "client",
- "editor",
- "gpui",
- "menu",
- "postage",
- "settings",
- "theme",
- "time 0.3.15",
- "util",
- "workspace",
-]
-
 [[package]]
 name = "chrono"
 version = "0.4.22"
@@ -1519,9 +1503,9 @@ dependencies = [
 
 [[package]]
 name = "cxx"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
+checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
 dependencies = [
  "cc",
  "cxxbridge-flags",
@@ -1531,9 +1515,9 @@ dependencies = [
 
 [[package]]
 name = "cxx-build"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
+checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
 dependencies = [
  "cc",
  "codespan-reporting",
@@ -1546,15 +1530,15 @@ dependencies = [
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
+checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
+checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1716,12 +1700,9 @@ dependencies = [
 
 [[package]]
 name = "dotenvy"
-version = "0.15.5"
+version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314"
-dependencies = [
- "dirs 4.0.0",
-]
+checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
 
 [[package]]
 name = "drag_and_drop"
@@ -2717,9 +2698,9 @@ dependencies = [
 
 [[package]]
 name = "iana-time-zone-haiku"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
 dependencies = [
  "cxx",
  "cxx-build",
@@ -3473,7 +3454,7 @@ dependencies = [
  "libc",
  "log",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -3902,7 +3883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
  "lock_api",
- "parking_lot_core 0.9.3",
+ "parking_lot_core 0.9.4",
 ]
 
 [[package]]
@@ -3921,15 +3902,15 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.3"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -4224,9 +4205,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.46"
+version = "1.0.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
 dependencies = [
  "unicode-ident",
 ]
@@ -4960,9 +4941,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.20.6"
+version = "0.20.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
+checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
 dependencies = [
  "log",
  "ring",
@@ -5041,7 +5022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
 dependencies = [
  "lazy_static",
- "windows-sys",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -5652,7 +5633,7 @@ dependencies = [
  "paste",
  "percent-encoding",
  "rand 0.8.5",
- "rustls 0.20.6",
+ "rustls 0.20.7",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -5973,6 +5954,18 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "theme_testbench"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "project",
+ "settings",
+ "smallvec",
+ "theme",
+ "workspace",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.37"
@@ -6159,7 +6152,7 @@ version = "0.23.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
 dependencies = [
- "rustls 0.20.6",
+ "rustls 0.20.7",
  "tokio",
  "webpki 0.22.0",
 ]
@@ -7443,43 +7436,100 @@ version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
 [[package]]
 name = "winreg"
 version = "0.10.1"
@@ -7566,9 +7616,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
 
 [[package]]
 name = "xmlparser"
-version = "0.13.3"
+version = "0.13.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
 
 [[package]]
 name = "xmlwriter"
@@ -7599,7 +7649,6 @@ dependencies = [
  "backtrace",
  "breadcrumbs",
  "call",
- "chat_panel",
  "chrono",
  "cli",
  "client",
@@ -7658,6 +7707,7 @@ dependencies = [
  "text",
  "theme",
  "theme_selector",
+ "theme_testbench",
  "thiserror",
  "tiny_http",
  "toml",

crates/chat_panel/Cargo.toml πŸ”—

@@ -1,20 +0,0 @@
-[package]
-name = "chat_panel"
-version = "0.1.0"
-edition = "2021"
-
-[lib]
-path = "src/chat_panel.rs"
-doctest = false
-
-[dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
-time = { version = "0.3", features = ["serde", "serde-well-known"] }

crates/chat_panel/src/chat_panel.rs πŸ”—

@@ -1,433 +0,0 @@
-use client::{
-    channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
-    Client,
-};
-use editor::Editor;
-use gpui::{
-    actions,
-    elements::*,
-    platform::CursorStyle,
-    views::{ItemType, Select, SelectStyle},
-    AnyViewHandle, AppContext, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
-    Subscription, Task, View, ViewContext, ViewHandle,
-};
-use menu::Confirm;
-use postage::prelude::Stream;
-use settings::{Settings, SoftWrap};
-use std::sync::Arc;
-use time::{OffsetDateTime, UtcOffset};
-use util::{ResultExt, TryFutureExt};
-
-const MESSAGE_LOADING_THRESHOLD: usize = 50;
-
-pub struct ChatPanel {
-    rpc: Arc<Client>,
-    channel_list: ModelHandle<ChannelList>,
-    active_channel: Option<(ModelHandle<Channel>, Subscription)>,
-    message_list: ListState,
-    input_editor: ViewHandle<Editor>,
-    channel_select: ViewHandle<Select>,
-    local_timezone: UtcOffset,
-    _observe_status: Task<()>,
-}
-
-pub enum Event {}
-
-actions!(chat_panel, [LoadMoreMessages]);
-
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(ChatPanel::send);
-    cx.add_action(ChatPanel::load_more_messages);
-}
-
-impl ChatPanel {
-    pub fn new(
-        rpc: Arc<Client>,
-        channel_list: ModelHandle<ChannelList>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let input_editor = cx.add_view(|cx| {
-            let mut editor =
-                Editor::auto_height(4, Some(|theme| theme.chat_panel.input_editor.clone()), cx);
-            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
-            editor
-        });
-        let channel_select = cx.add_view(|cx| {
-            let channel_list = channel_list.clone();
-            Select::new(0, cx, {
-                move |ix, item_type, is_hovered, cx| {
-                    Self::render_channel_name(
-                        &channel_list,
-                        ix,
-                        item_type,
-                        is_hovered,
-                        &cx.global::<Settings>().theme.chat_panel.channel_select,
-                        cx,
-                    )
-                }
-            })
-            .with_style(move |cx| {
-                let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
-                SelectStyle {
-                    header: theme.header.container,
-                    menu: theme.menu,
-                }
-            })
-        });
-
-        let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, {
-            let this = cx.weak_handle();
-            move |_, ix, cx| {
-                let this = this.upgrade(cx).unwrap().read(cx);
-                let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
-                this.render_message(message, cx)
-            }
-        });
-        message_list.set_scroll_handler(|visible_range, cx| {
-            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-                cx.dispatch_action(LoadMoreMessages);
-            }
-        });
-        let _observe_status = cx.spawn_weak(|this, mut cx| {
-            let mut status = rpc.status();
-            async move {
-                while (status.recv().await).is_some() {
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |_, cx| cx.notify());
-                    } else {
-                        break;
-                    }
-                }
-            }
-        });
-
-        let mut this = Self {
-            rpc,
-            channel_list,
-            active_channel: Default::default(),
-            message_list,
-            input_editor,
-            channel_select,
-            local_timezone: cx.platform().local_timezone(),
-            _observe_status,
-        };
-
-        this.init_active_channel(cx);
-        cx.observe(&this.channel_list, |this, _, cx| {
-            this.init_active_channel(cx);
-        })
-        .detach();
-        cx.observe(&this.channel_select, |this, channel_select, cx| {
-            let selected_ix = channel_select.read(cx).selected_index();
-            let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
-                let available_channels = channel_list.available_channels()?;
-                let channel_id = available_channels.get(selected_ix)?.id;
-                channel_list.get_channel(channel_id, cx)
-            });
-            if let Some(selected_channel) = selected_channel {
-                this.set_active_channel(selected_channel, cx);
-            }
-        })
-        .detach();
-
-        this
-    }
-
-    fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
-        let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
-            let channel_count;
-            let mut active_channel = None;
-
-            if let Some(available_channels) = list.available_channels() {
-                channel_count = available_channels.len();
-                if self.active_channel.is_none() {
-                    if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
-                        active_channel = list.get_channel(channel_id, cx);
-                    }
-                }
-            } else {
-                channel_count = 0;
-            }
-
-            (active_channel, channel_count)
-        });
-
-        if let Some(active_channel) = active_channel {
-            self.set_active_channel(active_channel, cx);
-        } else {
-            self.message_list.reset(0);
-            self.active_channel = None;
-        }
-
-        self.channel_select.update(cx, |select, cx| {
-            select.set_item_count(channel_count, cx);
-        });
-    }
-
-    fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
-        if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
-            {
-                let channel = channel.read(cx);
-                self.message_list.reset(channel.message_count());
-                let placeholder = format!("Message #{}", channel.name());
-                self.input_editor.update(cx, move |editor, cx| {
-                    editor.set_placeholder_text(placeholder, cx);
-                });
-            }
-            let subscription = cx.subscribe(&channel, Self::channel_did_change);
-            self.active_channel = Some((channel, subscription));
-        }
-    }
-
-    fn channel_did_change(
-        &mut self,
-        _: ModelHandle<Channel>,
-        event: &ChannelEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            ChannelEvent::MessagesUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.message_list.splice(old_range.clone(), *new_count);
-            }
-        }
-        cx.notify();
-    }
-
-    fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.global::<Settings>().theme;
-        Flex::column()
-            .with_child(
-                Container::new(ChildView::new(&self.channel_select, cx).boxed())
-                    .with_style(theme.chat_panel.channel_select.container)
-                    .boxed(),
-            )
-            .with_child(self.render_active_channel_messages())
-            .with_child(self.render_input_box(cx))
-            .boxed()
-    }
-
-    fn render_active_channel_messages(&self) -> ElementBox {
-        let messages = if self.active_channel.is_some() {
-            List::new(self.message_list.clone()).boxed()
-        } else {
-            Empty::new().boxed()
-        };
-
-        FlexItem::new(messages).flex(1., true).boxed()
-    }
-
-    fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
-        let now = OffsetDateTime::now_utc();
-        let settings = cx.global::<Settings>();
-        let theme = if message.is_pending() {
-            &settings.theme.chat_panel.pending_message
-        } else {
-            &settings.theme.chat_panel.message
-        };
-
-        Container::new(
-            Flex::column()
-                .with_child(
-                    Flex::row()
-                        .with_child(
-                            Container::new(
-                                Label::new(
-                                    message.sender.github_login.clone(),
-                                    theme.sender.text.clone(),
-                                )
-                                .boxed(),
-                            )
-                            .with_style(theme.sender.container)
-                            .boxed(),
-                        )
-                        .with_child(
-                            Container::new(
-                                Label::new(
-                                    format_timestamp(message.timestamp, now, self.local_timezone),
-                                    theme.timestamp.text.clone(),
-                                )
-                                .boxed(),
-                            )
-                            .with_style(theme.timestamp.container)
-                            .boxed(),
-                        )
-                        .boxed(),
-                )
-                .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
-                .boxed(),
-        )
-        .with_style(theme.container)
-        .boxed()
-    }
-
-    fn render_input_box(&self, cx: &AppContext) -> ElementBox {
-        let theme = &cx.global::<Settings>().theme;
-        Container::new(ChildView::new(&self.input_editor, cx).boxed())
-            .with_style(theme.chat_panel.input_editor.container)
-            .boxed()
-    }
-
-    fn render_channel_name(
-        channel_list: &ModelHandle<ChannelList>,
-        ix: usize,
-        item_type: ItemType,
-        is_hovered: bool,
-        theme: &theme::ChannelSelect,
-        cx: &AppContext,
-    ) -> ElementBox {
-        let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
-        let theme = match (item_type, is_hovered) {
-            (ItemType::Header, _) => &theme.header,
-            (ItemType::Selected, false) => &theme.active_item,
-            (ItemType::Selected, true) => &theme.hovered_active_item,
-            (ItemType::Unselected, false) => &theme.item,
-            (ItemType::Unselected, true) => &theme.hovered_item,
-        };
-        Container::new(
-            Flex::row()
-                .with_child(
-                    Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
-                        .with_style(theme.hash.container)
-                        .boxed(),
-                )
-                .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
-                .boxed(),
-        )
-        .with_style(theme.container)
-        .boxed()
-    }
-
-    fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.global::<Settings>().theme.clone();
-        let rpc = self.rpc.clone();
-        let this = cx.handle();
-
-        enum SignInPromptLabel {}
-
-        Align::new(
-            MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
-                Label::new(
-                    "Sign in to use chat".to_string(),
-                    if mouse_state.hovered() {
-                        theme.chat_panel.hovered_sign_in_prompt.clone()
-                    } else {
-                        theme.chat_panel.sign_in_prompt.clone()
-                    },
-                )
-                .boxed()
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, cx| {
-                let rpc = rpc.clone();
-                let this = this.clone();
-                cx.spawn(|mut cx| async move {
-                    if rpc
-                        .authenticate_and_connect(true, &cx)
-                        .log_err()
-                        .await
-                        .is_some()
-                    {
-                        cx.update(|cx| {
-                            if let Some(this) = this.upgrade(cx) {
-                                if this.is_focused(cx) {
-                                    this.update(cx, |this, cx| cx.focus(&this.input_editor));
-                                }
-                            }
-                        })
-                    }
-                })
-                .detach();
-            })
-            .boxed(),
-        )
-        .boxed()
-    }
-
-    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some((channel, _)) = self.active_channel.as_ref() {
-            let body = self.input_editor.update(cx, |editor, cx| {
-                let body = editor.text(cx);
-                editor.clear(cx);
-                body
-            });
-
-            if let Some(task) = channel
-                .update(cx, |channel, cx| channel.send_message(body, cx))
-                .log_err()
-            {
-                task.detach();
-            }
-        }
-    }
-
-    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-        if let Some((channel, _)) = self.active_channel.as_ref() {
-            channel.update(cx, |channel, cx| {
-                channel.load_more_messages(cx);
-            })
-        }
-    }
-}
-
-impl Entity for ChatPanel {
-    type Event = Event;
-}
-
-impl View for ChatPanel {
-    fn ui_name() -> &'static str {
-        "ChatPanel"
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let element = if self.rpc.user_id().is_some() {
-            self.render_channel(cx)
-        } else {
-            self.render_sign_in_prompt(cx)
-        };
-        let theme = &cx.global::<Settings>().theme;
-        ConstrainedBox::new(
-            Container::new(element)
-                .with_style(theme.chat_panel.container)
-                .boxed(),
-        )
-        .with_min_width(150.)
-        .boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if matches!(
-            *self.rpc.status().borrow(),
-            client::Status::Connected { .. }
-        ) {
-            cx.focus(&self.input_editor);
-        }
-    }
-}
-
-fn format_timestamp(
-    mut timestamp: OffsetDateTime,
-    mut now: OffsetDateTime,
-    local_timezone: UtcOffset,
-) -> String {
-    timestamp = timestamp.to_offset(local_timezone);
-    now = now.to_offset(local_timezone);
-
-    let today = now.date();
-    let date = timestamp.date();
-    let mut hour = timestamp.hour();
-    let mut part = "am";
-    if hour > 12 {
-        hour -= 12;
-        part = "pm";
-    }
-    if date == today {
-        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else if date.next_day() == Some(today) {
-        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else {
-        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-    }
-}

crates/client/src/client.rs πŸ”—

@@ -13,11 +13,13 @@ use async_tungstenite::tungstenite::{
     http::{Request, StatusCode},
 };
 use db::Db;
-use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
+use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
 use gpui::{
-    actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
-    AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
-    MutableAppContext, Task, View, ViewContext, ViewHandle,
+    actions,
+    serde_json::{self, Value},
+    AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
+    AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
+    ViewHandle,
 };
 use http::HttpClient;
 use lazy_static::lazy_static;
@@ -25,6 +27,7 @@ use parking_lot::RwLock;
 use postage::watch;
 use rand::prelude::*;
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
+use serde::Deserialize;
 use std::{
     any::TypeId,
     collections::HashMap,
@@ -50,6 +53,9 @@ lazy_static! {
     pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
         .ok()
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
+    pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
+        .ok()
+        .and_then(|s| if s.is_empty() { None } else { Some(s) });
 }
 
 pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -919,6 +925,37 @@ impl Client {
         self.establish_websocket_connection(credentials, cx)
     }
 
+    async fn get_rpc_url(http: Arc<dyn HttpClient>) -> Result<Url> {
+        let url = format!("{}/rpc", *ZED_SERVER_URL);
+        let response = http.get(&url, Default::default(), false).await?;
+
+        // Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
+        // The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
+        // which requires authorization via an HTTP header.
+        //
+        // For testing purposes, ZED_SERVER_URL can also set to the direct URL of
+        // of a collab server. In that case, a request to the /rpc endpoint will
+        // return an 'unauthorized' response.
+        let collab_url = if response.status().is_redirection() {
+            response
+                .headers()
+                .get("Location")
+                .ok_or_else(|| anyhow!("missing location header in /rpc response"))?
+                .to_str()
+                .map_err(EstablishConnectionError::other)?
+                .to_string()
+        } else if response.status() == StatusCode::UNAUTHORIZED {
+            url
+        } else {
+            Err(anyhow!(
+                "unexpected /rpc response status {}",
+                response.status()
+            ))?
+        };
+
+        Url::parse(&collab_url).context("invalid rpc url")
+    }
+
     fn establish_websocket_connection(
         self: &Arc<Self>,
         credentials: &Credentials,
@@ -933,28 +970,7 @@ impl Client {
 
         let http = self.http.clone();
         cx.background().spawn(async move {
-            let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
-            let rpc_response = http.get(&rpc_url, Default::default(), false).await?;
-            if rpc_response.status().is_redirection() {
-                rpc_url = rpc_response
-                    .headers()
-                    .get("Location")
-                    .ok_or_else(|| anyhow!("missing location header in /rpc response"))?
-                    .to_str()
-                    .map_err(EstablishConnectionError::other)?
-                    .to_string();
-            }
-            // Until we switch the zed.dev domain to point to the new Next.js app, there
-            // will be no redirect required, and the app will connect directly to
-            // wss://zed.dev/rpc.
-            else if rpc_response.status() != StatusCode::UPGRADE_REQUIRED {
-                Err(anyhow!(
-                    "unexpected /rpc response status {}",
-                    rpc_response.status()
-                ))?
-            }
-
-            let mut rpc_url = Url::parse(&rpc_url).context("invalid rpc url")?;
+            let mut rpc_url = Self::get_rpc_url(http).await?;
             let rpc_host = rpc_url
                 .host_str()
                 .zip(rpc_url.port_or_known_default())
@@ -997,6 +1013,7 @@ impl Client {
         let platform = cx.platform();
         let executor = cx.background();
         let telemetry = self.telemetry.clone();
+        let http = self.http.clone();
         executor.clone().spawn(async move {
             // Generate a pair of asymmetric encryption keys. The public key will be used by the
             // zed server to encrypt the user's access token, so that it can'be intercepted by
@@ -1006,6 +1023,10 @@ impl Client {
             let public_key_string =
                 String::try_from(public_key).expect("failed to serialize public key for auth");
 
+            if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
+                return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
+            }
+
             // Start an HTTP server to receive the redirect from Zed's sign-in page.
             let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
             let port = server.server_addr().port();
@@ -1084,6 +1105,50 @@ impl Client {
         })
     }
 
+    async fn authenticate_as_admin(
+        http: Arc<dyn HttpClient>,
+        login: String,
+        mut api_token: String,
+    ) -> Result<Credentials> {
+        #[derive(Deserialize)]
+        struct AuthenticatedUserResponse {
+            user: User,
+        }
+
+        #[derive(Deserialize)]
+        struct User {
+            id: u64,
+        }
+
+        // Use the collab server's admin API to retrieve the id
+        // of the impersonated user.
+        let mut url = Self::get_rpc_url(http.clone()).await?;
+        url.set_path("/user");
+        url.set_query(Some(&format!("github_login={login}")));
+        let request = Request::get(url.as_str())
+            .header("Authorization", format!("token {api_token}"))
+            .body("".into())?;
+
+        let mut response = http.send(request).await?;
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+        if !response.status().is_success() {
+            Err(anyhow!(
+                "admin user request failed {} - {}",
+                response.status().as_u16(),
+                body,
+            ))?;
+        }
+        let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
+
+        // Use the admin API token to authenticate as the impersonated user.
+        api_token.insert_str(0, "ADMIN_TOKEN:");
+        Ok(Credentials {
+            user_id: response.user.id,
+            access_token: api_token,
+        })
+    }
+
     pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
         let conn_id = self.connection_id()?;
         self.peer.disconnect(conn_id);

crates/collab/.env.toml πŸ”—

@@ -6,7 +6,5 @@ LIVE_KIT_SERVER = "http://localhost:7880"
 LIVE_KIT_KEY = "devkey"
 LIVE_KIT_SECRET = "secret"
 
-# HONEYCOMB_API_KEY=
-# HONEYCOMB_DATASET=
 # RUST_LOG=info
 # LOG_JSON=true

crates/collab/k8s/manifest.template.yml πŸ”—

@@ -65,31 +65,6 @@ spec:
                 secretKeyRef:
                   name: database
                   key: url
-            - name: SESSION_SECRET
-              valueFrom:
-                secretKeyRef:
-                  name: session
-                  key: secret
-            - name: GITHUB_APP_ID
-              valueFrom:
-                secretKeyRef:
-                  name: github
-                  key: appId
-            - name: GITHUB_CLIENT_ID
-              valueFrom:
-                secretKeyRef:
-                  name: github
-                  key: clientId
-            - name: GITHUB_CLIENT_SECRET
-              valueFrom:
-                secretKeyRef:
-                  name: github
-                  key: clientSecret
-            - name: GITHUB_PRIVATE_KEY
-              valueFrom:
-                secretKeyRef:
-                  name: github
-                  key: privateKey
             - name: API_TOKEN
               valueFrom:
                 secretKeyRef:
@@ -101,13 +76,6 @@ spec:
               value: ${RUST_LOG}
             - name: LOG_JSON
               value: "true"
-            - name: HONEYCOMB_DATASET
-              value: "collab"
-            - name: HONEYCOMB_API_KEY
-              valueFrom:
-                secretKeyRef:
-                  name: honeycomb
-                  key: apiKey
           securityContext:
             capabilities:
               # FIXME - Switch to the more restrictive `PERFMON` capability.

crates/collab/src/api.rs πŸ”—

@@ -76,7 +76,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
 
     let state = req.extensions().get::<Arc<AppState>>().unwrap();
 
-    if token != state.api_token {
+    if token != state.config.api_token {
         Err(Error::Http(
             StatusCode::UNAUTHORIZED,
             "invalid authorization token".to_string(),
@@ -88,7 +88,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
 
 #[derive(Debug, Deserialize)]
 struct AuthenticatedUserParams {
-    github_user_id: i32,
+    github_user_id: Option<i32>,
     github_login: String,
 }
 
@@ -104,7 +104,7 @@ async fn get_authenticated_user(
 ) -> Result<Json<AuthenticatedUserResponse>> {
     let user = app
         .db
-        .get_user_by_github_account(&params.github_login, Some(params.github_user_id))
+        .get_user_by_github_account(&params.github_login, params.github_user_id)
         .await?
         .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
     let metrics_id = app.db.get_user_metrics_id(user.id).await?;
@@ -156,7 +156,7 @@ async fn create_user(
     Json(params): Json<CreateUserParams>,
     Extension(app): Extension<Arc<AppState>>,
     Extension(rpc_server): Extension<Arc<rpc::Server>>,
-) -> Result<Json<CreateUserResponse>> {
+) -> Result<Json<Option<CreateUserResponse>>> {
     let user = NewUserParams {
         github_login: params.github_login,
         github_user_id: params.github_user_id,
@@ -165,7 +165,8 @@ async fn create_user(
 
     // Creating a user via the normal signup process
     let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
-        app.db
+        if let Some(result) = app
+            .db
             .create_user_from_invite(
                 &Invite {
                     email_address: params.email_address,
@@ -174,6 +175,11 @@ async fn create_user(
                 user,
             )
             .await?
+        {
+            result
+        } else {
+            return Ok(Json(None));
+        }
     }
     // Creating a user as an admin
     else if params.admin {
@@ -200,11 +206,11 @@ async fn create_user(
         .await?
         .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
 
-    Ok(Json(CreateUserResponse {
+    Ok(Json(Some(CreateUserResponse {
         user,
         metrics_id: result.metrics_id,
         signup_device_id: result.signup_device_id,
-    }))
+    })))
 }
 
 #[derive(Deserialize)]

crates/collab/src/auth.rs πŸ”—

@@ -1,7 +1,7 @@
-use std::sync::Arc;
-
-use super::db::{self, UserId};
-use crate::{AppState, Error, Result};
+use crate::{
+    db::{self, UserId},
+    AppState, Error, Result,
+};
 use anyhow::{anyhow, Context};
 use axum::{
     http::{self, Request, StatusCode},
@@ -13,6 +13,7 @@ use scrypt::{
     password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
     Scrypt,
 };
+use std::sync::Arc;
 
 pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
     let mut auth_header = req
@@ -21,7 +22,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
         .and_then(|header| header.to_str().ok())
         .ok_or_else(|| {
             Error::Http(
-                StatusCode::BAD_REQUEST,
+                StatusCode::UNAUTHORIZED,
                 "missing authorization header".to_string(),
             )
         })?
@@ -41,12 +42,18 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
         )
     })?;
 
-    let state = req.extensions().get::<Arc<AppState>>().unwrap();
     let mut credentials_valid = false;
-    for password_hash in state.db.get_access_token_hashes(user_id).await? {
-        if verify_access_token(access_token, &password_hash)? {
+    let state = req.extensions().get::<Arc<AppState>>().unwrap();
+    if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
+        if state.config.api_token == admin_token {
             credentials_valid = true;
-            break;
+        }
+    } else {
+        for password_hash in state.db.get_access_token_hashes(user_id).await? {
+            if verify_access_token(access_token, &password_hash)? {
+                credentials_valid = true;
+                break;
+            }
         }
     }
 

crates/collab/src/db.rs πŸ”—

@@ -51,7 +51,7 @@ pub trait Db: Send + Sync {
         &self,
         invite: &Invite,
         user: NewUserParams,
-    ) -> Result<NewUserResult>;
+    ) -> Result<Option<NewUserResult>>;
 
     /// Registers a new project for the given user.
     async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
@@ -482,7 +482,7 @@ impl Db for PostgresDb {
         &self,
         invite: &Invite,
         user: NewUserParams,
-    ) -> Result<NewUserResult> {
+    ) -> Result<Option<NewUserResult>> {
         let mut tx = self.pool.begin().await?;
 
         let (signup_id, existing_user_id, inviting_user_id, signup_device_id): (
@@ -506,10 +506,7 @@ impl Db for PostgresDb {
         .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
 
         if existing_user_id.is_some() {
-            Err(Error::Http(
-                StatusCode::UNPROCESSABLE_ENTITY,
-                "invitation already redeemed".to_string(),
-            ))?;
+            return Ok(None);
         }
 
         let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
@@ -576,12 +573,12 @@ impl Db for PostgresDb {
         }
 
         tx.commit().await?;
-        Ok(NewUserResult {
+        Ok(Some(NewUserResult {
             user_id,
             metrics_id,
             inviting_user_id,
             signup_device_id,
-        })
+        }))
     }
 
     // invite codes
@@ -1958,7 +1955,7 @@ mod test {
             &self,
             _invite: &Invite,
             _user: NewUserParams,
-        ) -> Result<NewUserResult> {
+        ) -> Result<Option<NewUserResult>> {
             unimplemented!()
         }
 

crates/collab/src/db_tests.rs πŸ”—

@@ -852,6 +852,7 @@ async fn test_invite_codes() {
             },
         )
         .await
+        .unwrap()
         .unwrap();
     let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
     assert_eq!(invite_count, 1);
@@ -897,6 +898,7 @@ async fn test_invite_codes() {
             },
         )
         .await
+        .unwrap()
         .unwrap();
     let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
     assert_eq!(invite_count, 0);
@@ -954,6 +956,7 @@ async fn test_invite_codes() {
         )
         .await
         .unwrap()
+        .unwrap()
         .user_id;
 
     let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
@@ -1099,6 +1102,7 @@ async fn test_signups() {
             },
         )
         .await
+        .unwrap()
         .unwrap();
     let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
     assert!(inviting_user_id.is_none());
@@ -1108,19 +1112,21 @@ async fn test_signups() {
     assert_eq!(signup_device_id.unwrap(), "device_id_0");
 
     // cannot redeem the same signup again.
-    db.create_user_from_invite(
-        &Invite {
-            email_address: signups_batch1[0].email_address.clone(),
-            email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
-        },
-        NewUserParams {
-            github_login: "some-other-github_account".into(),
-            github_user_id: 1,
-            invite_count: 5,
-        },
-    )
-    .await
-    .unwrap_err();
+    assert!(db
+        .create_user_from_invite(
+            &Invite {
+                email_address: signups_batch1[0].email_address.clone(),
+                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
+            },
+            NewUserParams {
+                github_login: "some-other-github_account".into(),
+                github_user_id: 1,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap()
+        .is_none());
 
     // cannot redeem a signup with the wrong confirmation code.
     db.create_user_from_invite(

crates/collab/src/integration_tests.rs πŸ”—

@@ -6478,8 +6478,7 @@ impl TestServer {
         Arc::new(AppState {
             db: test_db.db().clone(),
             live_kit_client: Some(Arc::new(fake_server.create_api_client())),
-            api_token: Default::default(),
-            invite_link_prefix: Default::default(),
+            config: Default::default(),
         })
     }
 

crates/collab/src/main.rs πŸ”—

@@ -30,8 +30,6 @@ pub struct Config {
     pub database_url: String,
     pub api_token: String,
     pub invite_link_prefix: String,
-    pub honeycomb_api_key: Option<String>,
-    pub honeycomb_dataset: Option<String>,
     pub live_kit_server: Option<String>,
     pub live_kit_key: Option<String>,
     pub live_kit_secret: Option<String>,
@@ -41,13 +39,12 @@ pub struct Config {
 
 pub struct AppState {
     db: Arc<dyn Db>,
-    api_token: String,
-    invite_link_prefix: String,
     live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
+    config: Config,
 }
 
 impl AppState {
-    async fn new(config: &Config) -> Result<Arc<Self>> {
+    async fn new(config: Config) -> Result<Arc<Self>> {
         let db = PostgresDb::new(&config.database_url, 5).await?;
         let live_kit_client = if let Some(((server, key), secret)) = config
             .live_kit_server
@@ -67,8 +64,7 @@ impl AppState {
         let this = Self {
             db: Arc::new(db),
             live_kit_client,
-            api_token: config.api_token.clone(),
-            invite_link_prefix: config.invite_link_prefix.clone(),
+            config,
         };
         Ok(Arc::new(this))
     }
@@ -85,9 +81,9 @@ async fn main() -> Result<()> {
 
     let config = envy::from_env::<Config>().expect("error loading config");
     init_tracing(&config);
-    let state = AppState::new(&config).await?;
+    let state = AppState::new(config).await?;
 
-    let listener = TcpListener::bind(&format!("0.0.0.0:{}", config.http_port))
+    let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
         .expect("failed to bind TCP listener");
     let rpc_server = rpc::Server::new(state.clone(), None);
 

crates/collab/src/rpc.rs πŸ”—

@@ -396,7 +396,7 @@ impl Server {
 
                 if let Some((code, count)) = invite_code {
                     this.peer.send(connection_id, proto::UpdateInviteInfo {
-                        url: format!("{}{}", this.app_state.invite_link_prefix, code),
+                        url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
                         count,
                     })?;
                 }
@@ -562,7 +562,7 @@ impl Server {
                     self.peer.send(
                         connection_id,
                         proto::UpdateInviteInfo {
-                            url: format!("{}{}", self.app_state.invite_link_prefix, &code),
+                            url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
                             count: user.invite_count as u32,
                         },
                     )?;
@@ -580,7 +580,10 @@ impl Server {
                     self.peer.send(
                         connection_id,
                         proto::UpdateInviteInfo {
-                            url: format!("{}{}", self.app_state.invite_link_prefix, invite_code),
+                            url: format!(
+                                "{}{}",
+                                self.app_state.config.invite_link_prefix, invite_code
+                            ),
                             count: user.invite_count as u32,
                         },
                     )?;

crates/db/src/items.rs πŸ”—

@@ -1,10 +1,8 @@
-use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::PathBuf, sync::Arc};
+use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
 
 use anyhow::Result;
-use rusqlite::{
-    named_params, params,
-    types::{FromSql, FromSqlError, FromSqlResult, ValueRef},
-};
+use collections::HashSet;
+use rusqlite::{named_params, params};
 
 use super::Db;
 
@@ -31,7 +29,13 @@ pub enum SerializedItemKind {
     Diagnostics,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+impl Display for SerializedItemKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&format!("{:?}", self))
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub enum SerializedItem {
     Editor(usize, PathBuf),
     Terminal(usize),
@@ -39,27 +43,6 @@ pub enum SerializedItem {
     Diagnostics(usize),
 }
 
-impl FromSql for SerializedItemKind {
-    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
-        match value {
-            ValueRef::Null => Err(FromSqlError::InvalidType),
-            ValueRef::Integer(_) => Err(FromSqlError::InvalidType),
-            ValueRef::Real(_) => Err(FromSqlError::InvalidType),
-            ValueRef::Text(bytes) => {
-                let str = std::str::from_utf8(bytes).map_err(|_| FromSqlError::InvalidType)?;
-                match str {
-                    "Editor" => Ok(SerializedItemKind::Editor),
-                    "Terminal" => Ok(SerializedItemKind::Terminal),
-                    "ProjectSearch" => Ok(SerializedItemKind::ProjectSearch),
-                    "Diagnostics" => Ok(SerializedItemKind::Diagnostics),
-                    _ => Err(FromSqlError::InvalidType),
-                }
-            }
-            ValueRef::Blob(_) => Err(FromSqlError::InvalidType),
-        }
-    }
-}
-
 impl SerializedItem {
     fn kind(&self) -> SerializedItemKind {
         match self {
@@ -82,117 +65,206 @@ impl SerializedItem {
 
 impl Db {
     fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
-        let mut lock = self.connection.lock();
-        let tx = lock.transaction()?;
-
-        // Serialize the item
-        let id = serialized_item.id();
-        {
-            let kind = format!("{:?}", serialized_item.kind());
+        self.real()
+            .map(|db| {
+                let mut lock = db.connection.lock();
+                let tx = lock.transaction()?;
+
+                // Serialize the item
+                let id = serialized_item.id();
+                {
+                    let mut stmt = tx.prepare_cached(
+                        "INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
+                    )?;
+
+                    dbg!("inserting item");
+                    stmt.execute(params![id, serialized_item.kind().to_string()])?;
+                }
 
-            let mut stmt =
-                tx.prepare_cached("INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))")?;
+                // Serialize item data
+                match &serialized_item {
+                    SerializedItem::Editor(_, path) => {
+                        dbg!("inserting path");
+                        let mut stmt = tx.prepare_cached(
+                            "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
+                        )?;
 
-            stmt.execute(params![id, kind])?;
-        }
+                        let path_bytes = path.as_os_str().as_bytes();
+                        stmt.execute(params![id, path_bytes])?;
+                    }
+                    SerializedItem::ProjectSearch(_, query) => {
+                        dbg!("inserting query");
+                        let mut stmt = tx.prepare_cached(
+                            "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
+                        )?;
 
-        // Serialize item data
-        match &serialized_item {
-            SerializedItem::Editor(_, path) => {
-                let mut stmt = tx.prepare_cached(
-                    "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
-                )?;
+                        stmt.execute(params![id, query])?;
+                    }
+                    _ => {}
+                }
 
-                let path_bytes = path.as_os_str().as_bytes();
-                stmt.execute(params![id, path_bytes])?;
-            }
-            SerializedItem::ProjectSearch(_, query) => {
-                let mut stmt = tx.prepare_cached(
-                    "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
-                )?;
+                tx.commit()?;
 
-                stmt.execute(params![id, query])?;
-            }
-            _ => {}
-        }
+                let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
+                let _ = stmt
+                    .query_map([], |row| {
+                        let zero: usize = row.get(0)?;
+                        let one: String = row.get(1)?;
 
-        tx.commit()?;
+                        dbg!(zero, one);
+                        Ok(())
+                    })?
+                    .collect::<Vec<Result<(), _>>>();
 
-        Ok(())
+                Ok(())
+            })
+            .unwrap_or(Ok(()))
     }
 
     fn delete_item(&self, item_id: usize) -> Result<()> {
-        let lock = self.connection.lock();
-
-        let mut stmt = lock.prepare_cached(
-            "
-            DELETE FROM items WHERE id = (:id);
-            DELETE FROM item_path WHERE id = (:id);
-            DELETE FROM item_query WHERE id = (:id);
-        ",
-        )?;
+        self.real()
+            .map(|db| {
+                let lock = db.connection.lock();
+
+                let mut stmt = lock.prepare_cached(
+                    r#"
+                    DELETE FROM items WHERE id = (:id);
+                    DELETE FROM item_path WHERE id = (:id);
+                    DELETE FROM item_query WHERE id = (:id);
+                    "#,
+                )?;
 
-        stmt.execute(named_params! {":id": item_id})?;
+                stmt.execute(named_params! {":id": item_id})?;
 
-        Ok(())
+                Ok(())
+            })
+            .unwrap_or(Ok(()))
     }
 
-    fn take_items(&self) -> Result<Vec<SerializedItem>> {
-        let mut lock = self.connection.lock();
-
-        let tx = lock.transaction()?;
-
-        // When working with transactions in rusqlite, need to make this kind of scope
-        // To make the borrow stuff work correctly. Don't know why, rust is wild.
-        let result = {
-            let mut read_stmt = tx.prepare_cached(
-                "
-                    SELECT items.id, items.kind, item_path.path, item_query.query
-                    FROM items
-                    LEFT JOIN item_path
-                        ON items.id = item_path.item_id
-                    LEFT JOIN item_query
-                        ON items.id = item_query.item_id
-                    ORDER BY items.id
-            ",
-            )?;
-
-            let result = read_stmt
-                .query_map([], |row| {
-                    let id: usize = row.get(0)?;
-                    let kind: SerializedItemKind = row.get(1)?;
-
-                    match kind {
-                        SerializedItemKind::Editor => {
-                            let buf: Vec<u8> = row.get(2)?;
+    fn take_items(&self) -> Result<HashSet<SerializedItem>> {
+        self.real()
+            .map(|db| {
+                let mut lock = db.connection.lock();
+
+                let tx = lock.transaction()?;
+
+                // When working with transactions in rusqlite, need to make this kind of scope
+                // To make the borrow stuff work correctly. Don't know why, rust is wild.
+                let result = {
+                    let mut editors_stmt = tx.prepare_cached(
+                        r#"
+                        SELECT items.id, item_path.path
+                        FROM items
+                        LEFT JOIN item_path
+                            ON items.id = item_path.item_id
+                        WHERE items.kind = ?;
+                        "#,
+                    )?;
+
+                    let editors_iter = editors_stmt.query_map(
+                        [SerializedItemKind::Editor.to_string()],
+                        |row| {
+                            let id: usize = row.get(0)?;
+
+                            let buf: Vec<u8> = row.get(1)?;
                             let path: PathBuf = OsStr::from_bytes(&buf).into();
 
                             Ok(SerializedItem::Editor(id, path))
-                        }
-                        SerializedItemKind::Terminal => Ok(SerializedItem::Terminal(id)),
-                        SerializedItemKind::ProjectSearch => {
-                            let query: Arc<str> = row.get(3)?;
-                            Ok(SerializedItem::ProjectSearch(id, query.to_string()))
-                        }
-                        SerializedItemKind::Diagnostics => Ok(SerializedItem::Diagnostics(id)),
-                    }
-                })?
-                .collect::<Result<Vec<SerializedItem>, rusqlite::Error>>()?;
-
-            let mut delete_stmt = tx.prepare_cached(
-                "DELETE FROM items;
-                DELETE FROM item_path;
-                DELETE FROM item_query;",
-            )?;
-
-            delete_stmt.execute([])?;
-
-            result
-        };
-
-        tx.commit()?;
-
-        Ok(result)
+                        },
+                    )?;
+
+                    let mut terminals_stmt = tx.prepare_cached(
+                        r#"
+                        SELECT items.id
+                        FROM items
+                        WHERE items.kind = ?;
+                        "#,
+                    )?;
+                    let terminals_iter = terminals_stmt.query_map(
+                        [SerializedItemKind::Terminal.to_string()],
+                        |row| {
+                            let id: usize = row.get(0)?;
+
+                            Ok(SerializedItem::Terminal(id))
+                        },
+                    )?;
+
+                    let mut search_stmt = tx.prepare_cached(
+                        r#"
+                        SELECT items.id, item_query.query
+                        FROM items
+                        LEFT JOIN item_query
+                            ON items.id = item_query.item_id
+                        WHERE items.kind = ?;
+                        "#,
+                    )?;
+                    let searches_iter = search_stmt.query_map(
+                        [SerializedItemKind::ProjectSearch.to_string()],
+                        |row| {
+                            let id: usize = row.get(0)?;
+                            let query = row.get(1)?;
+
+                            Ok(SerializedItem::ProjectSearch(id, query))
+                        },
+                    )?;
+
+                    #[cfg(debug_assertions)]
+                    let tmp =
+                        searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
+                    #[cfg(debug_assertions)]
+                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
+                    #[cfg(debug_assertions)]
+                    let searches_iter = tmp.into_iter();
+
+                    let mut diagnostic_stmt = tx.prepare_cached(
+                        r#"
+                        SELECT items.id
+                        FROM items
+                        WHERE items.kind = ?;
+                        "#,
+                    )?;
+
+                    let diagnostics_iter = diagnostic_stmt.query_map(
+                        [SerializedItemKind::Diagnostics.to_string()],
+                        |row| {
+                            let id: usize = row.get(0)?;
+
+                            Ok(SerializedItem::Diagnostics(id))
+                        },
+                    )?;
+
+                    #[cfg(debug_assertions)]
+                    let tmp =
+                        diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
+                    #[cfg(debug_assertions)]
+                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
+                    #[cfg(debug_assertions)]
+                    let diagnostics_iter = tmp.into_iter();
+
+                    let res = editors_iter
+                        .chain(terminals_iter)
+                        .chain(diagnostics_iter)
+                        .chain(searches_iter)
+                        .collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
+
+                    let mut delete_stmt = tx.prepare_cached(
+                        r#"
+                        DELETE FROM items;
+                        DELETE FROM item_path;
+                        DELETE FROM item_query;
+                        "#,
+                    )?;
+
+                    delete_stmt.execute([])?;
+
+                    res
+                };
+
+                tx.commit()?;
+
+                Ok(result)
+            })
+            .unwrap_or(Ok(HashSet::default()))
     }
 }
 
@@ -204,29 +276,32 @@ mod test {
 
     #[test]
     fn test_items_round_trip() -> Result<()> {
-        let db = Db::open_in_memory()?;
+        let db = Db::open_in_memory();
 
         let mut items = vec![
             SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
             SerializedItem::Terminal(1),
             SerializedItem::ProjectSearch(2, "Test query!".to_string()),
             SerializedItem::Diagnostics(3),
-        ];
+        ]
+        .into_iter()
+        .collect::<HashSet<_>>();
 
         for item in items.iter() {
+            dbg!("Inserting... ");
             db.write_item(item.clone())?;
         }
 
         assert_eq!(items, db.take_items()?);
 
         // Check that it's empty, as expected
-        assert_eq!(Vec::<SerializedItem>::new(), db.take_items()?);
+        assert_eq!(HashSet::default(), db.take_items()?);
 
         for item in items.iter() {
             db.write_item(item.clone())?;
         }
 
-        items.remove(2);
+        items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
         db.delete_item(2)?;
 
         assert_eq!(items, db.take_items()?);

crates/editor/src/editor.rs πŸ”—

@@ -35,9 +35,9 @@ use gpui::{
     impl_actions, impl_internal_actions,
     platform::CursorStyle,
     serde_json::json,
-    text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox,
-    Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    text_layout, AnyViewHandle, AppContext, AsyncAppContext, Axis, ClipboardItem, Element,
+    ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
+    Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -84,6 +84,7 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
+pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
@@ -94,7 +95,10 @@ pub struct SelectNext {
 }
 
 #[derive(Clone, PartialEq)]
-pub struct Scroll(pub Vector2F);
+pub struct Scroll {
+    pub scroll_position: Vector2F,
+    pub axis: Option<Axis>,
+}
 
 #[derive(Clone, PartialEq)]
 pub struct Select(pub SelectPhase);
@@ -263,7 +267,7 @@ struct ScrollbarAutoHide(bool);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::new_file);
-    cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
+    cx.add_action(Editor::scroll);
     cx.add_action(Editor::select);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
@@ -429,6 +433,69 @@ pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
 
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
+#[derive(Clone, Copy)]
+pub struct OngoingScroll {
+    last_timestamp: Instant,
+    axis: Option<Axis>,
+}
+
+impl OngoingScroll {
+    fn initial() -> OngoingScroll {
+        OngoingScroll {
+            last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION,
+            axis: None,
+        }
+    }
+
+    fn update(&mut self, axis: Option<Axis>) {
+        self.last_timestamp = Instant::now();
+        self.axis = axis;
+    }
+
+    pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
+        const UNLOCK_PERCENT: f32 = 1.9;
+        const UNLOCK_LOWER_BOUND: f32 = 6.;
+        let mut axis = self.axis;
+
+        let x = delta.x().abs();
+        let y = delta.y().abs();
+        let duration = Instant::now().duration_since(self.last_timestamp);
+        if duration > SCROLL_EVENT_SEPARATION {
+            //New ongoing scroll will start, determine axis
+            axis = if x <= y {
+                Some(Axis::Vertical)
+            } else {
+                Some(Axis::Horizontal)
+            };
+        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
+            //Check if the current ongoing will need to unlock
+            match axis {
+                Some(Axis::Vertical) => {
+                    if x > y && x >= y * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                Some(Axis::Horizontal) => {
+                    if y > x && y >= x * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                None => {}
+            }
+        }
+
+        match axis {
+            Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
+            Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
+            None => {}
+        }
+
+        axis
+    }
+}
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<MultiBuffer>,
@@ -443,6 +510,7 @@ pub struct Editor {
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
+    ongoing_scroll: OngoingScroll,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_request: Option<(Autoscroll, bool)>,
@@ -486,6 +554,7 @@ pub struct EditorSnapshot {
     pub display_snapshot: DisplaySnapshot,
     pub placeholder_text: Option<Arc<str>>,
     is_focused: bool,
+    ongoing_scroll: OngoingScroll,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
 }
@@ -1097,6 +1166,7 @@ impl Editor {
             soft_wrap_mode_override: None,
             get_field_editor_theme,
             project,
+            ongoing_scroll: OngoingScroll::initial(),
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
             autoscroll_request: None,
@@ -1189,6 +1259,7 @@ impl Editor {
         EditorSnapshot {
             mode: self.mode,
             display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
+            ongoing_scroll: self.ongoing_scroll,
             scroll_position: self.scroll_position,
             scroll_top_anchor: self.scroll_top_anchor.clone(),
             placeholder_text: self.placeholder_text.clone(),
@@ -1592,6 +1663,11 @@ impl Editor {
         });
     }
 
+    fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
+        self.ongoing_scroll.update(action.axis);
+        self.set_scroll_position(action.scroll_position, cx);
+    }
+
     fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
         self.hide_context_menu(cx);
 

crates/editor/src/element.rs πŸ”—

@@ -423,18 +423,27 @@ impl EditorElement {
             return false;
         }
 
+        let line_height = position_map.line_height;
         let max_glyph_width = position_map.em_width;
-        if !precise {
-            delta *= vec2f(max_glyph_width, position_map.line_height);
-        }
+
+        let axis = if precise {
+            //Trackpad
+            position_map.snapshot.ongoing_scroll.filter(&mut delta)
+        } else {
+            //Not trackpad
+            delta *= vec2f(max_glyph_width, line_height);
+            None //Resets ongoing scroll
+        };
 
         let scroll_position = position_map.snapshot.scroll_position();
         let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
-        let y =
-            (scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
+        let y = (scroll_position.y() * line_height - delta.y()) / line_height;
         let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
 
-        cx.dispatch_action(Scroll(scroll_position));
+        cx.dispatch_action(Scroll {
+            scroll_position,
+            axis,
+        });
 
         true
     }

crates/gpui/src/keymap.rs πŸ”—

@@ -301,13 +301,15 @@ impl Keystroke {
             }
         }
 
+        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
+
         Ok(Keystroke {
             ctrl,
             alt,
             shift,
             cmd,
             function,
-            key: key.unwrap(),
+            key,
         })
     }
 

crates/terminal/Cargo.toml πŸ”—

@@ -35,7 +35,6 @@ serde = { version = "1.0", features = ["derive"] }
 
 
 
-
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"]}

crates/terminal/src/mappings/colors.rs πŸ”—

@@ -1,77 +1,71 @@
 use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb};
 use gpui::color::Color;
-use theme::TerminalColors;
+use theme::TerminalStyle;
 
 ///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
-pub fn convert_color(alac_color: &AnsiColor, colors: &TerminalColors, modal: bool) -> Color {
-    let background = if modal {
-        colors.modal_background
-    } else {
-        colors.background
-    };
-
+pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
     match alac_color {
         //Named and theme defined colors
         alacritty_terminal::ansi::Color::Named(n) => match n {
-            alacritty_terminal::ansi::NamedColor::Black => colors.black,
-            alacritty_terminal::ansi::NamedColor::Red => colors.red,
-            alacritty_terminal::ansi::NamedColor::Green => colors.green,
-            alacritty_terminal::ansi::NamedColor::Yellow => colors.yellow,
-            alacritty_terminal::ansi::NamedColor::Blue => colors.blue,
-            alacritty_terminal::ansi::NamedColor::Magenta => colors.magenta,
-            alacritty_terminal::ansi::NamedColor::Cyan => colors.cyan,
-            alacritty_terminal::ansi::NamedColor::White => colors.white,
-            alacritty_terminal::ansi::NamedColor::BrightBlack => colors.bright_black,
-            alacritty_terminal::ansi::NamedColor::BrightRed => colors.bright_red,
-            alacritty_terminal::ansi::NamedColor::BrightGreen => colors.bright_green,
-            alacritty_terminal::ansi::NamedColor::BrightYellow => colors.bright_yellow,
-            alacritty_terminal::ansi::NamedColor::BrightBlue => colors.bright_blue,
-            alacritty_terminal::ansi::NamedColor::BrightMagenta => colors.bright_magenta,
-            alacritty_terminal::ansi::NamedColor::BrightCyan => colors.bright_cyan,
-            alacritty_terminal::ansi::NamedColor::BrightWhite => colors.bright_white,
-            alacritty_terminal::ansi::NamedColor::Foreground => colors.foreground,
-            alacritty_terminal::ansi::NamedColor::Background => background,
-            alacritty_terminal::ansi::NamedColor::Cursor => colors.cursor,
-            alacritty_terminal::ansi::NamedColor::DimBlack => colors.dim_black,
-            alacritty_terminal::ansi::NamedColor::DimRed => colors.dim_red,
-            alacritty_terminal::ansi::NamedColor::DimGreen => colors.dim_green,
-            alacritty_terminal::ansi::NamedColor::DimYellow => colors.dim_yellow,
-            alacritty_terminal::ansi::NamedColor::DimBlue => colors.dim_blue,
-            alacritty_terminal::ansi::NamedColor::DimMagenta => colors.dim_magenta,
-            alacritty_terminal::ansi::NamedColor::DimCyan => colors.dim_cyan,
-            alacritty_terminal::ansi::NamedColor::DimWhite => colors.dim_white,
-            alacritty_terminal::ansi::NamedColor::BrightForeground => colors.bright_foreground,
-            alacritty_terminal::ansi::NamedColor::DimForeground => colors.dim_foreground,
+            alacritty_terminal::ansi::NamedColor::Black => style.black,
+            alacritty_terminal::ansi::NamedColor::Red => style.red,
+            alacritty_terminal::ansi::NamedColor::Green => style.green,
+            alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
+            alacritty_terminal::ansi::NamedColor::Blue => style.blue,
+            alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
+            alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
+            alacritty_terminal::ansi::NamedColor::White => style.white,
+            alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
+            alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
+            alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
+            alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
+            alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
+            alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
+            alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
+            alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
+            alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
+            alacritty_terminal::ansi::NamedColor::Background => style.background,
+            alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
+            alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
+            alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
+            alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
+            alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
+            alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
+            alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
+            alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
+            alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
+            alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
+            alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
         },
         //'True' colors
         alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
         //8 bit, indexed colors
-        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), colors),
+        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
     }
 }
 
 ///Converts an 8 bit ANSI color to it's GPUI equivalent.
 ///Accepts usize for compatability with the alacritty::Colors interface,
 ///Other than that use case, should only be called with values in the [0,255] range
-pub fn get_color_at_index(index: &usize, colors: &TerminalColors) -> Color {
+pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
     match index {
         //0-15 are the same as the named colors above
-        0 => colors.black,
-        1 => colors.red,
-        2 => colors.green,
-        3 => colors.yellow,
-        4 => colors.blue,
-        5 => colors.magenta,
-        6 => colors.cyan,
-        7 => colors.white,
-        8 => colors.bright_black,
-        9 => colors.bright_red,
-        10 => colors.bright_green,
-        11 => colors.bright_yellow,
-        12 => colors.bright_blue,
-        13 => colors.bright_magenta,
-        14 => colors.bright_cyan,
-        15 => colors.bright_white,
+        0 => style.black,
+        1 => style.red,
+        2 => style.green,
+        3 => style.yellow,
+        4 => style.blue,
+        5 => style.magenta,
+        6 => style.cyan,
+        7 => style.white,
+        8 => style.bright_black,
+        9 => style.bright_red,
+        10 => style.bright_green,
+        11 => style.bright_yellow,
+        12 => style.bright_blue,
+        13 => style.bright_magenta,
+        14 => style.bright_cyan,
+        15 => style.bright_white,
         //16-231 are mapped to their RGB colors on a 0-5 range per channel
         16..=231 => {
             let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
@@ -85,19 +79,19 @@ pub fn get_color_at_index(index: &usize, colors: &TerminalColors) -> Color {
             Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
         }
         //For compatability with the alacritty::Colors interface
-        256 => colors.foreground,
-        257 => colors.background,
-        258 => colors.cursor,
-        259 => colors.dim_black,
-        260 => colors.dim_red,
-        261 => colors.dim_green,
-        262 => colors.dim_yellow,
-        263 => colors.dim_blue,
-        264 => colors.dim_magenta,
-        265 => colors.dim_cyan,
-        266 => colors.dim_white,
-        267 => colors.bright_foreground,
-        268 => colors.black, //'Dim Background', non-standard color
+        256 => style.foreground,
+        257 => style.background,
+        258 => style.cursor,
+        259 => style.dim_black,
+        260 => style.dim_red,
+        261 => style.dim_green,
+        262 => style.dim_yellow,
+        263 => style.dim_blue,
+        264 => style.dim_magenta,
+        265 => style.dim_cyan,
+        266 => style.dim_white,
+        267 => style.bright_foreground,
+        268 => style.black, //'Dim Background', non-standard color
         _ => Color::new(0, 0, 0, 255),
     }
 }

crates/terminal/src/terminal.rs πŸ”—

@@ -603,7 +603,7 @@ impl Terminal {
             InternalEvent::ColorRequest(index, format) => {
                 let color = term.colors()[*index].unwrap_or_else(|| {
                     let term_style = &cx.global::<Settings>().theme.terminal;
-                    to_alac_rgb(get_color_at_index(index, &term_style.colors))
+                    to_alac_rgb(get_color_at_index(index, &term_style))
                 });
                 self.write_to_pty(format(color))
             }

crates/terminal/src/terminal_container_view.rs πŸ”—

@@ -44,7 +44,6 @@ impl TerminalContainerContent {
 }
 
 pub struct TerminalContainer {
-    modal: bool,
     pub content: TerminalContainerContent,
     associated_directory: Option<PathBuf>,
 }
@@ -128,7 +127,6 @@ impl TerminalContainer {
         cx.focus(content.handle());
 
         TerminalContainer {
-            modal,
             content,
             associated_directory: working_directory,
         }
@@ -141,7 +139,6 @@ impl TerminalContainer {
     ) -> Self {
         let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
         TerminalContainer {
-            modal,
             content: TerminalContainerContent::Connected(connected_view),
             associated_directory: None,
         }
@@ -161,17 +158,11 @@ impl View for TerminalContainer {
     }
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        let child_view = match &self.content {
+        match &self.content {
             TerminalContainerContent::Connected(connected) => ChildView::new(connected, cx),
             TerminalContainerContent::Error(error) => ChildView::new(error, cx),
-        };
-        if self.modal {
-            let settings = cx.global::<Settings>();
-            let container_style = settings.theme.terminal.modal_container;
-            child_view.contained().with_style(container_style).boxed()
-        } else {
-            child_view.boxed()
         }
+        .boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -179,14 +170,6 @@ impl View for TerminalContainer {
             cx.focus(self.content.handle());
         }
     }
-
-    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
-        let mut context = Self::default_keymap_context();
-        if self.modal {
-            context.set.insert("ModalTerminal".into());
-        }
-        context
-    }
 }
 
 impl View for ErrorView {

crates/terminal/src/terminal_element.rs πŸ”—

@@ -152,7 +152,6 @@ impl LayoutRect {
 pub struct TerminalElement {
     terminal: WeakModelHandle<Terminal>,
     view: WeakViewHandle<TerminalView>,
-    modal: bool,
     focused: bool,
     cursor_visible: bool,
 }
@@ -161,14 +160,12 @@ impl TerminalElement {
     pub fn new(
         view: WeakViewHandle<TerminalView>,
         terminal: WeakModelHandle<Terminal>,
-        modal: bool,
         focused: bool,
         cursor_visible: bool,
     ) -> TerminalElement {
         TerminalElement {
             view,
             terminal,
-            modal,
             focused,
             cursor_visible,
         }
@@ -182,7 +179,6 @@ impl TerminalElement {
         terminal_theme: &TerminalStyle,
         text_layout_cache: &TextLayoutCache,
         font_cache: &FontCache,
-        modal: bool,
         hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
     ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
         let mut cells = vec![];
@@ -222,7 +218,7 @@ impl TerminalElement {
                                     cur_rect = Some(LayoutRect::new(
                                         Point::new(line_index as i32, cell.point.column.0 as i32),
                                         1,
-                                        convert_color(&bg, &terminal_theme.colors, modal),
+                                        convert_color(&bg, &terminal_theme),
                                     ));
                                 }
                             }
@@ -231,7 +227,7 @@ impl TerminalElement {
                                 cur_rect = Some(LayoutRect::new(
                                     Point::new(line_index as i32, cell.point.column.0 as i32),
                                     1,
-                                    convert_color(&bg, &terminal_theme.colors, modal),
+                                    convert_color(&bg, &terminal_theme),
                                 ));
                             }
                         }
@@ -248,7 +244,6 @@ impl TerminalElement {
                             terminal_theme,
                             text_style,
                             font_cache,
-                            modal,
                             hyperlink,
                         );
 
@@ -308,11 +303,10 @@ impl TerminalElement {
         style: &TerminalStyle,
         text_style: &TextStyle,
         font_cache: &FontCache,
-        modal: bool,
         hyperlink: Option<(HighlightStyle, &RangeInclusive<Point>)>,
     ) -> RunStyle {
         let flags = indexed.cell.flags;
-        let fg = convert_color(&fg, &style.colors, modal);
+        let fg = convert_color(&fg, &style);
 
         let mut underline = flags
             .intersects(Flags::ALL_UNDERLINES)
@@ -574,11 +568,7 @@ impl Element for TerminalElement {
             Default::default()
         };
 
-        let background_color = if self.modal {
-            terminal_theme.colors.modal_background
-        } else {
-            terminal_theme.colors.background
-        };
+        let background_color = terminal_theme.background;
         let terminal_handle = self.terminal.upgrade(cx).unwrap();
 
         let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| {
@@ -639,7 +629,6 @@ impl Element for TerminalElement {
             &terminal_theme,
             cx.text_layout_cache,
             cx.font_cache(),
-            self.modal,
             last_hovered_hyperlink
                 .as_ref()
                 .map(|(_, range, _)| (link_style, range)),
@@ -655,9 +644,9 @@ impl Element for TerminalElement {
                 let str_trxt = cursor_char.to_string();
 
                 let color = if self.focused {
-                    terminal_theme.colors.background
+                    terminal_theme.background
                 } else {
-                    terminal_theme.colors.foreground
+                    terminal_theme.foreground
                 };
 
                 cx.text_layout_cache.layout_str(
@@ -691,7 +680,7 @@ impl Element for TerminalElement {
                         cursor_position,
                         block_width,
                         dimensions.line_height,
-                        terminal_theme.colors.cursor,
+                        terminal_theme.cursor,
                         shape,
                         text,
                     )

crates/theme/src/theme.rs πŸ”—

@@ -2,7 +2,7 @@ mod theme_registry;
 
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, ImageStyle, LabelStyle, TooltipStyle},
+    elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
     fonts::{HighlightStyle, TextStyle},
     Border, MouseState,
 };
@@ -18,7 +18,6 @@ pub struct Theme {
     pub meta: ThemeMeta,
     pub workspace: Workspace,
     pub context_menu: ContextMenu,
-    pub chat_panel: ChatPanel,
     pub contacts_popover: ContactsPopover,
     pub contact_list: ContactList,
     pub contact_finder: ContactFinder,
@@ -35,6 +34,7 @@ pub struct Theme {
     pub incoming_call_notification: IncomingCallNotification,
     pub tooltip: TooltipStyle,
     pub terminal: TerminalStyle,
+    pub color_scheme: ColorScheme,
 }
 
 #[derive(Deserialize, Default, Clone)]
@@ -319,18 +319,6 @@ pub struct SidebarItem {
     pub icon_size: f32,
 }
 
-#[derive(Deserialize, Default)]
-pub struct ChatPanel {
-    #[serde(flatten)]
-    pub container: ContainerStyle,
-    pub message: ChatMessage,
-    pub pending_message: ChatMessage,
-    pub channel_select: ChannelSelect,
-    pub input_editor: FieldEditor,
-    pub sign_in_prompt: TextStyle,
-    pub hovered_sign_in_prompt: TextStyle,
-}
-
 #[derive(Deserialize, Default)]
 pub struct ProjectPanel {
     #[serde(flatten)]
@@ -773,12 +761,6 @@ pub struct HoverPopover {
 
 #[derive(Clone, Deserialize, Default)]
 pub struct TerminalStyle {
-    pub colors: TerminalColors,
-    pub modal_container: ContainerStyle,
-}
-
-#[derive(Clone, Deserialize, Default)]
-pub struct TerminalColors {
     pub black: Color,
     pub red: Color,
     pub green: Color,
@@ -810,3 +792,67 @@ pub struct TerminalColors {
     pub bright_foreground: Color,
     pub dim_foreground: Color,
 }
+
+#[derive(Clone, Deserialize, Default)]
+pub struct ColorScheme {
+    pub name: String,
+    pub is_light: bool,
+
+    pub ramps: RampSet,
+
+    pub lowest: Layer,
+    pub middle: Layer,
+    pub highest: Layer,
+
+    pub popover_shadow: Shadow,
+    pub modal_shadow: Shadow,
+
+    pub players: Vec<Player>,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Player {
+    pub cursor: Color,
+    pub selection: Color,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct RampSet {
+    pub neutral: Vec<Color>,
+    pub red: Vec<Color>,
+    pub orange: Vec<Color>,
+    pub yellow: Vec<Color>,
+    pub green: Vec<Color>,
+    pub cyan: Vec<Color>,
+    pub blue: Vec<Color>,
+    pub violet: Vec<Color>,
+    pub magenta: Vec<Color>,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Layer {
+    pub base: StyleSet,
+    pub variant: StyleSet,
+    pub on: StyleSet,
+    pub accent: StyleSet,
+    pub positive: StyleSet,
+    pub warning: StyleSet,
+    pub negative: StyleSet,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct StyleSet {
+    pub default: Style,
+    pub active: Style,
+    pub disabled: Style,
+    pub hovered: Style,
+    pub pressed: Style,
+    pub inverted: Style,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Style {
+    pub background: Color,
+    pub border: Color,
+    pub foreground: Color,
+}

crates/theme_testbench/Cargo.toml πŸ”—

@@ -0,0 +1,18 @@
+[package]
+name = "theme_testbench"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/theme_testbench.rs"
+doctest = false
+
+
+[dependencies]
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+settings = { path = "../settings" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }
+
+smallvec = { version = "1.6", features = ["union"] }

crates/theme_testbench/src/theme_testbench.rs πŸ”—

@@ -0,0 +1,357 @@
+use gpui::{
+    actions,
+    color::Color,
+    elements::{
+        Canvas, Container, ContainerStyle, ElementBox, Flex, Label, Margin, MouseEventHandler,
+        Padding, ParentElement,
+    },
+    fonts::TextStyle,
+    Border, Element, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
+};
+use project::{Project, ProjectEntryId, ProjectPath};
+use settings::Settings;
+use smallvec::SmallVec;
+use theme::{ColorScheme, Layer, Style, StyleSet};
+use workspace::{Item, Workspace};
+
+actions!(theme, [DeployThemeTestbench]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(ThemeTestbench::deploy);
+}
+
+pub struct ThemeTestbench {}
+
+impl ThemeTestbench {
+    pub fn deploy(
+        workspace: &mut Workspace,
+        _: &DeployThemeTestbench,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let view = cx.add_view(|_| ThemeTestbench {});
+        workspace.add_item(Box::new(view), cx);
+    }
+
+    fn render_ramps(color_scheme: &ColorScheme) -> Flex {
+        fn display_ramp(ramp: &Vec<Color>) -> ElementBox {
+            Flex::row()
+                .with_children(ramp.iter().cloned().map(|color| {
+                    Canvas::new(move |bounds, _, cx| {
+                        cx.scene.push_quad(Quad {
+                            bounds,
+                            background: Some(color),
+                            ..Default::default()
+                        });
+                    })
+                    .flex(1.0, false)
+                    .boxed()
+                }))
+                .flex(1.0, false)
+                .boxed()
+        }
+
+        Flex::column()
+            .with_child(display_ramp(&color_scheme.ramps.neutral))
+            .with_child(display_ramp(&color_scheme.ramps.red))
+            .with_child(display_ramp(&color_scheme.ramps.orange))
+            .with_child(display_ramp(&color_scheme.ramps.yellow))
+            .with_child(display_ramp(&color_scheme.ramps.green))
+            .with_child(display_ramp(&color_scheme.ramps.cyan))
+            .with_child(display_ramp(&color_scheme.ramps.blue))
+            .with_child(display_ramp(&color_scheme.ramps.violet))
+            .with_child(display_ramp(&color_scheme.ramps.magenta))
+    }
+
+    fn render_layer(
+        layer_index: usize,
+        layer: &Layer,
+        cx: &mut RenderContext<'_, Self>,
+    ) -> Container {
+        Flex::column()
+            .with_child(
+                Self::render_button_set(0, layer_index, "base", &layer.base, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(2, layer_index, "on", &layer.on, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .with_child(
+                Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .contained()
+            .with_style(ContainerStyle {
+                margin: Margin {
+                    top: 10.,
+                    bottom: 10.,
+                    left: 10.,
+                    right: 10.,
+                },
+                background_color: Some(layer.base.default.background),
+                ..Default::default()
+            })
+    }
+
+    fn render_button_set(
+        set_index: usize,
+        layer_index: usize,
+        set_name: &'static str,
+        style_set: &StyleSet,
+        cx: &mut RenderContext<'_, Self>,
+    ) -> Flex {
+        Flex::row()
+            .with_child(Self::render_button(
+                set_index * 6,
+                layer_index,
+                set_name,
+                &style_set,
+                None,
+                cx,
+            ))
+            .with_child(Self::render_button(
+                set_index * 6 + 1,
+                layer_index,
+                "hovered",
+                &style_set,
+                Some(|style_set| &style_set.hovered),
+                cx,
+            ))
+            .with_child(Self::render_button(
+                set_index * 6 + 2,
+                layer_index,
+                "pressed",
+                &style_set,
+                Some(|style_set| &style_set.pressed),
+                cx,
+            ))
+            .with_child(Self::render_button(
+                set_index * 6 + 3,
+                layer_index,
+                "active",
+                &style_set,
+                Some(|style_set| &style_set.active),
+                cx,
+            ))
+            .with_child(Self::render_button(
+                set_index * 6 + 4,
+                layer_index,
+                "disabled",
+                &style_set,
+                Some(|style_set| &style_set.disabled),
+                cx,
+            ))
+            .with_child(Self::render_button(
+                set_index * 6 + 5,
+                layer_index,
+                "inverted",
+                &style_set,
+                Some(|style_set| &style_set.inverted),
+                cx,
+            ))
+    }
+
+    fn render_button(
+        button_index: usize,
+        layer_index: usize,
+        text: &'static str,
+        style_set: &StyleSet,
+        style_override: Option<fn(&StyleSet) -> &Style>,
+        cx: &mut RenderContext<'_, Self>,
+    ) -> ElementBox {
+        enum TestBenchButton {}
+        MouseEventHandler::<TestBenchButton>::new(layer_index + button_index, cx, |state, cx| {
+            let style = if let Some(style_override) = style_override {
+                style_override(&style_set)
+            } else if state.clicked().is_some() {
+                &style_set.pressed
+            } else if state.hovered() {
+                &style_set.hovered
+            } else {
+                &style_set.default
+            };
+
+            Self::render_label(text.to_string(), style, cx)
+                .contained()
+                .with_style(ContainerStyle {
+                    margin: Margin {
+                        top: 4.,
+                        bottom: 4.,
+                        left: 4.,
+                        right: 4.,
+                    },
+                    padding: Padding {
+                        top: 4.,
+                        bottom: 4.,
+                        left: 4.,
+                        right: 4.,
+                    },
+                    background_color: Some(style.background),
+                    border: Border {
+                        width: 1.,
+                        color: style.border,
+                        overlay: false,
+                        top: true,
+                        bottom: true,
+                        left: true,
+                        right: true,
+                    },
+                    corner_radius: 2.,
+                    ..Default::default()
+                })
+                .boxed()
+        })
+        .flex(1., true)
+        .boxed()
+    }
+
+    fn render_label(text: String, style: &Style, cx: &mut RenderContext<'_, Self>) -> Label {
+        let settings = cx.global::<Settings>();
+        let font_cache = cx.font_cache();
+        let family_id = settings.buffer_font_family;
+        let font_size = settings.buffer_font_size;
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text_style = TextStyle {
+            color: style.foreground,
+            font_family_id: family_id,
+            font_family_name: font_cache.family_name(family_id).unwrap(),
+            font_id,
+            font_size,
+            font_properties: Default::default(),
+            underline: Default::default(),
+        };
+
+        Label::new(text, text_style)
+    }
+}
+
+impl Entity for ThemeTestbench {
+    type Event = ();
+}
+
+impl View for ThemeTestbench {
+    fn ui_name() -> &'static str {
+        "ThemeTestbench"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        let color_scheme = &cx.global::<Settings>().theme.clone().color_scheme;
+
+        Flex::row()
+            .with_child(
+                Self::render_ramps(color_scheme)
+                    .contained()
+                    .with_margin_right(10.)
+                    .flex(0.1, false)
+                    .boxed(),
+            )
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Self::render_layer(100, &color_scheme.lowest, cx)
+                            .flex(1., true)
+                            .boxed(),
+                    )
+                    .with_child(
+                        Self::render_layer(200, &color_scheme.middle, cx)
+                            .flex(1., true)
+                            .boxed(),
+                    )
+                    .with_child(
+                        Self::render_layer(300, &color_scheme.highest, cx)
+                            .flex(1., true)
+                            .boxed(),
+                    )
+                    .flex(1., false)
+                    .boxed(),
+            )
+            .boxed()
+    }
+}
+
+impl Item for ThemeTestbench {
+    fn tab_content(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        _: &gpui::AppContext,
+    ) -> gpui::ElementBox {
+        Label::new("Theme Testbench".into(), style.label.clone())
+            .aligned()
+            .contained()
+            .boxed()
+    }
+
+    fn project_path(&self, _: &gpui::AppContext) -> Option<ProjectPath> {
+        None
+    }
+
+    fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[ProjectEntryId; 3]> {
+        SmallVec::new()
+    }
+
+    fn is_singleton(&self, _: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
+
+    fn can_save(&self, _: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _: gpui::ModelHandle<Project>,
+        _: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _: gpui::ModelHandle<Project>,
+        _: std::path::PathBuf,
+        _: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _: gpui::ModelHandle<Project>,
+        _: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+
+    fn to_item_events(_: &Self::Event) -> Vec<workspace::ItemEvent> {
+        Vec::new()
+    }
+}

crates/zed/Cargo.toml πŸ”—

@@ -20,7 +20,6 @@ assets = { path = "../assets" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 call = { path = "../call" }
-chat_panel = { path = "../chat_panel" }
 cli = { path = "../cli" }
 collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }
@@ -52,6 +51,7 @@ text = { path = "../text" }
 terminal = { path = "../terminal" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
+theme_testbench = { path = "../theme_testbench" }
 util = { path = "../util" }
 vim = { path = "../vim" }
 workspace = { path = "../workspace" }

crates/zed/build.rs πŸ”—

@@ -35,12 +35,12 @@ fn main() {
 
     let output = Command::new("npm")
         .current_dir("../../styles")
-        .args(["run", "build-themes"])
+        .args(["run", "build"])
         .output()
         .expect("failed to run npm");
     if !output.status.success() {
         panic!(
-            "build-themes script failed {}",
+            "build script failed {}",
             String::from_utf8_lossy(&output.stderr)
         );
     }

crates/zed/src/main.rs πŸ”—

@@ -116,7 +116,6 @@ fn main() {
         editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
-        chat_panel::init(cx);
         outline::init(cx);
         project_symbols::init(cx);
         project_panel::init(cx);
@@ -124,6 +123,7 @@ fn main() {
         search::init(cx);
         vim::init(cx);
         terminal::init(cx);
+        theme_testbench::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
             .detach();
@@ -441,7 +441,7 @@ async fn watch_themes(
     while (events.next().await).is_some() {
         let output = Command::new("npm")
             .current_dir("styles")
-            .args(["run", "build-themes"])
+            .args(["run", "build"])
             .output()
             .await
             .log_err()?;
@@ -449,7 +449,7 @@ async fn watch_themes(
             cx.update(|cx| theme_selector::ThemeSelector::reload(themes.clone(), cx))
         } else {
             eprintln!(
-                "build-themes script failed {}",
+                "build script failed {}",
                 String::from_utf8_lossy(&output.stderr)
             );
         }

styles/package.json πŸ”—

@@ -1,19 +1,18 @@
 {
-  "name": "styles",
-  "version": "1.0.0",
-  "description": "",
-  "main": "index.js",
-  "scripts": {
-    "build": "npm run build-themes && npm run build-tokens",
-    "build-themes": "ts-node ./src/buildThemes.ts"
-  },
-  "author": "",
-  "license": "ISC",
-  "dependencies": {
-    "@types/chroma-js": "^2.1.3",
-    "@types/node": "^17.0.23",
-    "case-anything": "^2.1.10",
-    "chroma-js": "^2.4.2",
-    "ts-node": "^10.7.0"
-  }
+    "name": "styles",
+    "version": "1.0.0",
+    "description": "",
+    "main": "index.js",
+    "scripts": {
+        "build": "ts-node ./src/buildThemes.ts"
+    },
+    "author": "",
+    "license": "ISC",
+    "dependencies": {
+        "@types/chroma-js": "^2.1.3",
+        "@types/node": "^17.0.23",
+        "case-anything": "^2.1.10",
+        "chroma-js": "^2.4.2",
+        "ts-node": "^10.7.0"
+    }
 }

styles/src/buildThemes.ts πŸ”—

@@ -2,9 +2,12 @@ import * as fs from "fs";
 import * as path from "path";
 import { tmpdir } from "os";
 import app from "./styleTree/app";
-import themes, { internalThemes, experimentalThemes } from "./themes";
+import colorSchemes, {
+  internalColorSchemes,
+  experimentalColorSchemes,
+} from "./colorSchemes";
 import snakeCase from "./utils/snakeCase";
-import Theme from "./themes/common/theme";
+import { ColorScheme } from "./themes/common/colorScheme";
 
 const themeDirectory = `${__dirname}/../../assets/themes`;
 const internalDirectory = `${themeDirectory}/internal`;
@@ -16,7 +19,7 @@ function clearThemes(themeDirectory: string) {
   for (const file of fs.readdirSync(themeDirectory)) {
     if (file.endsWith(".json")) {
       const name = file.replace(/\.json$/, "");
-      if (!themes.find((theme) => theme.name === name)) {
+      if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
         fs.unlinkSync(path.join(themeDirectory, file));
       }
     }
@@ -27,12 +30,12 @@ clearThemes(themeDirectory);
 clearThemes(internalDirectory);
 clearThemes(experimentsDirectory);
 
-function writeThemes(themes: Theme[], outputDirectory: string) {
-  for (let theme of themes) {
-    let styleTree = snakeCase(app(theme));
+function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
+  for (let colorScheme of colorSchemes) {
+    let styleTree = snakeCase(app(colorScheme));
     let styleTreeJSON = JSON.stringify(styleTree, null, 2);
-    let tempPath = path.join(tempDirectory, `${theme.name}.json`);
-    let outPath = path.join(outputDirectory, `${theme.name}.json`);
+    let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`);
+    let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
     fs.writeFileSync(tempPath, styleTreeJSON);
     fs.renameSync(tempPath, outPath);
     console.log(`- ${outPath} created`);
@@ -40,6 +43,6 @@ function writeThemes(themes: Theme[], outputDirectory: string) {
 }
 
 // Write new themes to theme directory
-writeThemes(themes, themeDirectory);
-writeThemes(internalThemes, internalDirectory);
-writeThemes(experimentalThemes, experimentsDirectory);
+writeThemes(colorSchemes, themeDirectory);
+writeThemes(internalColorSchemes, internalDirectory);
+writeThemes(experimentalColorSchemes, experimentsDirectory);

styles/src/colorSchemes.ts πŸ”—

@@ -0,0 +1,35 @@
+import fs from "fs";
+import path from "path";
+import { ColorScheme } from "./themes/common/colorScheme";
+
+const colorSchemes: ColorScheme[] = [];
+export default colorSchemes;
+
+const internalColorSchemes: ColorScheme[] = [];
+export { internalColorSchemes };
+
+const experimentalColorSchemes: ColorScheme[] = [];
+export { experimentalColorSchemes };
+
+function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
+  for (const fileName of fs.readdirSync(themesPath)) {
+    if (fileName == "template.ts") continue;
+    const filePath = path.join(themesPath, fileName);
+
+    if (fs.statSync(filePath).isFile()) {
+      const colorScheme = require(filePath);
+      if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
+      if (colorScheme.light) colorSchemes.push(colorScheme.light);
+    }
+  }
+}
+
+fillColorSchemes(path.resolve(`${__dirname}/themes`), colorSchemes);
+fillColorSchemes(
+  path.resolve(`${__dirname}/themes/internal`),
+  internalColorSchemes
+);
+fillColorSchemes(
+  path.resolve(`${__dirname}/themes/experiments`),
+  experimentalColorSchemes
+);

styles/src/styleTree/app.ts πŸ”—

@@ -1,5 +1,3 @@
-import Theme from "../themes/common/theme";
-import chatPanel from "./chatPanel";
 import { text } from "./components";
 import contactFinder from "./contactFinder";
 import contactsPopover from "./contactsPopover";
@@ -18,40 +16,51 @@ import tooltip from "./tooltip";
 import terminal from "./terminal";
 import contactList from "./contactList";
 import incomingCallNotification from "./incomingCallNotification";
+import { ColorScheme } from "../themes/common/colorScheme";
 
-export const panel = {
-  padding: { top: 12, bottom: 12 },
-};
-
-export default function app(theme: Theme): Object {
+export default function app(colorScheme: ColorScheme): Object {
   return {
     meta: {
-      name: theme.name,
-      isLight: theme.isLight
+      name: colorScheme.name,
+      isLight: colorScheme.isLight,
     },
-    picker: picker(theme),
-    workspace: workspace(theme),
-    contextMenu: contextMenu(theme),
-    editor: editor(theme),
-    projectDiagnostics: projectDiagnostics(theme),
-    commandPalette: commandPalette(theme),
-    projectPanel: projectPanel(theme),
-    chatPanel: chatPanel(theme),
-    contactsPopover: contactsPopover(theme),
-    contactList: contactList(theme),
-    contactFinder: contactFinder(theme),
-    search: search(theme),
+    commandPalette: commandPalette(colorScheme),
+    contactNotification: contactNotification(colorScheme),
+    projectSharedNotification: projectSharedNotification(colorScheme),
+    incomingCallNotification: incomingCallNotification(colorScheme),
+    picker: picker(colorScheme),
+    workspace: workspace(colorScheme),
+    contextMenu: contextMenu(colorScheme),
+    editor: editor(colorScheme),
+    projectDiagnostics: projectDiagnostics(colorScheme),
+    projectPanel: projectPanel(colorScheme),
+    contactsPopover: contactsPopover(colorScheme),
+    contactFinder: contactFinder(colorScheme),
+    contactList: contactList(colorScheme),
+    search: search(colorScheme),
     breadcrumbs: {
-      ...text(theme, "sans", "secondary"),
+      ...text(colorScheme.highest, "sans", "variant"),
       padding: {
         left: 6,
       },
     },
-    contactNotification: contactNotification(theme),
-    updateNotification: updateNotification(theme),
-    projectSharedNotification: projectSharedNotification(theme),
-    incomingCallNotification: incomingCallNotification(theme),
-    tooltip: tooltip(theme),
-    terminal: terminal(theme),
+    updateNotification: updateNotification(colorScheme),
+    tooltip: tooltip(colorScheme),
+    terminal: terminal(colorScheme),
+    colorScheme: {
+      ...colorScheme,
+      players: Object.values(colorScheme.players),
+      ramps: {
+        neutral: colorScheme.ramps.neutral.colors(100, "hex"),
+        red: colorScheme.ramps.red.colors(100, "hex"),
+        orange: colorScheme.ramps.orange.colors(100, "hex"),
+        yellow: colorScheme.ramps.yellow.colors(100, "hex"),
+        green: colorScheme.ramps.green.colors(100, "hex"),
+        cyan: colorScheme.ramps.cyan.colors(100, "hex"),
+        blue: colorScheme.ramps.blue.colors(100, "hex"),
+        violet: colorScheme.ramps.violet.colors(100, "hex"),
+        magenta: colorScheme.ramps.magenta.colors(100, "hex"),
+      },
+    },
   };
 }

styles/src/styleTree/chatPanel.ts πŸ”—

@@ -1,108 +0,0 @@
-import Theme from "../themes/common/theme";
-import { panel } from "./app";
-import {
-  backgroundColor,
-  border,
-  player,
-  text,
-  TextColor,
-  popoverShadow,
-} from "./components";
-
-export default function chatPanel(theme: Theme) {
-  function channelSelectItem(
-    theme: Theme,
-    textColor: TextColor,
-    hovered: boolean
-  ) {
-    return {
-      name: text(theme, "sans", textColor),
-      padding: 4,
-      hash: {
-        ...text(theme, "sans", "muted"),
-        margin: {
-          right: 8,
-        },
-      },
-      background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
-      cornerRadius: hovered ? 6 : 0,
-    };
-  }
-
-  const message = {
-    body: text(theme, "sans", "secondary"),
-    timestamp: text(theme, "sans", "muted", { size: "sm" }),
-    padding: {
-      bottom: 6,
-    },
-    sender: {
-      ...text(theme, "sans", "primary", { weight: "bold" }),
-      margin: {
-        right: 8,
-      },
-    },
-  };
-
-  return {
-    ...panel,
-    channelName: text(theme, "sans", "primary", { weight: "bold" }),
-    channelNameHash: {
-      ...text(theme, "sans", "muted"),
-      padding: {
-        right: 8,
-      },
-    },
-    channelSelect: {
-      header: {
-        ...channelSelectItem(theme, "primary", false),
-        padding: {
-          bottom: 4,
-          left: 0,
-        },
-      },
-      item: channelSelectItem(theme, "secondary", false),
-      hoveredItem: channelSelectItem(theme, "secondary", true),
-      activeItem: channelSelectItem(theme, "primary", false),
-      hoveredActiveItem: channelSelectItem(theme, "primary", true),
-      menu: {
-        background: backgroundColor(theme, 500),
-        cornerRadius: 6,
-        padding: 4,
-        border: border(theme, "primary"),
-        shadow: popoverShadow(theme),
-      },
-    },
-    signInPrompt: text(theme, "sans", "secondary", { underline: true }),
-    hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
-    message,
-    pendingMessage: {
-      ...message,
-      body: {
-        ...message.body,
-        color: theme.textColor.muted,
-      },
-      sender: {
-        ...message.sender,
-        color: theme.textColor.muted,
-      },
-      timestamp: {
-        ...message.timestamp,
-        color: theme.textColor.muted,
-      },
-    },
-    inputEditor: {
-      background: backgroundColor(theme, 500),
-      cornerRadius: 6,
-      text: text(theme, "mono", "primary"),
-      placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
-      selection: player(theme, 1).selection,
-      border: border(theme, "secondary"),
-      padding: {
-        bottom: 7,
-        left: 8,
-        right: 8,
-        top: 7,
-      },
-    },
-  };
-}

styles/src/styleTree/commandPalette.ts πŸ”—

@@ -1,25 +1,29 @@
-import Theme from "../themes/common/theme";
-import { text, backgroundColor, border } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { withOpacity } from "../utils/color";
+import { text, background } from "./components";
 
-export default function commandPalette(theme: Theme) {
+export default function commandPalette(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
   return {
     keystrokeSpacing: 8,
     key: {
-      text: text(theme, "mono", "secondary", { size: "xs" }),
-      cornerRadius: 4,
-      background: backgroundColor(theme, "on300"),
-      border: border(theme, "secondary"),
+      text: text(layer, "mono", "variant", "default", { size: "xs" }),
+      cornerRadius: 2,
+      background: background(layer, "on"),
       padding: {
-        top: 2,
-        bottom: 2,
-        left: 8,
-        right: 8,
+        top: 1,
+        bottom: 1,
+        left: 6,
+        right: 6,
       },
       margin: {
+        top: 1,
+        bottom: 1,
         left: 2,
       },
       active: {
-        text: text(theme, "mono", "active", { size: "xs" }),
+        text: text(layer, "mono", "on", "default", { size: "xs" }),
+        background: withOpacity(background(layer, "on"), 0.2),
       },
     },
   };

styles/src/styleTree/components.ts πŸ”—

@@ -1,104 +1,200 @@
-import Theme, { BackgroundColorSet } from "../themes/common/theme";
 import { fontFamilies, fontSizes, FontWeight } from "../common";
+import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
+
+function isStyleSet(key: any): key is StyleSets {
+  return [
+    "base",
+    "variant",
+    "on",
+    "accent",
+    "positive",
+    "warning",
+    "negative",
+  ].includes(key);
+}
+function isStyle(key: any): key is Styles {
+  return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key);
+}
+function getStyle(
+  layer: Layer,
+  possibleStyleSetOrStyle?: any,
+  possibleStyle?: any
+): Style {
+  let styleSet: StyleSets = "base";
+  let style: Styles = "default";
+  if (isStyleSet(possibleStyleSetOrStyle)) {
+    styleSet = possibleStyleSetOrStyle;
+  } else if (isStyle(possibleStyleSetOrStyle)) {
+    style = possibleStyleSetOrStyle;
+  }
+
+  if (isStyle(possibleStyle)) {
+    style = possibleStyle;
+  }
+
+  return layer[styleSet][style];
+}
+
+export function background(layer: Layer, style?: Styles): string;
+export function background(
+  layer: Layer,
+  styleSet?: StyleSets,
+  style?: Styles
+): string;
+export function background(
+  layer: Layer,
+  styleSetOrStyles?: StyleSets | Styles,
+  style?: Styles
+): string {
+  return getStyle(layer, styleSetOrStyles, style).background;
+}
+
+export function borderColor(layer: Layer, style?: Styles): string;
+export function borderColor(
+  layer: Layer,
+  styleSet?: StyleSets,
+  style?: Styles
+): string;
+export function borderColor(
+  layer: Layer,
+  styleSetOrStyles?: StyleSets | Styles,
+  style?: Styles
+): string {
+  return getStyle(layer, styleSetOrStyles, style).border;
+}
+
+export function foreground(layer: Layer, style?: Styles): string;
+export function foreground(
+  layer: Layer,
+  styleSet?: StyleSets,
+  style?: Styles
+): string;
+export function foreground(
+  layer: Layer,
+  styleSetOrStyles?: StyleSets | Styles,
+  style?: Styles
+): string {
+  return getStyle(layer, styleSetOrStyles, style).foreground;
+}
+
+interface Text {
+  family: keyof typeof fontFamilies;
+  color: string;
+  size: number;
+  weight?: FontWeight;
+  underline?: boolean;
+}
+
+interface TextProperties {
+  size?: keyof typeof fontSizes;
+  weight?: FontWeight;
+  underline?: boolean;
+}
 
-export type TextColor = keyof Theme["textColor"];
 export function text(
-  theme: Theme,
+  layer: Layer,
   fontFamily: keyof typeof fontFamilies,
-  color: TextColor,
-  properties?: {
-    size?: keyof typeof fontSizes;
-    weight?: FontWeight;
-    underline?: boolean;
-  }
+  styleSet: StyleSets,
+  style: Styles,
+  properties?: TextProperties
+): Text;
+export function text(
+  layer: Layer,
+  fontFamily: keyof typeof fontFamilies,
+  styleSet: StyleSets,
+  properties?: TextProperties
+): Text;
+export function text(
+  layer: Layer,
+  fontFamily: keyof typeof fontFamilies,
+  style: Styles,
+  properties?: TextProperties
+): Text;
+export function text(
+  layer: Layer,
+  fontFamily: keyof typeof fontFamilies,
+  properties?: TextProperties
+): Text;
+export function text(
+  layer: Layer,
+  fontFamily: keyof typeof fontFamilies,
+  styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
+  styleOrProperties?: Styles | TextProperties,
+  properties?: TextProperties
 ) {
+  let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
+
+  if (typeof styleSetStyleOrProperties === "object") {
+    properties = styleSetStyleOrProperties;
+  }
+  if (typeof styleOrProperties === "object") {
+    properties = styleOrProperties;
+  }
+
   let size = fontSizes[properties?.size || "sm"];
+
   return {
     family: fontFamilies[fontFamily],
-    color: theme.textColor[color],
+    color: style.foreground,
     ...properties,
     size,
   };
 }
-export function textColor(theme: Theme, color: TextColor) {
-  return theme.textColor[color];
-}
 
-export type BorderColor = keyof Theme["borderColor"];
-export interface BorderOptions {
-  width?: number;
+export interface Border {
+  color: string;
+  width: number;
   top?: boolean;
   bottom?: boolean;
   left?: boolean;
   right?: boolean;
   overlay?: boolean;
 }
-export function border(
-  theme: Theme,
-  color: BorderColor,
-  options?: BorderOptions
-) {
-  return {
-    color: borderColor(theme, color),
-    width: 1,
-    ...options,
-  };
-}
-export function borderColor(theme: Theme, color: BorderColor) {
-  return theme.borderColor[color];
-}
-
-export type IconColor = keyof Theme["iconColor"];
-export function iconColor(theme: Theme, color: IconColor) {
-  return theme.iconColor[color];
-}
 
-export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
-export interface Player {
-  selection: {
-    cursor: string;
-    selection: string;
-  };
-}
-export function player(theme: Theme, playerNumber: PlayerIndex): Player {
-  return {
-    selection: {
-      cursor: theme.player[playerNumber].cursorColor,
-      selection: theme.player[playerNumber].selectionColor,
-    },
-  };
-}
-
-export type BackgroundColor = keyof Theme["backgroundColor"];
-export type BackgroundState = keyof BackgroundColorSet;
-export function backgroundColor(
-  theme: Theme,
-  name: BackgroundColor,
-  state?: BackgroundState
-): string {
-  return theme.backgroundColor[name][state || "base"];
+export interface BorderProperties {
+  width?: number;
+  top?: boolean;
+  bottom?: boolean;
+  left?: boolean;
+  right?: boolean;
+  overlay?: boolean;
 }
 
-export function modalShadow(theme: Theme) {
-  return {
-    blur: 16,
-    color: theme.shadow,
-    offset: [0, 2],
-  };
-}
+export function border(
+  layer: Layer,
+  styleSet: StyleSets,
+  style: Styles,
+  properties?: BorderProperties
+): Border;
+export function border(
+  layer: Layer,
+  styleSet: StyleSets,
+  properties?: BorderProperties
+): Border;
+export function border(
+  layer: Layer,
+  style: Styles,
+  properties?: BorderProperties
+): Border;
+export function border(layer: Layer, properties?: BorderProperties): Border;
+export function border(
+  layer: Layer,
+  styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
+  styleOrProperties?: Styles | BorderProperties,
+  properties?: BorderProperties
+): Border {
+  let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
 
-export function popoverShadow(theme: Theme) {
-  return {
-    blur: 4,
-    color: theme.shadow,
-    offset: [1, 2],
-  };
-}
+  if (typeof styleSetStyleOrProperties === "object") {
+    properties = styleSetStyleOrProperties;
+  }
+  if (typeof styleOrProperties === "object") {
+    properties = styleOrProperties;
+  }
 
-export function draggedShadow(theme: Theme) {
   return {
-    blur: 6,
-    color: theme.shadow,
-    offset: [1, 2],
+    color: style.border,
+    width: 1,
+    ...properties,
   };
 }

styles/src/styleTree/contactFinder.ts πŸ”—

@@ -1,12 +1,14 @@
-import Theme from "../themes/common/theme";
 import picker from "./picker";
-import { backgroundColor, border, iconColor, player, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, foreground, text } from "./components";
+
+export default function contactFinder(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
 
-export default function contactFinder(theme: Theme) {
   const sideMargin = 6;
   const contactButton = {
-    background: backgroundColor(theme, 100),
-    color: iconColor(theme, "primary"),
+    background: background(layer, "variant"),
+    color: foreground(layer, "variant"),
     iconWidth: 8,
     buttonWidth: 16,
     cornerRadius: 8,
@@ -15,17 +17,17 @@ export default function contactFinder(theme: Theme) {
   return {
     picker: {
       item: {
-        ...picker(theme).item,
+        ...picker(colorScheme).item,
         margin: { left: sideMargin, right: sideMargin }
       },
-      empty: picker(theme).empty,
+      empty: picker(colorScheme).empty,
       inputEditor: {
-        background: backgroundColor(theme, 500),
+        background: background(layer, "on"),
         cornerRadius: 6,
-        text: text(theme, "mono", "primary"),
-        placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
-        selection: player(theme, 1).selection,
-        border: border(theme, "secondary"),
+        text: text(layer, "mono",),
+        placeholderText: text(layer, "mono", "variant", { size: "sm" }),
+        selection: colorScheme.players[0],
+        border: border(layer),
         padding: {
           bottom: 4,
           left: 8,
@@ -51,13 +53,13 @@ export default function contactFinder(theme: Theme) {
     contactButton: {
       ...contactButton,
       hover: {
-        background: backgroundColor(theme, 100, "hovered"),
+        background: background(layer, "variant", "hovered"),
       },
     },
     disabledContactButton: {
       ...contactButton,
-      background: backgroundColor(theme, 100),
-      color: iconColor(theme, "muted"),
+      background: background(layer, "disabled"),
+      color: foreground(layer, "disabled"),
     },
   };
 }

styles/src/styleTree/contactList.ts πŸ”—

@@ -1,13 +1,21 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, borderColor, iconColor, player, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import {
+  background,
+  border,
+  borderColor,
+  foreground,
+  text,
+} from "./components";
 
-export default function contactList(theme: Theme) {
+export default function contactsPanel(colorScheme: ColorScheme) {
   const nameMargin = 8;
   const sidePadding = 12;
 
+  let layer = colorScheme.middle;
+
   const contactButton = {
-    background: backgroundColor(theme, 100),
-    color: iconColor(theme, "primary"),
+    background: background(layer, "on"),
+    color: foreground(layer, "on"),
     iconWidth: 8,
     buttonWidth: 16,
     cornerRadius: 8,
@@ -20,7 +28,7 @@ export default function contactList(theme: Theme) {
       width: 14,
     },
     name: {
-      ...text(theme, "mono", "placeholder", { size: "sm" }),
+      ...text(layer, "mono", { size: "sm" }),
       margin: {
         left: nameMargin,
         right: 6,
@@ -39,13 +47,15 @@ export default function contactList(theme: Theme) {
   };
 
   return {
+    background: background(layer),
+    padding: { top: 12, bottom: 0 },
     userQueryEditor: {
-      background: backgroundColor(theme, 500),
+      background: background(layer, "on"),
       cornerRadius: 6,
-      text: text(theme, "mono", "primary"),
-      placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
-      selection: player(theme, 1).selection,
-      border: border(theme, "secondary"),
+      text: text(layer, "mono", "on"),
+      placeholderText: text(layer, "mono", "on", "disabled", { size: "sm" }),
+      selection: colorScheme.players[0],
+      border: border(layer, "on"),
       padding: {
         bottom: 4,
         left: 8,
@@ -58,27 +68,28 @@ export default function contactList(theme: Theme) {
     },
     userQueryEditorHeight: 33,
     addContactButton: {
-      color: iconColor(theme, "primary"),
+      margin: { left: 6, right: 12 },
+      color: foreground(layer, "on"),
       buttonWidth: 28,
       iconWidth: 16,
     },
     rowHeight: 28,
     sectionIconSize: 8,
     headerRow: {
-      ...text(theme, "mono", "secondary", { size: "sm" }),
-      margin: { top: 6 },
+      ...text(layer, "mono", { size: "sm" }),
+      margin: { top: 14 },
       padding: {
         left: sidePadding,
         right: sidePadding,
       },
       active: {
-        ...text(theme, "mono", "primary", { size: "sm" }),
-        background: backgroundColor(theme, 100, "active"),
+        ...text(layer, "mono", "active", { size: "sm" }),
+        background: background(layer, "active"),
       },
     },
     leaveCall: {
-      background: backgroundColor(theme, 100),
-      border: border(theme, "secondary"),
+      background: background(layer),
+      border: border(layer),
       cornerRadius: 6,
       margin: {
         top: 1,
@@ -89,11 +100,11 @@ export default function contactList(theme: Theme) {
         left: 7,
         right: 7,
       },
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(layer, "sans", "variant", { size: "xs" }),
       hover: {
-        ...text(theme, "sans", "active", { size: "xs" }),
-        background: backgroundColor(theme, "on300", "hovered"),
-        border: border(theme, "primary"),
+        ...text(layer, "sans", "hovered", { size: "xs" }),
+        background: background(layer, "hovered"),
+        border: border(layer, "hovered"),
       },
     },
     contactRow: {
@@ -102,7 +113,7 @@ export default function contactList(theme: Theme) {
         right: sidePadding,
       },
       active: {
-        background: backgroundColor(theme, 100, "active"),
+        background: background(layer, "active"),
       },
     },
     contactAvatar: {
@@ -113,16 +124,16 @@ export default function contactList(theme: Theme) {
       cornerRadius: 4,
       padding: 4,
       margin: { top: 12, left: 12 },
-      background: iconColor(theme, "ok"),
+      background: foreground(layer, "positive"),
     },
     contactStatusBusy: {
       cornerRadius: 4,
       padding: 4,
       margin: { top: 12, left: 12 },
-      background: iconColor(theme, "error"),
+      background: foreground(layer, "negative"),
     },
     contactUsername: {
-      ...text(theme, "mono", "primary", { size: "sm" }),
+      ...text(layer, "mono", { size: "sm" }),
       margin: {
         left: nameMargin,
       },
@@ -131,39 +142,39 @@ export default function contactList(theme: Theme) {
     contactButton: {
       ...contactButton,
       hover: {
-        background: backgroundColor(theme, "on300", "hovered"),
+        background: background(layer, "hovered"),
       },
     },
     disabledButton: {
       ...contactButton,
-      background: backgroundColor(theme, 100),
-      color: iconColor(theme, "muted"),
+      background: background(layer, "on"),
+      color: foreground(layer, "on"),
     },
     callingIndicator: {
-      ...text(theme, "mono", "muted", { size: "xs" })
+      ...text(layer, "mono", "variant", { size: "xs" })
     },
     treeBranch: {
-      color: borderColor(theme, "active"),
+      color: borderColor(layer),
       width: 1,
       hover: {
-        color: borderColor(theme, "active"),
+        color: borderColor(layer),
       },
       active: {
-        color: borderColor(theme, "active"),
+        color: borderColor(layer),
       },
     },
     projectRow: {
       ...projectRow,
-      background: backgroundColor(theme, 300),
+      background: background(layer, "on"),
       name: {
         ...projectRow.name,
-        ...text(theme, "mono", "secondary", { size: "sm" }),
+        ...text(layer, "mono", { size: "sm" }),
       },
       hover: {
-        background: backgroundColor(theme, 300, "hovered"),
+        background: background(layer, "on", "hovered"),
       },
       active: {
-        background: backgroundColor(theme, 300, "active"),
+        background: background(layer, "on", "active"),
       },
     },
   }

styles/src/styleTree/contactNotification.ts πŸ”—

@@ -1,10 +1,11 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, iconColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, foreground, text } from "./components";
 
 const avatarSize = 12;
 const headerPadding = 8;
 
-export default function contactNotification(theme: Theme): Object {
+export default function contactNotification(colorScheme: ColorScheme): Object {
+  let layer = colorScheme.lowest;
   return {
     headerAvatar: {
       height: avatarSize,
@@ -12,32 +13,32 @@ export default function contactNotification(theme: Theme): Object {
       cornerRadius: 6,
     },
     headerMessage: {
-      ...text(theme, "sans", "primary", { size: "xs" }),
+      ...text(layer, "sans", { size: "xs" }),
       margin: { left: headerPadding, right: headerPadding },
     },
     headerHeight: 18,
     bodyMessage: {
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(layer, "sans", { size: "xs" }),
       margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
     },
     button: {
-      ...text(theme, "sans", "primary", { size: "xs" }),
-      background: backgroundColor(theme, "on300"),
+      ...text(layer, "sans", "on", { size: "xs" }),
+      background: background(layer, "on"),
       padding: 4,
       cornerRadius: 6,
       margin: { left: 6 },
       hover: {
-        background: backgroundColor(theme, "on300", "hovered"),
+        background: background(layer, "on", "hovered"),
       },
     },
     dismissButton: {
-      color: iconColor(theme, "secondary"),
+      color: foreground(layer, "on"),
       iconWidth: 8,
       iconHeight: 8,
       buttonWidth: 8,
       buttonHeight: 8,
       hover: {
-        color: iconColor(theme, "primary"),
+        color: foreground(layer, "on", "hovered"),
       },
     },
   };

styles/src/styleTree/contactsPopover.ts πŸ”—

@@ -1,15 +1,16 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
 
-export default function contactsPopover(theme: Theme) {
+export default function contactsPopover(colorScheme: ColorScheme) {
+  let layer = colorScheme.middle;
   const sidePadding = 12;
   return {
-    background: backgroundColor(theme, 300, "base"),
+    background: background(layer),
     cornerRadius: 6,
     padding: { top: 6 },
     margin: { top: -6 },
-    shadow: popoverShadow(theme),
-    border: border(theme, "primary"),
+    shadow: colorScheme.popoverShadow,
+    border: border(layer),
     width: 300,
     height: 400,
     inviteRowHeight: 28,
@@ -18,10 +19,10 @@ export default function contactsPopover(theme: Theme) {
         left: sidePadding,
         right: sidePadding,
       },
-      border: { top: true, width: 1, color: borderColor(theme, "primary") },
-      text: text(theme, "sans", "secondary", { size: "sm" }),
+      border: border(layer, { top: true }),
+      text: text(layer, "sans", "variant", { size: "sm" }),
       hover: {
-        text: text(theme, "sans", "active", { size: "sm" }),
+        text: text(layer, "sans", "hovered", { size: "sm" }),
       },
     },
   }

styles/src/styleTree/contextMenu.ts πŸ”—

@@ -1,45 +1,40 @@
-import Theme from "../themes/common/theme";
-import {
-  backgroundColor,
-  border,
-  borderColor,
-  popoverShadow,
-  text,
-} from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, borderColor, text } from "./components";
 
-export default function contextMenu(theme: Theme) {
+export default function contextMenu(colorScheme: ColorScheme) {
+  let layer = colorScheme.middle;
   return {
-    background: backgroundColor(theme, 300, "base"),
-    cornerRadius: 6,
-    padding: 6,
-    shadow: popoverShadow(theme),
-    border: border(theme, "primary"),
+    background: background(layer),
+    cornerRadius: 10,
+    padding: 4,
+    shadow: colorScheme.popoverShadow,
+    border: border(layer),
     keystrokeMargin: 30,
     item: {
       iconSpacing: 8,
       iconWidth: 14,
-      padding: { left: 4, right: 4, top: 2, bottom: 2 },
+      padding: { left: 6, right: 6, top: 2, bottom: 2 },
       cornerRadius: 6,
-      label: text(theme, "sans", "primary", { size: "sm" }),
+      label: text(layer, "sans", { size: "sm" }),
       keystroke: {
-        ...text(theme, "sans", "muted", { size: "sm", weight: "bold" }),
+        ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
         padding: { left: 3, right: 3 },
       },
       hover: {
-        background: backgroundColor(theme, 300, "hovered"),
-        text: text(theme, "sans", "primary", { size: "sm" }),
+        background: background(layer, "hovered"),
+        label: text(layer, "sans", "hovered", { size: "sm" }),
       },
       active: {
-        background: backgroundColor(theme, 300, "active"),
-        text: text(theme, "sans", "active", { size: "sm" }),
+        background: background(layer, "active"),
+        label: text(layer, "sans", "active", { size: "sm" }),
       },
       activeHover: {
-        background: backgroundColor(theme, 300, "hovered"),
-        text: text(theme, "sans", "active", { size: "sm" }),
+        background: background(layer, "active"),
+        label: text(layer, "sans", "active", { size: "sm" }),
       },
     },
     separator: {
-      background: borderColor(theme, "primary"),
+      background: borderColor(layer),
       margin: { top: 2, bottom: 2 },
     },
   };

styles/src/styleTree/editor.ts πŸ”—

@@ -1,19 +1,22 @@
-import Theme from "../themes/common/theme";
+import { fontWeights } from "../common";
 import { withOpacity } from "../utils/color";
 import {
-  backgroundColor,
+  ColorScheme,
+  Layer,
+  StyleSets,
+} from "../themes/common/colorScheme";
+import {
+  background,
   border,
   borderColor,
-  iconColor,
-  player,
-  popoverShadow,
+  foreground,
   text,
-  textColor,
-  TextColor,
 } from "./components";
 import hoverPopover from "./hoverPopover";
 
-export default function editor(theme: Theme) {
+export default function editor(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
+
   const autocompleteItem = {
     cornerRadius: 6,
     padding: {
@@ -24,17 +27,17 @@ export default function editor(theme: Theme) {
     },
   };
 
-  function diagnostic(theme: Theme, color: TextColor) {
+  function diagnostic(layer: Layer, styleSet: StyleSets) {
     return {
       textScaleFactor: 0.857,
       header: {
-        border: border(theme, "primary", {
+        border: border(layer, {
           top: true,
         }),
       },
       message: {
-        text: text(theme, "sans", color, { size: "sm" }),
-        highlightText: text(theme, "sans", color, {
+        text: text(layer, "sans", styleSet, "inverted", { size: "sm" }),
+        highlightText: text(layer, "sans", styleSet, "inverted", {
           size: "sm",
           weight: "bold",
         }),
@@ -42,121 +45,207 @@ export default function editor(theme: Theme) {
     };
   }
 
-  const syntax: any = {};
-  for (const syntaxKey in theme.syntax) {
-    const style = theme.syntax[syntaxKey];
-    syntax[syntaxKey] = {
-      color: style.color,
-      weight: style.weight,
-      underline: style.underline,
-      italic: style.italic,
-    };
-  }
+  const syntax = {
+    primary: {
+      color: colorScheme.ramps.neutral(1).hex(),
+      weight: fontWeights.normal,
+    },
+    comment: {
+      color: colorScheme.ramps.neutral(0.71).hex(),
+      weight: fontWeights.normal,
+    },
+    punctuation: {
+      color: colorScheme.ramps.neutral(0.86).hex(),
+      weight: fontWeights.normal,
+    },
+    constant: {
+      color: colorScheme.ramps.green(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    keyword: {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    function: {
+      color: colorScheme.ramps.yellow(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    type: {
+      color: colorScheme.ramps.cyan(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    constructor: {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    variant: {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    property: {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    enum: {
+      color: colorScheme.ramps.orange(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    operator: {
+      color: colorScheme.ramps.orange(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    string: {
+      color: colorScheme.ramps.orange(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    number: {
+      color: colorScheme.ramps.green(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    boolean: {
+      color: colorScheme.ramps.green(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    predictive: {
+      color: colorScheme.ramps.neutral(0.57).hex(),
+      weight: fontWeights.normal,
+    },
+    title: {
+      color: colorScheme.ramps.yellow(0.5).hex(),
+      weight: fontWeights.bold,
+    },
+    emphasis: {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.normal,
+    },
+    "emphasis.strong": {
+      color: colorScheme.ramps.blue(0.5).hex(),
+      weight: fontWeights.bold,
+    },
+    linkUri: {
+      color: colorScheme.ramps.green(0.5).hex(),
+      weight: fontWeights.normal,
+      underline: true,
+    },
+    linkText: {
+      color: colorScheme.ramps.orange(0.5).hex(),
+      weight: fontWeights.normal,
+      italic: true,
+    },
+  };
 
   return {
-    textColor: theme.syntax.primary.color,
-    background: backgroundColor(theme, 500),
-    activeLineBackground: theme.editor.line.active,
+    textColor: syntax.primary.color,
+    background: background(layer),
+    activeLineBackground: background(layer, "on"),
+    highlightedLineBackground: background(layer, "on"),
     codeActions: {
-      indicator: iconColor(theme, "secondary"),
-      verticalScale: 0.618
+      indicator: foreground(layer, "variant"),
+      verticalScale: 0.55,
     },
     diff: {
-      deleted: theme.iconColor.error,
-      inserted: theme.iconColor.ok,
-      modified: theme.iconColor.warning,
+      deleted: foreground(layer, "negative"),
+      modified: foreground(layer, "warning"),
+      inserted: foreground(layer, "positive"),
       removedWidthEm: 0.275,
       widthEm: 0.16,
       cornerRadius: 0.05,
     },
-    documentHighlightReadBackground: theme.editor.highlight.occurrence,
-    documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
-    errorColor: theme.textColor.error,
-    gutterBackground: backgroundColor(theme, 500),
+    documentHighlightReadBackground: colorScheme.ramps
+      .neutral(0.5)
+      .alpha(0.2)
+      .hex(), // TODO: This was blend
+    documentHighlightWriteBackground: colorScheme.ramps
+      .neutral(0.5)
+      .alpha(0.4)
+      .hex(), // TODO: This was blend * 2
+    errorColor: background(layer, "negative"),
+    gutterBackground: background(layer),
     gutterPaddingFactor: 3.5,
-    highlightedLineBackground: theme.editor.line.highlighted,
-    lineNumber: theme.editor.gutter.primary,
-    lineNumberActive: theme.editor.gutter.active,
+    lineNumber: foreground(layer, "disabled"),
+    lineNumberActive: foreground(layer),
     renameFade: 0.6,
     unnecessaryCodeFade: 0.5,
-    selection: player(theme, 1).selection,
+    selection: colorScheme.players[0],
     guestSelections: [
-      player(theme, 2).selection,
-      player(theme, 3).selection,
-      player(theme, 4).selection,
-      player(theme, 5).selection,
-      player(theme, 6).selection,
-      player(theme, 7).selection,
-      player(theme, 8).selection,
+      colorScheme.players[1],
+      colorScheme.players[2],
+      colorScheme.players[3],
+      colorScheme.players[4],
+      colorScheme.players[5],
+      colorScheme.players[6],
+      colorScheme.players[7],
     ],
     autocomplete: {
-      background: backgroundColor(theme, 500),
+      background: background(colorScheme.middle),
       cornerRadius: 8,
       padding: 4,
-      border: border(theme, "secondary"),
-      shadow: popoverShadow(theme),
+      margin: {
+        left: -14,
+      },
+      border: border(colorScheme.middle),
+      shadow: colorScheme.popoverShadow,
+      matchHighlight: foreground(colorScheme.middle, "accent"),
       item: autocompleteItem,
       hoveredItem: {
         ...autocompleteItem,
-        background: backgroundColor(theme, 500, "hovered"),
-      },
-      margin: {
-        left: -14,
+        matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
+        background: background(colorScheme.middle, "hovered"),
       },
-      matchHighlight: text(theme, "mono", "feature"),
       selectedItem: {
         ...autocompleteItem,
-        background: backgroundColor(theme, 500, "active"),
+        matchHighlight: foreground(colorScheme.middle, "accent", "active"),
+        background: background(colorScheme.middle, "active"),
       },
     },
     diagnosticHeader: {
-      background: backgroundColor(theme, 300),
+      background: background(colorScheme.middle),
       iconWidthFactor: 1.5,
-      textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
-      border: border(theme, "secondary", {
+      textScaleFactor: 0.857,
+      border: border(colorScheme.middle, {
         bottom: true,
         top: true,
       }),
       code: {
-        ...text(theme, "mono", "secondary", { size: "sm" }),
+        ...text(colorScheme.middle, "mono", { size: "sm" }),
         margin: {
           left: 10,
         },
       },
       message: {
-        highlightText: text(theme, "sans", "primary", {
+        highlightText: text(colorScheme.middle, "sans", {
           size: "sm",
           weight: "bold",
         }),
-        text: text(theme, "sans", "secondary", { size: "sm" }),
+        text: text(colorScheme.middle, "sans", { size: "sm" }),
       },
     },
     diagnosticPathHeader: {
-      background: theme.editor.line.active,
+      background: background(colorScheme.middle),
       textScaleFactor: 0.857,
-      filename: text(theme, "mono", "primary", { size: "sm" }),
+      filename: text(colorScheme.middle, "mono", { size: "sm" }),
       path: {
-        ...text(theme, "mono", "muted", { size: "sm" }),
+        ...text(colorScheme.middle, "mono", { size: "sm" }),
         margin: {
           left: 12,
         },
       },
     },
-    errorDiagnostic: diagnostic(theme, "error"),
-    warningDiagnostic: diagnostic(theme, "warning"),
-    informationDiagnostic: diagnostic(theme, "info"),
-    hintDiagnostic: diagnostic(theme, "info"),
-    invalidErrorDiagnostic: diagnostic(theme, "secondary"),
-    invalidHintDiagnostic: diagnostic(theme, "secondary"),
-    invalidInformationDiagnostic: diagnostic(theme, "secondary"),
-    invalidWarningDiagnostic: diagnostic(theme, "secondary"),
-    hoverPopover: hoverPopover(theme),
+    errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
+    warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
+    informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
+    hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
+    invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
+    invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
+    invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
+    invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
+    hoverPopover: hoverPopover(colorScheme),
     linkDefinition: {
-      color: theme.syntax.linkUri.color,
-      underline: theme.syntax.linkUri.underline,
+      color: syntax.linkUri.color,
+      underline: syntax.linkUri.underline,
     },
     jumpIcon: {
-      color: iconColor(theme, "secondary"),
+      color: foreground(layer, "on"),
       iconWidth: 20,
       buttonWidth: 20,
       cornerRadius: 6,
@@ -167,32 +256,28 @@ export default function editor(theme: Theme) {
         right: 6,
       },
       hover: {
-        color: iconColor(theme, "active"),
-        background: backgroundColor(theme, "on500"),
+        color: foreground(layer, "on", "hovered"),
+        background: background(layer, "on", "hovered"),
       },
     },
     scrollbar: {
       width: 12,
       minHeightFactor: 1.0,
       track: {
-        border: {
-          left: true,
-          width: 1,
-          color: borderColor(theme, "secondary"),
-        },
+        border: border(layer, "variant", { left: true }),
       },
       thumb: {
-        background: withOpacity(borderColor(theme, "secondary"), 0.5),
+        background: withOpacity(borderColor(layer, "variant"), 0.5),
         border: {
           width: 1,
-          color: withOpacity(borderColor(theme, 'muted'), 0.5),
+          color: withOpacity(borderColor(layer, 'variant'), 0.5),
         }
       }
     },
     compositionMark: {
       underline: {
         thickness: 1.0,
-        color: borderColor(theme, "active")
+        color: borderColor(layer),
       },
     },
     syntax,

styles/src/styleTree/hoverPopover.ts πŸ”—

@@ -1,18 +1,19 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, popoverShadow, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
 
-export default function HoverPopover(theme: Theme) {
+export default function HoverPopover(colorScheme: ColorScheme) {
+  let layer = colorScheme.middle;
   let baseContainer = {
-    background: backgroundColor(theme, "on500"),
+    background: background(layer),
     cornerRadius: 8,
     padding: {
       left: 8,
       right: 8,
       top: 4,
-      bottom: 4
+      bottom: 4,
     },
-    shadow: popoverShadow(theme),
-    border: border(theme, "secondary"),
+    shadow: colorScheme.popoverShadow,
+    border: border(layer),
     margin: {
       left: -8,
     },
@@ -22,32 +23,23 @@ export default function HoverPopover(theme: Theme) {
     container: baseContainer,
     infoContainer: {
       ...baseContainer,
-      background: backgroundColor(theme, "on500Info"),
-      border: {
-        color: theme.ramps.blue(0).hex(),
-        width: 1,
-      },
+      background: background(layer, "accent"),
+      border: border(layer, "accent"),
     },
     warningContainer: {
       ...baseContainer,
-      background: backgroundColor(theme, "on500Warning"),
-      border: {
-        color: theme.ramps.yellow(0).hex(),
-        width: 1,
-      },
+      background: background(layer, "warning"),
+      border: border(layer, "warning"),
     },
     errorContainer: {
       ...baseContainer,
-      background: backgroundColor(theme, "on500Error"),
-      border: {
-        color: theme.ramps.red(0).hex(),
-        width: 1,
-      }
+      background: background(layer, "negative"),
+      border: border(layer, "negative"),
     },
     block_style: {
       padding: { top: 4 },
     },
-    prose: text(theme, "sans", "primary", { size: "sm" }),
-    highlight: theme.editor.highlight.occurrence,
+    prose: text(layer, "sans", { size: "sm" }),
+    highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
   };
 }

styles/src/styleTree/incomingCallNotification.ts πŸ”—

@@ -1,12 +1,13 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, borderColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
 
-export default function incomingCallNotification(theme: Theme): Object {
+export default function incomingCallNotification(colorScheme: ColorScheme): Object {
+  let layer = colorScheme.middle;
   const avatarSize = 48;
   return {
     windowHeight: 74,
     windowWidth: 380,
-    background: backgroundColor(theme, 300),
+    background: background(layer),
     callerContainer: {
       padding: 12,
     },
@@ -19,26 +20,26 @@ export default function incomingCallNotification(theme: Theme): Object {
       margin: { left: 10 },
     },
     callerUsername: {
-      ...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
+      ...text(layer, "sans", { size: "sm", weight: "bold" }),
       margin: { top: -3 },
     },
     callerMessage: {
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(layer, "sans", "variant", { size: "xs" }),
       margin: { top: -3 },
     },
     worktreeRoots: {
-      ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
+      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
       margin: { top: -3 },
     },
     buttonWidth: 96,
     acceptButton: {
-      background: backgroundColor(theme, "ok", "active"),
-      border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
-      ...text(theme, "sans", "ok", { size: "xs", weight: "extra_bold" })
+      background: background(layer, "accent"),
+      border: border(layer, { left: true, bottom: true }),
+      ...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
     },
     declineButton: {
-      border: { left: true, width: 1, color: borderColor(theme, "primary") },
-      ...text(theme, "sans", "error", { size: "xs", weight: "extra_bold" })
+      border: border(layer, { left: true }),
+      ...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
     },
   };
 }

styles/src/styleTree/picker.ts πŸ”—

@@ -1,17 +1,16 @@
-import Theme from "../themes/common/theme";
-import {
-  backgroundColor,
-  border,
-  player,
-  modalShadow,
-  text,
-} from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
 
-export default function picker(theme: Theme) {
+export default function picker(colorScheme: ColorScheme) {
+  let layer = colorScheme.lowest;
   return {
-    background: backgroundColor(theme, 300),
-    cornerRadius: 8,
-    padding: 8,
+    background: background(layer),
+    border: border(layer),
+    shadow: colorScheme.modalShadow,
+    cornerRadius: 12,
+    padding: {
+      bottom: 4,
+    },
     item: {
       padding: {
         bottom: 4,
@@ -19,41 +18,48 @@ export default function picker(theme: Theme) {
         right: 12,
         top: 4,
       },
+      margin: {
+        top: 1,
+        left: 4,
+        right: 4,
+      },
       cornerRadius: 8,
-      text: text(theme, "sans", "secondary"),
-      highlightText: text(theme, "sans", "feature", { weight: "bold" }),
+      text: text(layer, "sans", "variant"),
+      highlightText: text(layer, "sans", "accent", { weight: "bold" }),
       active: {
-        background: backgroundColor(theme, 300, "active"),
-        text: text(theme, "sans", "active"),
+        background: background(layer, "base", "active"),
+        text: text(layer, "sans", "base", "active"),
+        highlightText: text(layer, "sans", "accent", {
+          weight: "bold",
+        }),
       },
       hover: {
-        background: backgroundColor(theme, 300, "hovered"),
+        background: background(layer, "hovered"),
       },
     },
-    border: border(theme, "primary"),
     empty: {
-      text: text(theme, "sans", "muted"),
+      text: text(layer, "sans", "variant"),
       padding: {
-        bottom: 4,
-        left: 12,
-        right: 12,
+        bottom: 8,
+        left: 16,
+        right: 16,
         top: 8,
       },
     },
     inputEditor: {
-      background: backgroundColor(theme, 500),
-      cornerRadius: 8,
-      placeholderText: text(theme, "sans", "placeholder"),
-      selection: player(theme, 1).selection,
-      text: text(theme, "mono", "primary"),
-      border: border(theme, "secondary"),
+      placeholderText: text(layer, "sans", "on", "disabled"),
+      selection: colorScheme.players[0],
+      text: text(layer, "mono", "on"),
+      border: border(layer, { bottom: true }),
       padding: {
-        bottom: 7,
+        bottom: 8,
         left: 16,
         right: 16,
-        top: 7,
+        top: 8,
+      },
+      margin: {
+        bottom: 4,
       },
     },
-    shadow: modalShadow(theme),
   };
 }

styles/src/styleTree/projectDiagnostics.ts πŸ”—

@@ -1,12 +1,13 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, text } from "./components";
 
-export default function projectDiagnostics(theme: Theme) {
+export default function projectDiagnostics(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
   return {
-    background: backgroundColor(theme, 500),
+    background: background(layer),
     tabIconSpacing: 4,
     tabIconWidth: 13,
     tabSummarySpacing: 10,
-    emptyMessage: text(theme, "sans", "secondary", { size: "md" }),
+    emptyMessage: text(layer, "sans", "variant", { size: "md" }),
   };
 }

styles/src/styleTree/projectPanel.ts πŸ”—

@@ -1,36 +1,36 @@
-import Theme from "../themes/common/theme";
-import { panel } from "./app";
-import { backgroundColor, iconColor, player, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, foreground, text } from "./components";
 
-export default function projectPanel(theme: Theme) {
+export default function projectPanel(colorScheme: ColorScheme) {
+  let layer = colorScheme.middle;
   return {
-    ...panel,
+    background: background(layer),
     padding: { left: 12, right: 12, top: 6, bottom: 6 },
     indentWidth: 8,
     entry: {
       height: 24,
-      iconColor: iconColor(theme, "muted"),
+      iconColor: foreground(layer, "variant"),
       iconSize: 8,
       iconSpacing: 8,
-      text: text(theme, "mono", "secondary", { size: "sm" }),
+      text: text(layer, "mono", "variant", { size: "sm" }),
       hover: {
-        background: backgroundColor(theme, 300, "hovered"),
+        background: background(layer, "variant", "hovered"),
       },
       active: {
-        background: backgroundColor(theme, 300, "active"),
-        text: text(theme, "mono", "active", { size: "sm" }),
+        background: background(layer, "active"),
+        text: text(layer, "mono", "active", { size: "sm" }),
       },
       activeHover: {
-        background: backgroundColor(theme, 300, "active"),
-        text: text(theme, "mono", "active", { size: "sm" }),
+        background: background(layer, "active"),
+        text: text(layer, "mono", "active", { size: "sm" }),
       },
     },
     cutEntryFade: 0.4,
     ignoredEntryFade: 0.6,
     filenameEditor: {
-      background: backgroundColor(theme, "on300"),
-      text: text(theme, "mono", "active", { size: "sm" }),
-      selection: player(theme, 1).selection,
+      background: background(layer, "on"),
+      text: text(layer, "mono", "on", { size: "sm" }),
+      selection: colorScheme.players[0],
     },
   };
 }

styles/src/styleTree/projectSharedNotification.ts πŸ”—

@@ -1,12 +1,14 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, borderColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
+
+export default function projectSharedNotification(colorScheme: ColorScheme): Object {
+  let layer = colorScheme.middle;
 
-export default function projectSharedNotification(theme: Theme): Object {
   const avatarSize = 48;
   return {
     windowHeight: 74,
     windowWidth: 380,
-    background: backgroundColor(theme, 300),
+    background: background(layer),
     ownerContainer: {
       padding: 12,
     },
@@ -19,26 +21,26 @@ export default function projectSharedNotification(theme: Theme): Object {
       margin: { left: 10 },
     },
     ownerUsername: {
-      ...text(theme, "sans", "active", { size: "sm", weight: "bold" }),
+      ...text(layer, "sans", { size: "sm", weight: "bold" }),
       margin: { top: -3 },
     },
     message: {
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(layer, "sans", "variant", { size: "xs" }),
       margin: { top: -3 },
     },
     worktreeRoots: {
-      ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
+      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
       margin: { top: -3 },
     },
     buttonWidth: 96,
     openButton: {
-      background: backgroundColor(theme, "info", "active"),
-      border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") },
-      ...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" })
+      background: background(layer, "accent"),
+      border: border(layer, { left: true, bottom: true, }),
+      ...text(layer, "sans", "accent", { size: "xs", weight: "extra_bold" })
     },
     dismissButton: {
-      border: { left: true, width: 1, color: borderColor(theme, "primary") },
-      ...text(theme, "sans", "secondary", { size: "xs", weight: "extra_bold" })
+      border: border(layer, { left: true }),
+      ...text(layer, "sans", "variant", { size: "xs", weight: "extra_bold" })
     },
   };
 }

styles/src/styleTree/search.ts πŸ”—

@@ -1,17 +1,19 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, player, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
+
+export default function search(colorScheme: ColorScheme) {
+  let layer = colorScheme.highest;
 
-export default function search(theme: Theme) {
   // Search input
   const editor = {
-    background: backgroundColor(theme, 500),
+    background: background(layer),
     cornerRadius: 8,
     minWidth: 200,
     maxWidth: 500,
-    placeholderText: text(theme, "mono", "placeholder"),
-    selection: player(theme, 1).selection,
-    text: text(theme, "mono", "active"),
-    border: border(theme, "secondary"),
+    placeholderText: text(layer, "mono", "disabled"),
+    selection: colorScheme.players[0],
+    text: text(layer, "mono", "default"),
+    border: border(layer),
     margin: {
       right: 12,
     },
@@ -24,14 +26,14 @@ export default function search(theme: Theme) {
   };
 
   return {
-    matchBackground: theme.editor.highlight.match,
+    matchBackground: background(layer), // theme.editor.highlight.match,
     tabIconSpacing: 8,
     tabIconWidth: 14,
     optionButton: {
-      ...text(theme, "mono", "secondary"),
-      background: backgroundColor(theme, "on500"),
+      ...text(layer, "mono", "on"),
+      background: background(layer, "on"),
       cornerRadius: 6,
-      border: border(theme, "secondary"),
+      border: border(layer, "on"),
       margin: {
         right: 4,
       },
@@ -42,28 +44,28 @@ export default function search(theme: Theme) {
         top: 2,
       },
       active: {
-        ...text(theme, "mono", "active"),
-        background: backgroundColor(theme, "on500", "active"),
-        border: border(theme, "muted"),
+        ...text(layer, "mono", "on", "inverted"),
+        background: background(layer, "on", "inverted"),
+        border: border(layer, "on", "inverted"),
       },
       clicked: {
-        ...text(theme, "mono", "active"),
-        background: backgroundColor(theme, "on300", "active"),
-        border: border(theme, "secondary"),
+        ...text(layer, "mono", "on", "pressed"),
+        background: background(layer, "on", "pressed"),
+        border: border(layer, "on", "pressed"),
       },
       hover: {
-        ...text(theme, "mono", "active"),
-        background: backgroundColor(theme, "on500", "hovered"),
-        border: border(theme, "muted"),
+        ...text(layer, "mono", "on", "hovered"),
+        background: background(layer, "on", "hovered"),
+        border: border(layer, "on", "hovered"),
       },
     },
     editor,
     invalidEditor: {
       ...editor,
-      border: border(theme, "error"),
+      border: border(layer, "negative"),
     },
     matchIndex: {
-      ...text(theme, "mono", "muted"),
+      ...text(layer, "mono", "variant"),
       padding: 6,
     },
     optionButtonGroup: {
@@ -73,7 +75,7 @@ export default function search(theme: Theme) {
       },
     },
     resultsStatus: {
-      ...text(theme, "mono", "primary"),
+      ...text(layer, "mono", "on"),
       size: 18,
     },
   };

styles/src/styleTree/statusBar.ts πŸ”—

@@ -1,8 +1,9 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, iconColor, text } from "./components";
-import { workspaceBackground } from "./workspace";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, foreground, text } from "./components";
+
+export default function statusBar(colorScheme: ColorScheme) {
+  let layer = colorScheme.lowest;
 
-export default function statusBar(theme: Theme) {
   const statusContainer = {
     cornerRadius: 6,
     padding: { top: 3, bottom: 3, left: 6, right: 6 },
@@ -22,70 +23,70 @@ export default function statusBar(theme: Theme) {
       left: 6,
       right: 6,
     },
-    border: border(theme, "primary", { top: true, overlay: true }),
-    cursorPosition: text(theme, "sans", "secondary"),
-    autoUpdateProgressMessage: text(theme, "sans", "secondary"),
-    autoUpdateDoneMessage: text(theme, "sans", "secondary"),
+    border: border(layer, { top: true, overlay: true }),
+    cursorPosition: text(layer, "sans", "variant"),
+    autoUpdateProgressMessage: text(layer, "sans", "variant"),
+    autoUpdateDoneMessage: text(layer, "sans", "variant"),
     lspStatus: {
       ...diagnosticStatusContainer,
       iconSpacing: 4,
       iconWidth: 14,
       height: 18,
-      message: text(theme, "sans", "secondary"),
-      iconColor: iconColor(theme, "muted"),
+      message: text(layer, "sans"),
+      iconColor: foreground(layer),
       hover: {
-        message: text(theme, "sans", "primary"),
-        iconColor: iconColor(theme, "primary"),
-        background: backgroundColor(theme, 300, "hovered"),
+        message: text(layer, "sans"),
+        iconColor: foreground(layer),
+        background: background(layer),
       },
     },
     diagnosticMessage: {
-      ...text(theme, "sans", "secondary"),
-      hover: text(theme, "sans", "active"),
+      ...text(layer, "sans"),
+      hover: text(layer, "sans", "hovered"),
     },
     feedback: {
-      ...text(theme, "sans", "secondary"),
-      hover: text(theme, "sans", "active"),
+      ...text(layer, "sans", "variant"),
+      hover: text(layer, "sans", "hovered"),
     },
     diagnosticSummary: {
-      height: 16,
+      height: 20,
       iconWidth: 16,
       iconSpacing: 2,
       summarySpacing: 6,
-      text: text(theme, "sans", "primary", { size: "sm" }),
-      iconColorOk: iconColor(theme, "muted"),
-      iconColorWarning: iconColor(theme, "warning"),
-      iconColorError: iconColor(theme, "error"),
+      text: text(layer, "sans", { size: "sm" }),
+      iconColorOk: foreground(layer, "variant"),
+      iconColorWarning: foreground(layer, "warning"),
+      iconColorError: foreground(layer, "negative"),
       containerOk: {
         cornerRadius: 6,
         padding: { top: 3, bottom: 3, left: 7, right: 7 },
       },
       containerWarning: {
         ...diagnosticStatusContainer,
-        background: backgroundColor(theme, "warning"),
-        border: border(theme, "warning"),
+        background: background(layer, "warning"),
+        border: border(layer, "warning"),
       },
       containerError: {
         ...diagnosticStatusContainer,
-        background: backgroundColor(theme, "error"),
-        border: border(theme, "error"),
+        background: background(layer, "negative"),
+        border: border(layer, "negative"),
       },
       hover: {
-        iconColorOk: iconColor(theme, "active"),
+        iconColorOk: foreground(layer, "on"),
         containerOk: {
           cornerRadius: 6,
           padding: { top: 3, bottom: 3, left: 7, right: 7 },
-          background: backgroundColor(theme, 300, "hovered"),
+          background: background(layer, "on", "hovered"),
         },
         containerWarning: {
           ...diagnosticStatusContainer,
-          background: backgroundColor(theme, "warning", "hovered"),
-          border: border(theme, "warning"),
+          background: background(layer, "warning", "hovered"),
+          border: border(layer, "warning", "hovered"),
         },
         containerError: {
           ...diagnosticStatusContainer,
-          background: backgroundColor(theme, "error", "hovered"),
-          border: border(theme, "error"),
+          background: background(layer, "negative", "hovered"),
+          border: border(layer, "negative", "hovered"),
         },
       },
     },
@@ -95,22 +96,22 @@ export default function statusBar(theme: Theme) {
       item: {
         ...statusContainer,
         iconSize: 16,
-        iconColor: iconColor(theme, "muted"),
+        iconColor: foreground(layer, "variant"),
         hover: {
-          iconColor: iconColor(theme, "active"),
-          background: backgroundColor(theme, 300, "hovered"),
+          iconColor: foreground(layer, "hovered"),
+          background: background(layer, "variant"),
         },
         active: {
-          iconColor: iconColor(theme, "active"),
-          background: backgroundColor(theme, 300, "active"),
+          iconColor: foreground(layer, "active"),
+          background: background(layer, "active"),
         },
       },
       badge: {
         cornerRadius: 3,
         padding: 2,
         margin: { bottom: -1, right: -1 },
-        border: { width: 1, color: workspaceBackground(theme) },
-        background: iconColor(theme, "feature"),
+        border: border(layer),
+        background: background(layer, "accent"),
       },
     },
   };

styles/src/styleTree/tabBar.ts πŸ”—

@@ -1,76 +1,84 @@
-import Theme from "../themes/common/theme";
+import { ColorScheme } from "../themes/common/colorScheme";
 import { withOpacity } from "../utils/color";
-import { iconColor, text, border, backgroundColor, draggedShadow } from "./components";
+import { text, border, background, foreground } from "./components";
 
-export default function tabBar(theme: Theme) {
+export default function tabBar(colorScheme: ColorScheme) {
   const height = 32;
 
+  let activeLayer = colorScheme.highest;
+  let layer = colorScheme.middle;
+
   const tab = {
     height,
-    background: backgroundColor(theme, 300),
-    border: border(theme, "primary", {
-      left: true,
+    text: text(layer, "sans", "variant", { size: "sm" }),
+    background: background(layer),
+    border: border(layer, {
+      right: true,
       bottom: true,
       overlay: true,
     }),
-    iconClose: iconColor(theme, "muted"),
-    iconCloseActive: iconColor(theme, "active"),
-    iconConflict: iconColor(theme, "warning"),
-    iconDirty: iconColor(theme, "info"),
-    iconWidth: 8,
-    spacing: 8,
-    text: text(theme, "sans", "secondary", { size: "sm" }),
     padding: {
       left: 8,
-      right: 8,
+      right: 12,
     },
+    spacing: 8,
+
+    // Close icons
+    iconWidth: 8,
+    iconClose: foreground(layer, "variant"),
+    iconCloseActive: foreground(layer),
+
+    // Indicators
+    iconConflict: foreground(layer, "warning"),
+    iconDirty: foreground(layer, "accent"),
+
+    // When two tabs of the same name are open, a label appears next to them
     description: {
-      margin: { left: 6, top: 1 },
-      ...text(theme, "sans", "muted", { size: "2xs" })
-    }
+      margin: { left: 8 },
+      ...text(layer, "sans", "disabled", { size: "2xs" }),
+    },
   };
 
   const activePaneActiveTab = {
     ...tab,
-    background: backgroundColor(theme, 500),
-    text: text(theme, "sans", "active", { size: "sm" }),
+    background: background(activeLayer),
+    text: text(activeLayer, "sans", "active", { size: "sm" }),
     border: {
       ...tab.border,
-      bottom: false
+      bottom: false,
     },
   };
 
   const inactivePaneInactiveTab = {
     ...tab,
-    background: backgroundColor(theme, 300),
-    text: text(theme, "sans", "muted", { size: "sm" }),
+    background: background(layer),
+    text: text(layer, "sans", "variant", { size: "sm" }),
   };
 
   const inactivePaneActiveTab = {
     ...tab,
-    background: backgroundColor(theme, 500),
-    text: text(theme, "sans", "secondary", { size: "sm" }),
+    background: background(activeLayer),
+    text: text(layer, "sans", "variant", { size: "sm" }),
     border: {
       ...tab.border,
-      bottom: false
+      bottom: false,
     },
-  }
+  };
 
   const draggedTab = {
     ...activePaneActiveTab,
-    background: withOpacity(tab.background, 0.8),
-    border: undefined as any, // Remove border
-    shadow: draggedShadow(theme),
-  }
+    background: withOpacity(tab.background, 0.95),
+    border: undefined as any,
+    shadow: colorScheme.popoverShadow,
+  };
 
   return {
     height,
-    background: backgroundColor(theme, 300),
-    dropTargetOverlayColor: withOpacity(theme.textColor.muted, 0.6),
-    border: border(theme, "primary", {
-      bottom: true,
-      overlay: true,
-    }),
+    background: background(layer),
+    dropTargetOverlayColor: withOpacity(
+      foreground(layer),
+      0.6
+    ),
     activePane: {
       activeTab: activePaneActiveTab,
       inactiveTab: tab,
@@ -81,11 +89,11 @@ export default function tabBar(theme: Theme) {
     },
     draggedTab,
     paneButton: {
-      color: iconColor(theme, "secondary"),
+      color: foreground(layer, "variant"),
       iconWidth: 12,
       buttonWidth: activePaneActiveTab.height,
       hover: {
-        color: iconColor(theme, "active"),
+        color: foreground(layer, "hovered"),
       },
     },
     paneButtonContainer: {
@@ -93,7 +101,7 @@ export default function tabBar(theme: Theme) {
       border: {
         ...tab.border,
         right: false,
-      }
-    }
-  }
-}
+      },
+    },
+  };
+}

styles/src/styleTree/terminal.ts πŸ”—

@@ -1,65 +1,52 @@
-import Theme from "../themes/common/theme";
-import { border, modalShadow, player } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
 
-export default function terminal(theme: Theme) {
+export default function terminal(colorScheme: ColorScheme) {
   /**
-  * Colors are controlled per-cell in the terminal grid. 
-  * Cells can be set to any of these more 'theme-capable' colors
-  * or can be set directly with RGB values. 
-  * Here are the common interpretations of these names:
-  * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
-  */
-  let colors = {
-    black: theme.ramps.neutral(0).hex(),
-    red: theme.ramps.red(0.5).hex(),
-    green: theme.ramps.green(0.5).hex(),
-    yellow: theme.ramps.yellow(0.5).hex(),
-    blue: theme.ramps.blue(0.5).hex(),
-    magenta: theme.ramps.magenta(0.5).hex(),
-    cyan: theme.ramps.cyan(0.5).hex(),
-    white: theme.ramps.neutral(7).hex(),
-    brightBlack: theme.ramps.neutral(4).hex(),
-    brightRed: theme.ramps.red(0.25).hex(),
-    brightGreen: theme.ramps.green(0.25).hex(),
-    brightYellow: theme.ramps.yellow(0.25).hex(),
-    brightBlue: theme.ramps.blue(0.25).hex(),
-    brightMagenta: theme.ramps.magenta(0.25).hex(),
-    brightCyan: theme.ramps.cyan(0.25).hex(),
-    brightWhite: theme.ramps.neutral(7).hex(),
+   * Colors are controlled per-cell in the terminal grid.
+   * Cells can be set to any of these more 'theme-capable' colors
+   * or can be set directly with RGB values.
+   * Here are the common interpretations of these names:
+   * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+   */
+  return {
+    black: colorScheme.ramps.neutral(0).hex(),
+    red: colorScheme.ramps.red(0.5).hex(),
+    green: colorScheme.ramps.green(0.5).hex(),
+    yellow: colorScheme.ramps.yellow(0.5).hex(),
+    blue: colorScheme.ramps.blue(0.5).hex(),
+    magenta: colorScheme.ramps.magenta(0.5).hex(),
+    cyan: colorScheme.ramps.cyan(0.5).hex(),
+    white: colorScheme.ramps.neutral(1).hex(),
+    brightBlack: colorScheme.ramps.neutral(0.4).hex(),
+    brightRed: colorScheme.ramps.red(0.25).hex(),
+    brightGreen: colorScheme.ramps.green(0.25).hex(),
+    brightYellow: colorScheme.ramps.yellow(0.25).hex(),
+    brightBlue: colorScheme.ramps.blue(0.25).hex(),
+    brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
+    brightCyan: colorScheme.ramps.cyan(0.25).hex(),
+    brightWhite: colorScheme.ramps.neutral(1).hex(),
     /**
      * Default color for characters
      */
-    foreground: theme.ramps.neutral(7).hex(),
+    foreground: colorScheme.ramps.neutral(1).hex(),
     /**
      * Default color for the rectangle background of a cell
      */
-    background: theme.ramps.neutral(0).hex(),
-    modalBackground: theme.ramps.neutral(1).hex(),
+    background: colorScheme.ramps.neutral(0).hex(),
+    modalBackground: colorScheme.ramps.neutral(0.1).hex(),
     /**
      * Default color for the cursor
      */
-    cursor: player(theme, 1).selection.cursor,
-    dimBlack: theme.ramps.neutral(7).hex(),
-    dimRed: theme.ramps.red(0.75).hex(),
-    dimGreen: theme.ramps.green(0.75).hex(),
-    dimYellow: theme.ramps.yellow(0.75).hex(),
-    dimBlue: theme.ramps.blue(0.75).hex(),
-    dimMagenta: theme.ramps.magenta(0.75).hex(),
-    dimCyan: theme.ramps.cyan(0.75).hex(),
-    dimWhite: theme.ramps.neutral(5).hex(),
-    brightForeground: theme.ramps.neutral(7).hex(),
-    dimForeground: theme.ramps.neutral(0).hex(),
-  };
-
-  return {
-    colors,
-    modalContainer: {
-      background: colors.modalBackground,
-      cornerRadius: 8,
-      padding: 8,
-      margin: 25,
-      border: border(theme, "primary"),
-      shadow: modalShadow(theme),
-    },
+    cursor: colorScheme.players[0].cursor,
+    dimBlack: colorScheme.ramps.neutral(1).hex(),
+    dimRed: colorScheme.ramps.red(0.75).hex(),
+    dimGreen: colorScheme.ramps.green(0.75).hex(),
+    dimYellow: colorScheme.ramps.yellow(0.75).hex(),
+    dimBlue: colorScheme.ramps.blue(0.75).hex(),
+    dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
+    dimCyan: colorScheme.ramps.cyan(0.75).hex(),
+    dimWhite: colorScheme.ramps.neutral(0.6).hex(),
+    brightForeground: colorScheme.ramps.neutral(1).hex(),
+    dimForeground: colorScheme.ramps.neutral(0).hex(),
   };
 }

styles/src/styleTree/tooltip.ts πŸ”—

@@ -1,21 +1,22 @@
-import Theme from "../themes/common/theme";
-import { backgroundColor, border, popoverShadow, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { background, border, text } from "./components";
 
-export default function tooltip(theme: Theme) {
+export default function tooltip(colorScheme: ColorScheme) {
+  let layer = colorScheme.middle;
   return {
-    background: backgroundColor(theme, 500),
-    border: border(theme, "secondary"),
+    background: background(layer),
+    border: border(layer),
     padding: { top: 4, bottom: 4, left: 8, right: 8 },
     margin: { top: 6, left: 6 },
-    shadow: popoverShadow(theme),
+    shadow: colorScheme.popoverShadow,
     cornerRadius: 6,
-    text: text(theme, "sans", "primary", { size: "xs" }),
+    text: text(layer, "sans", { size: "xs" }),
     keystroke: {
-      background: backgroundColor(theme, "on500"),
+      background: background(layer, "on"),
       cornerRadius: 4,
       margin: { left: 6 },
       padding: { left: 4, right: 4 },
-      ...text(theme, "mono", "secondary", { size: "xs", weight: "bold" }),
+      ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
     },
     maxTextWidth: 200,
   };

styles/src/styleTree/updateNotification.ts πŸ”—

@@ -1,29 +1,30 @@
-import Theme from "../themes/common/theme";
-import { iconColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme";
+import { foreground, text } from "./components";
 
 const headerPadding = 8;
 
-export default function updateNotification(theme: Theme): Object {
+export default function updateNotification(colorScheme: ColorScheme): Object {
+  let layer = colorScheme.middle;
   return {
     message: {
-      ...text(theme, "sans", "primary", { size: "xs" }),
+      ...text(layer, "sans", { size: "xs" }),
       margin: { left: headerPadding, right: headerPadding },
     },
     actionMessage: {
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(layer, "sans", { size: "xs" }),
       margin: { left: headerPadding, top: 6, bottom: 6 },
       hover: {
-        color: theme.textColor["active"],
+        color: foreground(layer, "hovered"),
       },
     },
     dismissButton: {
-      color: iconColor(theme, "secondary"),
+      color: foreground(layer),
       iconWidth: 8,
       iconHeight: 8,
       buttonWidth: 8,
       buttonHeight: 8,
       hover: {
-        color: iconColor(theme, "primary"),
+        color: foreground(layer, "hovered"),
       },
     },
   };

styles/src/styleTree/workspace.ts πŸ”—

@@ -1,64 +1,58 @@
-import Theme from "../themes/common/theme";
+import { ColorScheme } from "../themes/common/colorScheme";
 import { withOpacity } from "../utils/color";
 import {
-  backgroundColor,
+  background,
   border,
-  iconColor,
-  modalShadow,
+  borderColor,
+  foreground,
   text,
 } from "./components";
 import statusBar from "./statusBar";
 import tabBar from "./tabBar";
 
-export function workspaceBackground(theme: Theme) {
-  return backgroundColor(theme, 300);
-}
-
-export default function workspace(theme: Theme) {
+export default function workspace(colorScheme: ColorScheme) {
+  const layer = colorScheme.lowest;
   const titlebarPadding = 6;
   const titlebarButton = {
-    background: backgroundColor(theme, 100),
-    border: border(theme, "secondary"),
     cornerRadius: 6,
-    margin: {
-      top: 1,
-    },
     padding: {
       top: 1,
       bottom: 1,
-      left: 7,
-      right: 7,
+      left: 8,
+      right: 8,
     },
-    ...text(theme, "sans", "secondary", { size: "xs" }),
+    ...text(layer, "sans", "variant", { size: "xs" }),
+    background: background(layer, "variant"),
+    border: border(layer),
     hover: {
-      ...text(theme, "sans", "active", { size: "xs" }),
-      background: backgroundColor(theme, "on300", "hovered"),
-      border: border(theme, "primary"),
+      ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
+      background: background(layer, "variant", "hovered"),
+      border: border(layer, "variant", "hovered"),
     },
   };
   const avatarWidth = 18;
 
   return {
-    background: backgroundColor(theme, 300),
+    background: background(layer),
     joiningProjectAvatar: {
       cornerRadius: 40,
       width: 80,
     },
     joiningProjectMessage: {
       padding: 12,
-      ...text(theme, "sans", "primary", { size: "lg" }),
+      ...text(layer, "sans", { size: "lg" }),
     },
     externalLocationMessage: {
-      background: backgroundColor(theme, "on500Info"),
-      border: border(theme, "secondary"),
+      background: background(colorScheme.middle, "accent"),
+      border: border(colorScheme.middle, "accent"),
       cornerRadius: 6,
       padding: 12,
       margin: { bottom: 8, right: 8 },
-      ...text(theme, "sans", "secondary", { size: "xs" }),
+      ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
     },
     leaderBorderOpacity: 0.7,
     leaderBorderWidth: 2.0,
-    tabBar: tabBar(theme),
+    tabBar: tabBar(colorScheme),
     modal: {
       margin: {
         bottom: 52,
@@ -68,28 +62,28 @@ export default function workspace(theme: Theme) {
     },
     sidebar: {
       initialSize: 240,
-      border: {
-        color: border(theme, "primary").color,
-        width: 1,
-        left: true,
-        right: true,
-      }
+      border: border(layer, { left: true, right: true }),
     },
     paneDivider: {
-      color: border(theme, "secondary").color,
+      color: borderColor(layer),
       width: 1,
     },
-    statusBar: statusBar(theme),
+    statusBar: statusBar(colorScheme),
     titlebar: {
       avatarWidth,
       avatarMargin: 8,
-      height: 33,
-      background: backgroundColor(theme, 100),
+      height: 33, // 32px + 1px for overlaid border
+      background: background(layer),
+      border: border(layer, { bottom: true, overlay: true }),
       padding: {
         left: 80,
         right: titlebarPadding,
       },
-      title: text(theme, "sans", "primary"),
+
+      // Project
+      title: text(layer, "sans", "variant"),
+
+      // Collaborators
       avatar: {
         cornerRadius: avatarWidth / 2,
         border: {
@@ -108,15 +102,18 @@ export default function workspace(theme: Theme) {
       avatarRibbon: {
         height: 3,
         width: 12,
-        // TODO: The background for this ideally should be
-        // set with a token, not hardcoded in rust
+        // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
       },
-      border: border(theme, "primary", { bottom: true, overlay: true }),
+
+      // Sign in buttom
+      // FlatButton, Variant
       signInPrompt: {
         ...titlebarButton
       },
+
+      // Offline Indicator
       offlineIcon: {
-        color: iconColor(theme, "secondary"),
+        color: foreground(layer, "variant"),
         width: 16,
         margin: {
           left: titlebarPadding,
@@ -125,90 +122,93 @@ export default function workspace(theme: Theme) {
           right: 4,
         },
       },
+
+      // Notice that the collaboration server is out of date
       outdatedWarning: {
-        ...text(theme, "sans", "warning", { size: "xs" }),
-        background: backgroundColor(theme, "warning"),
-        border: border(theme, "warning"),
+        ...text(layer, "sans", "warning", { size: "xs" }),
+        background: withOpacity(background(layer, "warning"), 0.3),
+        border: border(layer, "warning"),
         margin: {
           left: titlebarPadding,
         },
         padding: {
-          left: 6,
-          right: 6,
+          left: 8,
+          right: 8,
         },
         cornerRadius: 6,
       },
       callControl: {
         cornerRadius: 6,
-        color: iconColor(theme, "secondary"),
+        color: foreground(layer, "variant"),
         iconWidth: 12,
         buttonWidth: 20,
         hover: {
-          background: backgroundColor(theme, "on300", "hovered"),
-          color: iconColor(theme, "active"),
+          background: background(layer, "variant", "hovered"),
+          color: foreground(layer, "variant", "hovered"),
         },
       },
       toggleContactsButton: {
         margin: { left: 6 },
         cornerRadius: 6,
-        color: iconColor(theme, "secondary"),
+        color: foreground(layer, "variant"),
         iconWidth: 8,
         buttonWidth: 20,
         active: {
-          background: backgroundColor(theme, "on300", "active"),
-          color: iconColor(theme, "active"),
+          background: background(layer, "variant", "active"),
+          color: foreground(layer, "variant", "active"),
         },
         hover: {
-          background: backgroundColor(theme, "on300", "hovered"),
-          color: iconColor(theme, "active"),
+          background: background(layer, "variant", "hovered"),
+          color: foreground(layer, "variant", "hovered"),
         },
       },
       toggleContactsBadge: {
         cornerRadius: 3,
         padding: 2,
         margin: { top: 3, left: 3 },
-        border: { width: 1, color: workspaceBackground(theme) },
-        background: iconColor(theme, "feature"),
+        border: border(layer),
+        background: foreground(layer, "accent"),
       },
       shareButton: {
         ...titlebarButton
       }
     },
+
     toolbar: {
       height: 34,
-      background: backgroundColor(theme, 500),
-      border: border(theme, "secondary", { bottom: true }),
+      background: background(colorScheme.highest),
+      border: border(colorScheme.highest, { bottom: true }),
       itemSpacing: 8,
       navButton: {
-        color: iconColor(theme, "primary"),
+        color: foreground(colorScheme.highest, "on"),
         iconWidth: 12,
         buttonWidth: 24,
         cornerRadius: 6,
         hover: {
-          color: iconColor(theme, "active"),
-          background: backgroundColor(theme, "on500", "hovered"),
+          color: foreground(colorScheme.highest, "on", "hovered"),
+          background: background(colorScheme.highest, "on", "hovered"),
         },
         disabled: {
-          color: withOpacity(iconColor(theme, "muted"), 0.6),
+          color: foreground(colorScheme.highest, "on", "disabled"),
         },
       },
       padding: { left: 8, right: 8, top: 4, bottom: 4 },
     },
     breadcrumbs: {
-      ...text(theme, "mono", "secondary"),
+      ...text(layer, "mono", "variant"),
       padding: { left: 6 },
     },
     disconnectedOverlay: {
-      ...text(theme, "sans", "active"),
-      background: withOpacity(theme.backgroundColor[500].base, 0.8),
+      ...text(layer, "sans"),
+      background: withOpacity(background(layer), 0.8),
     },
     notification: {
       margin: { top: 10 },
-      background: backgroundColor(theme, 300),
+      background: background(colorScheme.middle),
       cornerRadius: 6,
       padding: 12,
-      border: border(theme, "primary"),
-      shadow: modalShadow(theme),
+      border: border(colorScheme.middle),
+      shadow: colorScheme.popoverShadow,
     },
     notifications: {
       width: 400,
@@ -217,18 +217,15 @@ export default function workspace(theme: Theme) {
     dock: {
       initialSizeRight: 640,
       initialSizeBottom: 480,
-      wash_color: withOpacity(theme.backgroundColor[500].base, 0.5),
+      wash_color: withOpacity(background(colorScheme.highest), 0.5),
       panel: {
-        border: {
-          ...border(theme, "secondary"),
-          width: 1
-        },
+        border: border(colorScheme.highest),
       },
       maximized: {
-        margin: 24,
-        border: border(theme, "secondary", { "overlay": true }),
-        shadow: modalShadow(theme),
-      }
-    }
+        margin: 32,
+        border: border(colorScheme.highest, { overlay: true }),
+        shadow: colorScheme.modalShadow,
+      },
+    },
   };
 }

styles/src/themes.ts πŸ”—

@@ -1,31 +0,0 @@
-import fs from "fs";
-import path from "path";
-import Theme from "./themes/common/theme";
-
-const themes: Theme[] = [];
-export default themes;
-
-const internalThemes: Theme[] = [];
-export { internalThemes }
-
-const experimentalThemes: Theme[] = [];
-export { experimentalThemes }
-
-
-function fillThemes(themesPath: string, themes: Theme[]) {
-  for (const fileName of fs.readdirSync(themesPath)) {
-    if (fileName == "template.ts") continue;
-    const filePath = path.join(themesPath, fileName);
-
-    if (fs.statSync(filePath).isFile()) {
-      const theme = require(filePath);
-      if (theme.dark) themes.push(theme.dark);
-      if (theme.light) themes.push(theme.light);
-    }
-  }
-}
-
-fillThemes(path.resolve(`${__dirname}/themes`), themes)
-fillThemes(path.resolve(`${__dirname}/themes/internal`), internalThemes)
-fillThemes(path.resolve(`${__dirname}/themes/experiments`), experimentalThemes)
-

styles/src/themes/abruzzo.ts πŸ”—

@@ -1,28 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
-
-const name = "abruzzo";
-
-const ramps = {
-  neutral: chroma.scale([
-    "#1b0d05",
-    "#2c1e18",
-    "#654035",
-    "#9d5e4a",
-    "#b37354",
-    "#c1825a",
-    "#dda66e",
-    "#fbf3e2",
-  ]),
-  red: colorRamp(chroma("#e594c4")),
-  orange: colorRamp(chroma("#d9e87e")),
-  yellow: colorRamp(chroma("#fd9d83")),
-  green: colorRamp(chroma("#96adf7")),
-  cyan: colorRamp(chroma("#fc798f")),
-  blue: colorRamp(chroma("#BCD0F5")),
-  violet: colorRamp(chroma("#dac5eb")),
-  magenta: colorRamp(chroma("#c1a3ef")),
-};
-
-export const dark = createTheme(`${name}`, false, ramps);
-// export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/andromeda.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "andromeda";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#1E2025",
-    "#23262E",
-    "#292E38",
-    "#2E323C",
-    "#ACA8AE",
-    "#CBC9CF",
-    "#E1DDE4",
-    "#F7F7F8",
-  ]),
+  neutral: chroma
+    .scale([
+      "#1E2025",
+      "#23262E",
+      "#292E38",
+      "#2E323C",
+      "#ACA8AE",
+      "#CBC9CF",
+      "#E1DDE4",
+      "#F7F7F8",
+    ])
+    .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
   red: colorRamp(chroma("#F92672")),
   orange: colorRamp(chroma("#F39C12")),
   yellow: colorRamp(chroma("#FFE66D")),
@@ -24,4 +26,4 @@ const ramps = {
   magenta: colorRamp(chroma("#C74DED")),
 };
 
-export const dark = createTheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps);

styles/src/themes/brushtrees.ts πŸ”—

@@ -1,28 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
-
-const name = "brush-tree";
-
-const ramps = {
-  neutral: chroma.scale([
-    "#485867",
-    "#5A6D7A",
-    "#6D828E",
-    "#8299A1",
-    "#98AFB5",
-    "#B0C5C8",
-    "#C9DBDC",
-    "#E3EFEF",
-  ]),
-  red: colorRamp(chroma("#b38686")),
-  orange: colorRamp(chroma("#d8bba2")),
-  yellow: colorRamp(chroma("#aab386")),
-  green: colorRamp(chroma("#87b386")),
-  cyan: colorRamp(chroma("#86b3b3")),
-  blue: colorRamp(chroma("#868cb3")),
-  violet: colorRamp(chroma("#b386b2")),
-  magenta: colorRamp(chroma("#b39f9f")),
-};
-
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/cave.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "cave";
 
-const ramps = {
-  neutral: chroma.scale([
-    "#19171c",
-    "#26232a",
-    "#585260",
-    "#655f6d",
-    "#7e7887",
-    "#8b8792",
-    "#e2dfe7",
-    "#efecf4",
-  ]),
+export const dark = createColorScheme(`${name}-dark`, false, {
+  neutral: chroma
+    .scale([
+      "#19171c",
+      "#26232a",
+      "#585260",
+      "#655f6d",
+      "#7e7887",
+      "#8b8792",
+      "#e2dfe7",
+      "#efecf4",
+    ])
+    .domain([0, 0.15, 0.45, 0.6, 0.65, 0.7, 0.85, 1]),
   red: colorRamp(chroma("#be4678")),
   orange: colorRamp(chroma("#aa573c")),
   yellow: colorRamp(chroma("#a06e3b")),
@@ -22,7 +24,26 @@ const ramps = {
   blue: colorRamp(chroma("#576ddb")),
   violet: colorRamp(chroma("#955ae7")),
   magenta: colorRamp(chroma("#bf40bf")),
-};
+});
 
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createColorScheme(`${name}-light`, true, {
+  neutral: chroma
+    .scale([
+      "#19171c",
+      "#26232a",
+      "#585260",
+      "#655f6d",
+      "#7e7887",
+      "#8b8792",
+      "#e2dfe7",
+      "#efecf4",
+    ]).correctLightness(),
+  red: colorRamp(chroma("#be4678")),
+  orange: colorRamp(chroma("#aa573c")),
+  yellow: colorRamp(chroma("#a06e3b")),
+  green: colorRamp(chroma("#2a9292")),
+  cyan: colorRamp(chroma("#398bc6")),
+  blue: colorRamp(chroma("#576ddb")),
+  violet: colorRamp(chroma("#955ae7")),
+  magenta: colorRamp(chroma("#bf40bf")),
+});

styles/src/themes/common/colorScheme.ts πŸ”—

@@ -0,0 +1,78 @@
+import { Scale } from "chroma-js";
+
+export interface ColorScheme {
+  name: string;
+  isLight: boolean;
+
+  lowest: Layer;
+  middle: Layer;
+  highest: Layer;
+
+  ramps: RampSet;
+
+  popoverShadow: Shadow;
+  modalShadow: Shadow;
+
+  players: Players;
+}
+
+export interface Player {
+  cursor: string;
+  selection: string;
+}
+
+export interface Players {
+  "0": Player;
+  "1": Player;
+  "2": Player;
+  "3": Player;
+  "4": Player;
+  "5": Player;
+  "6": Player;
+  "7": Player;
+}
+
+export interface Shadow {
+  blur: number;
+  color: string;
+  offset: number[];
+}
+
+export type StyleSets = keyof Layer;
+export interface Layer {
+  base: StyleSet;
+  variant: StyleSet;
+  on: StyleSet;
+  accent: StyleSet;
+  positive: StyleSet;
+  warning: StyleSet;
+  negative: StyleSet;
+}
+
+export interface RampSet {
+  neutral: Scale;
+  red: Scale;
+  orange: Scale;
+  yellow: Scale;
+  green: Scale;
+  cyan: Scale;
+  blue: Scale;
+  violet: Scale;
+  magenta: Scale;
+}
+
+export type Styles = keyof StyleSet;
+export interface StyleSet {
+  default: Style;
+  active: Style;
+  disabled: Style;
+  hovered: Style;
+  pressed: Style;
+  inverted: Style;
+}
+
+export interface Style {
+  background: string;
+  border: string;
+  foreground: string;
+}

styles/src/themes/common/ramps.ts πŸ”—

@@ -0,0 +1,202 @@
+import chroma, { Color, Scale } from "chroma-js";
+import {
+  ColorScheme,
+  Layer,
+  Player,
+  RampSet,
+  Style,
+  Styles,
+  StyleSet,
+} from "./colorScheme";
+
+export function colorRamp(color: Color): Scale {
+  let endColor = color.desaturate(1).brighten(5);
+  let startColor = color.desaturate(1).darken(4);
+  return chroma.scale([startColor, color, endColor]).mode("lab");
+}
+
+export function createColorScheme(
+  name: string,
+  isLight: boolean,
+  colorRamps: { [rampName: string]: Scale }
+): ColorScheme {
+  // Chromajs scales from 0 to 1 flipped if isLight is true
+  let ramps: RampSet = {} as any;
+
+  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
+  // we now store the ramps object in the theme so that we can pull colors out of them.
+  // So instead of calling domain and storing the result, we have to construct new ramps for each
+  // theme so that we don't modify the passed in ramps.
+  // This combined with an error in the type definitions for chroma js means we have to cast the colors
+  // function to any in order to get the colors back out from the original ramps.
+  if (isLight) {
+    for (var rampName in colorRamps) {
+      (ramps as any)[rampName] = chroma.scale(
+        colorRamps[rampName].colors(100).reverse()
+      );
+    }
+    ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse());
+  } else {
+    for (var rampName in colorRamps) {
+      (ramps as any)[rampName] = chroma.scale(colorRamps[rampName].colors(100));
+    }
+    ramps.neutral = chroma.scale(colorRamps.neutral.colors(100));
+  }
+
+  let lowest = lowestLayer(ramps);
+  let middle = middleLayer(ramps);
+  let highest = highestLayer(ramps);
+
+  let popoverShadow = {
+    blur: 4,
+    color: ramps
+      .neutral(isLight ? 7 : 0)
+      .darken()
+      .alpha(0.2)
+      .hex(), // TODO used blend previously. Replace with something else
+    offset: [1, 2],
+  };
+
+  let modalShadow = {
+    blur: 16,
+    color: ramps
+      .neutral(isLight ? 7 : 0)
+      .darken()
+      .alpha(0.2)
+      .hex(), // TODO used blend previously. Replace with something else
+    offset: [0, 2],
+  };
+
+  let players = {
+    "0": player(ramps.blue),
+    "1": player(ramps.green),
+    "2": player(ramps.magenta),
+    "3": player(ramps.orange),
+    "4": player(ramps.violet),
+    "5": player(ramps.cyan),
+    "6": player(ramps.red),
+    "7": player(ramps.yellow),
+  };
+
+  return {
+    name,
+    isLight,
+
+    ramps,
+
+    lowest,
+    middle,
+    highest,
+
+    popoverShadow,
+    modalShadow,
+
+    players,
+  };
+}
+
+function player(ramp: Scale): Player {
+  return {
+    selection: ramp(0.5).alpha(0.24).hex(),
+    cursor: ramp(0.5).hex(),
+  };
+}
+
+function lowestLayer(ramps: RampSet): Layer {
+  return {
+    base: buildStyleSet(ramps.neutral, 0.2, 1),
+    variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
+    on: buildStyleSet(ramps.neutral, 0.1, 1),
+    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+    positive: buildStyleSet(ramps.green, 0.1, 0.5),
+    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+    negative: buildStyleSet(ramps.red, 0.1, 0.5),
+  };
+}
+
+function middleLayer(ramps: RampSet): Layer {
+  return {
+    base: buildStyleSet(ramps.neutral, 0.1, 1),
+    variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
+    on: buildStyleSet(ramps.neutral, 0, 1),
+    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+    positive: buildStyleSet(ramps.green, 0.1, 0.5),
+    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+    negative: buildStyleSet(ramps.red, 0.1, 0.5),
+  };
+}
+
+function highestLayer(ramps: RampSet): Layer {
+  return {
+    base: buildStyleSet(ramps.neutral, 0, 1),
+    variant: buildStyleSet(ramps.neutral, 0, 0.7),
+    on: buildStyleSet(ramps.neutral, 0.1, 1),
+    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+    positive: buildStyleSet(ramps.green, 0.1, 0.5),
+    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+    negative: buildStyleSet(ramps.red, 0.1, 0.5),
+  };
+}
+
+function buildStyleSet(
+  ramp: Scale,
+  backgroundBase: number,
+  foregroundBase: number,
+  step: number = 0.08,
+): StyleSet {
+  let styleDefinitions = buildStyleDefinition(backgroundBase, foregroundBase, step);
+
+  function colorString(indexOrColor: number | Color): string {
+    if (typeof indexOrColor === "number") {
+      return ramp(indexOrColor).hex();
+    } else {
+      return indexOrColor.hex();
+    }
+  }
+
+  function buildStyle(style: Styles): Style {
+    return {
+      background: colorString(styleDefinitions.background[style]),
+      border: colorString(styleDefinitions.border[style]),
+      foreground: colorString(styleDefinitions.foreground[style]),
+    };
+  }
+
+  return {
+    default: buildStyle("default"),
+    hovered: buildStyle("hovered"),
+    pressed: buildStyle("pressed"),
+    active: buildStyle("active"),
+    disabled: buildStyle("disabled"),
+    inverted: buildStyle("inverted"),
+  };
+}
+
+function buildStyleDefinition(bgBase: number, fgBase: number, step: number = 0.08) {
+  return {
+    background: {
+      default: bgBase,
+      hovered: bgBase + step,
+      pressed: bgBase + step * 1.5,
+      active: bgBase + step * 2.2,
+      disabled: bgBase,
+      inverted: fgBase + step * 6,
+    },
+    border: {
+      default: bgBase + step * 1,
+      hovered: bgBase + step,
+      pressed: bgBase + step,
+      active: bgBase + step * 3,
+      disabled: bgBase + step * 0.5,
+      inverted: bgBase - step * 3,
+    },
+    foreground: {
+      default: fgBase,
+      hovered: fgBase,
+      pressed: fgBase,
+      active: fgBase + step * 6,
+      disabled: bgBase + step * 4,
+      inverted: bgBase + step * 2,
+    },
+  };
+}

styles/src/themes/internal/zed-pro.ts πŸ”—

@@ -0,0 +1,30 @@
+import chroma from "chroma-js";
+import { colorRamp, createColorScheme } from "../common/ramps";
+
+const name = "zed-pro";
+
+const ramps = {
+  neutral: chroma
+    .scale([
+      "#101010",
+      "#1C1C1C",
+      "#212121",
+      "#2D2D2D",
+      "#B9B9B9",
+      "#DADADA",
+      "#E6E6E6",
+      "#FFFFFF",
+    ])
+    .domain([0, 0.1, 0.2, 0.3, 0.7, 0.8, 0.9, 1]),
+  red: colorRamp(chroma("#DC604F")),
+  orange: colorRamp(chroma("#DE782F")),
+  yellow: colorRamp(chroma("#E0B750")),
+  green: colorRamp(chroma("#2A643D")),
+  cyan: colorRamp(chroma("#215050")),
+  blue: colorRamp(chroma("#2F6DB7")),
+  violet: colorRamp(chroma("#5874C1")),
+  magenta: colorRamp(chroma("#DE9AB8")),
+};
+
+export const dark = createColorScheme(`${name}-dark`, false, ramps);
+export const light = createColorScheme(`${name}-light`, true, ramps);

styles/src/themes/one-dark.ts πŸ”—

@@ -1,5 +1,5 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "one";
 const author = "Chris Kempson (http://chriskempson.com)";
@@ -24,16 +24,9 @@ const base0E = "#c678dd";
 const base0F = "#be5046";
 
 const ramps = {
-  neutral: chroma.scale([
-    base00,
-    base01,
-    base02,
-    base03,
-    base04,
-    base05,
-    base06,
-    base07,
-  ]),
+  neutral: chroma
+    .scale([base00, base01, base02, base03, base04, base05, base06, base07])
+    .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
   red: colorRamp(chroma(base08)),
   orange: colorRamp(chroma(base09)),
   yellow: colorRamp(chroma(base0A)),
@@ -44,4 +37,4 @@ const ramps = {
   magenta: colorRamp(chroma(base0F)),
 };
 
-export const dark = createTheme(`${name}-dark`, false, ramps);
+export const dark = createColorScheme(`${name}-dark`, false, ramps);

styles/src/themes/one-light.ts πŸ”—

@@ -1,5 +1,5 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "one";
 const author = "Daniel Pfeifer (http://github.com/purpleKarrot)";
@@ -24,16 +24,9 @@ const base0E = "#a626a4";
 const base0F = "#986801";
 
 const ramps = {
-  neutral: chroma.scale([
-    base00,
-    base01,
-    base02,
-    base03,
-    base04,
-    base05,
-    base06,
-    base07,
-  ]),
+  neutral: chroma
+    .scale([base00, base01, base02, base03, base04, base05, base06, base07])
+    .domain([0, 0.05, 0.77, 1]),
   red: colorRamp(chroma(base08)),
   orange: colorRamp(chroma(base09)),
   yellow: colorRamp(chroma(base0A)),
@@ -44,4 +37,4 @@ const ramps = {
   magenta: colorRamp(chroma(base0F)),
 };
 
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createColorScheme(`${name}-light`, true, ramps);

styles/src/themes/rose-pine-dawn.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "rosΓ©-pine-dawn";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#575279",
-    "#797593",
-    "#9893A5",
-    "#B5AFB8",
-    "#D3CCCC",
-    "#F2E9E1",
-    "#FFFAF3",
-    "#FAF4ED",
-  ]),
+  neutral: chroma
+    .scale([
+      "#575279",
+      "#797593",
+      "#9893A5",
+      "#B5AFB8",
+      "#D3CCCC",
+      "#F2E9E1",
+      "#FFFAF3",
+      "#FAF4ED",
+    ])
+    .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
   red: colorRamp(chroma("#B4637A")),
   orange: colorRamp(chroma("#D7827E")),
   yellow: colorRamp(chroma("#EA9D34")),
@@ -24,4 +26,4 @@ const ramps = {
   magenta: colorRamp(chroma("#79549F")),
 };
 
-export const light = createTheme(`${name}`, true, ramps);
+export const light = createColorScheme(`${name}`, true, ramps);

styles/src/themes/rose-pine-moon.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "rosΓ©-pine-moon";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#232136",
-    "#2A273F",
-    "#393552",
-    "#3E3A53",
-    "#56526C",
-    "#6E6A86",
-    "#908CAA",
-    "#E0DEF4",
-  ]),
+  neutral: chroma
+    .scale([
+      "#232136",
+      "#2A273F",
+      "#393552",
+      "#3E3A53",
+      "#56526C",
+      "#6E6A86",
+      "#908CAA",
+      "#E0DEF4",
+    ])
+    .domain([0, 0.3, 0.55, 1]),
   red: colorRamp(chroma("#EB6F92")),
   orange: colorRamp(chroma("#EBBCBA")),
   yellow: colorRamp(chroma("#F6C177")),
@@ -24,4 +26,4 @@ const ramps = {
   magenta: colorRamp(chroma("#AB6FE9")),
 };
 
-export const dark = createTheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps);

styles/src/themes/rose-pine.ts πŸ”—

@@ -1,5 +1,5 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "rosΓ©-pine";
 
@@ -24,4 +24,4 @@ const ramps = {
   magenta: colorRamp(chroma("#AB6FE9")),
 };
 
-export const dark = createTheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps);

styles/src/themes/sandcastle.ts πŸ”—

@@ -1,5 +1,5 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "sandcastle";
 
@@ -17,11 +17,11 @@ const ramps = {
   red: colorRamp(chroma("#B4637A")),
   orange: colorRamp(chroma("#a07e3b")),
   yellow: colorRamp(chroma("#a07e3b")),
-  green: colorRamp(chroma("#528b8b")),
+  green: colorRamp(chroma("#83a598")),
   cyan: colorRamp(chroma("#83a598")),
-  blue: colorRamp(chroma("#83a598")),
+  blue: colorRamp(chroma("#528b8b")),
   violet: colorRamp(chroma("#d75f5f")),
   magenta: colorRamp(chroma("#a87322")),
 };
 
-export const dark = createTheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps);

styles/src/themes/solarized.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "solarized";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#002b36",
-    "#073642",
-    "#586e75",
-    "#657b83",
-    "#839496",
-    "#93a1a1",
-    "#eee8d5",
-    "#fdf6e3",
-  ]),
+  neutral: chroma
+    .scale([
+      "#002b36",
+      "#073642",
+      "#586e75",
+      "#657b83",
+      "#839496",
+      "#93a1a1",
+      "#eee8d5",
+      "#fdf6e3",
+    ])
+    .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
   red: colorRamp(chroma("#dc322f")),
   orange: colorRamp(chroma("#cb4b16")),
   yellow: colorRamp(chroma("#b58900")),
@@ -24,5 +26,5 @@ const ramps = {
   magenta: colorRamp(chroma("#d33682")),
 };
 
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const dark = createColorScheme(`${name}-dark`, false, ramps);
+export const light = createColorScheme(`${name}-light`, true, ramps);

styles/src/themes/sulphurpool.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "sulphurpool";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#202746",
-    "#293256",
-    "#5e6687",
-    "#6b7394",
-    "#898ea4",
-    "#979db4",
-    "#dfe2f1",
-    "#f5f7ff",
-  ]),
+  neutral: chroma
+    .scale([
+      "#202746",
+      "#293256",
+      "#5e6687",
+      "#6b7394",
+      "#898ea4",
+      "#979db4",
+      "#dfe2f1",
+      "#f5f7ff",
+    ])
+    .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
   red: colorRamp(chroma("#c94922")),
   orange: colorRamp(chroma("#c76b29")),
   yellow: colorRamp(chroma("#c08b30")),
@@ -24,5 +26,5 @@ const ramps = {
   magenta: colorRamp(chroma("#9c637a")),
 };
 
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const dark = createColorScheme(`${name}-dark`, false, ramps);
+export const light = createColorScheme(`${name}-light`, true, ramps);

styles/src/themes/summercamp.ts πŸ”—

@@ -1,19 +1,21 @@
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 const name = "summercamp";
 
 const ramps = {
-  neutral: chroma.scale([
-    "#1c1810",
-    "#2a261c",
-    "#3a3527",
-    "#3a3527",
-    "#5f5b45",
-    "#736e55",
-    "#bab696",
-    "#f8f5de",
-  ]),
+  neutral: chroma
+    .scale([
+      "#1c1810",
+      "#2a261c",
+      "#3a3527",
+      "#3a3527",
+      "#5f5b45",
+      "#736e55",
+      "#bab696",
+      "#f8f5de",
+    ])
+    .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
   red: colorRamp(chroma("#e35142")),
   orange: colorRamp(chroma("#fba11b")),
   yellow: colorRamp(chroma("#f2ff27")),
@@ -24,4 +26,4 @@ const ramps = {
   magenta: colorRamp(chroma("#F69BE7")),
 };
 
-export const dark = createTheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps);

styles/src/themes/summerfruit.ts πŸ”—

@@ -1,28 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
-
-const name = "summerfruit";
-
-const ramps = {
-  neutral: chroma.scale([
-    "#151515",
-    "#202020",
-    "#303030",
-    "#505050",
-    "#B0B0B0",
-    "#D0D0D0",
-    "#E0E0E0",
-    "#FFFFFF",
-  ]),
-  red: colorRamp(chroma("#FF0086")),
-  orange: colorRamp(chroma("#FD8900")),
-  yellow: colorRamp(chroma("#ABA800")),
-  green: colorRamp(chroma("#00C918")),
-  cyan: colorRamp(chroma("#1FAAAA")),
-  blue: colorRamp(chroma("#3777E6")),
-  violet: colorRamp(chroma("#AD00A1")),
-  magenta: colorRamp(chroma("#CC6633")),
-};
-
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/template.ts πŸ”—

@@ -3,7 +3,7 @@
  **/
 
 import chroma from "chroma-js";
-import { colorRamp, createTheme } from "./common/base16";
+import { colorRamp, createColorScheme } from "./common/ramps";
 
 /**
  * Theme Name
@@ -56,14 +56,14 @@ const ramps = {
 };
 
 /**
- * Theme Variants
+ * Color Scheme Variants
  *
  * Currently we only support (and require) dark and light themes
  * Eventually you will be able to have only a light or dark theme,
  * and define other variants here.
  *
- * createTheme([name], [isLight], [arrayOfRamps])
+ * createColorScheme([name], [isLight], [arrayOfRamps])
  **/
 
-export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const dark = createColorScheme(`${name}-dark`, false, ramps);
+export const light = createColorScheme(`${name}-light`, true, ramps);