Merge branch 'main' into add-link-to-community-repo-in-feedback-editor

Joseph Lyons created

Change summary

.github/workflows/ci.yml                                       |   4 
Cargo.lock                                                     |   7 
assets/icons/ellipsis_14.svg                                   |   3 
assets/icons/leave_12.svg                                      |   3 
assets/keymaps/default.json                                    |   5 
assets/keymaps/vim.json                                        |   1 
assets/settings/default.json                                   |   2 
crates/activity_indicator/src/activity_indicator.rs            | 147 
crates/call/src/call.rs                                        |  12 
crates/call/src/room.rs                                        |  91 
crates/client/src/client.rs                                    |  22 
crates/collab/Cargo.toml                                       |   2 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql |  14 
crates/collab/migrations/20230202155735_followers.sql          |  15 
crates/collab/src/db.rs                                        | 282 +
crates/collab/src/db/follower.rs                               |  51 
crates/collab/src/db/room.rs                                   |   8 
crates/collab/src/rpc.rs                                       |  35 
crates/collab/src/tests/integration_tests.rs                   | 120 
crates/collab_ui/Cargo.toml                                    |   1 
crates/collab_ui/src/collab_titlebar_item.rs                   | 761 ++-
crates/collab_ui/src/collab_ui.rs                              |   6 
crates/collab_ui/src/collaborator_list_popover.rs              | 165 
crates/collab_ui/src/contact_list.rs                           |  28 
crates/collab_ui/src/contacts_popover.rs                       |   4 
crates/collab_ui/src/face_pile.rs                              | 101 
crates/context_menu/src/context_menu.rs                        |  47 
crates/editor/src/editor.rs                                    |  72 
crates/editor/src/editor_tests.rs                              |   8 
crates/editor/src/items.rs                                     |   4 
crates/editor/src/test/editor_lsp_test_context.rs              |   6 
crates/feedback/src/feedback_editor.rs                         |  24 
crates/file_finder/src/file_finder.rs                          |  69 
crates/fuzzy/src/matcher.rs                                    |   1 
crates/fuzzy/src/paths.rs                                      |  29 
crates/gpui/src/app.rs                                         | 175 
crates/gpui/src/app/action.rs                                  |   8 
crates/gpui/src/app/menu.rs                                    |  37 
crates/gpui/src/elements.rs                                    |   1 
crates/gpui/src/elements/flex.rs                               |   2 
crates/gpui/src/keymap_matcher.rs                              |  86 
crates/gpui/src/keymap_matcher/keymap_context.rs               |  29 
crates/gpui/src/platform/mac/platform.rs                       |  47 
crates/gpui/src/platform/mac/window.rs                         |   1 
crates/live_kit_client/examples/test_app.rs                    |   1 
crates/picker/src/picker.rs                                    |   2 
crates/project_panel/src/project_panel.rs                      |   2 
crates/rope/Cargo.toml                                         |   2 
crates/rpc/proto/zed.proto                                     |  11 
crates/rpc/src/rpc.rs                                          |   2 
crates/search/src/project_search.rs                            |   8 
crates/terminal_view/src/terminal_view.rs                      |  99 
crates/theme/src/theme.rs                                      |  24 
crates/theme_testbench/src/theme_testbench.rs                  |  47 
crates/vim/src/motion.rs                                       |  50 
crates/vim/src/normal.rs                                       |  21 
crates/vim/src/state.rs                                        |  20 
crates/vim/test_data/test_enter.json                           |   1 
crates/workspace/src/item.rs                                   |  62 
crates/workspace/src/pane.rs                                   |   9 
crates/workspace/src/shared_screen.rs                          |  62 
crates/workspace/src/workspace.rs                              |  45 
crates/zed/Cargo.toml                                          |   2 
crates/zed/resources/app-icon-preview.png                      |   0 
crates/zed/resources/app-icon-preview@2x.png                   |   0 
crates/zed/resources/app-icon.png                              |   0 
crates/zed/resources/app-icon@2x.png                           |   0 
crates/zed/src/menus.rs                                        | 420 -
crates/zed/src/zed.rs                                          |   6 
styles/.prettierignore                                         |   2 
styles/package-lock.json                                       | 229 
styles/package.json                                            |  14 
styles/src/buildLicenses.ts                                    | 112 
styles/src/buildThemes.ts                                      |  72 
styles/src/colorSchemes.ts                                     |  74 
styles/src/common.ts                                           |  95 
styles/src/styleTree/app.ts                                    | 138 
styles/src/styleTree/commandPalette.ts                         |  54 
styles/src/styleTree/components.ts                             | 318 
styles/src/styleTree/contactFinder.ts                          | 126 
styles/src/styleTree/contactList.ts                            | 356 
styles/src/styleTree/contactNotification.ts                    |  82 
styles/src/styleTree/contactsPopover.ts                        |  52 
styles/src/styleTree/contextMenu.ts                            |  79 
styles/src/styleTree/editor.ts                                 | 586 +-
styles/src/styleTree/feedback.ts                               |  69 
styles/src/styleTree/hoverPopover.ts                           |  82 
styles/src/styleTree/incomingCallNotification.ts               |  94 
styles/src/styleTree/picker.ts                                 | 146 
styles/src/styleTree/projectDiagnostics.ts                     |  20 
styles/src/styleTree/projectPanel.ts                           | 110 
styles/src/styleTree/projectSharedNotification.ts              |  94 
styles/src/styleTree/search.ts                                 | 180 
styles/src/styleTree/sharedScreen.ts                           |  12 
styles/src/styleTree/simpleMessageNotification.ts              |  58 
styles/src/styleTree/statusBar.ts                              | 214 
styles/src/styleTree/tabBar.ts                                 | 180 
styles/src/styleTree/terminal.ts                               |  94 
styles/src/styleTree/tooltip.ts                                |  40 
styles/src/styleTree/updateNotification.ts                     |  54 
styles/src/styleTree/workspace.ts                              | 471 +-
styles/src/system/lib/convert.ts                               |  11 
styles/src/system/lib/curve.ts                                 |  26 
styles/src/system/lib/generate.ts                              | 159 
styles/src/system/ref/color.ts                                 | 445 ++
styles/src/system/ref/curves.ts                                |  25 
styles/src/system/system.ts                                    |  32 
styles/src/system/types.ts                                     |  66 
styles/src/themes/andromeda.ts                                 |  72 
styles/src/themes/atelier-cave.ts                              | 112 
styles/src/themes/atelier-sulphurpool.ts                       |  73 
styles/src/themes/common/base16.ts                             | 296 -
styles/src/themes/common/colorScheme.ts                        | 158 
styles/src/themes/common/ramps.ts                              | 359 
styles/src/themes/common/theme.ts                              | 165 
styles/src/themes/one-dark.ts                                  |  97 
styles/src/themes/one-light.ts                                 | 107 
styles/src/themes/rose-pine-dawn.ts                            |  72 
styles/src/themes/rose-pine-moon.ts                            |  72 
styles/src/themes/rose-pine.ts                                 |  68 
styles/src/themes/sandcastle.ts                                |  67 
styles/src/themes/solarized.ts                                 |  73 
styles/src/themes/staff/abruzzo.ts                             |  52 
styles/src/themes/staff/atelier-dune.ts                        |  59 
styles/src/themes/staff/atelier-heath.ts                       |  93 
styles/src/themes/staff/atelier-seaside.ts                     |  59 
styles/src/themes/staff/ayu-mirage.ts                          |  52 
styles/src/themes/staff/ayu.ts                                 |  90 
styles/src/themes/staff/brushtrees.ts                          | 128 
styles/src/themes/staff/dracula.ts                             |  54 
styles/src/themes/staff/gruvbox-medium.ts                      | 264 
styles/src/themes/staff/monokai.ts                             |  54 
styles/src/themes/staff/nord.ts                                |  54 
styles/src/themes/staff/seti-ui.ts                             |  54 
styles/src/themes/staff/tokyo-night-storm.ts                   |  54 
styles/src/themes/staff/tokyo-night.ts                         |  92 
styles/src/themes/staff/zed-pro.ts                             |  58 
styles/src/themes/staff/zenburn.ts                             |  54 
styles/src/themes/summercamp.ts                                |  72 
styles/src/utils/color.ts                                      |   4 
styles/src/utils/snakeCase.ts                                  |  38 
styles/tsconfig.json                                           |  20 
142 files changed, 6,690 insertions(+), 5,009 deletions(-)

Detailed changes

.github/workflows/ci.yml πŸ”—

@@ -19,7 +19,9 @@ env:
 jobs:
   rustfmt:
     name: Check formatting
-    runs-on: self-hosted
+    runs-on:
+      - self-hosted
+      - test
     steps:
       - name: Install Rust
         run: |

Cargo.lock πŸ”—

@@ -794,7 +794,7 @@ dependencies = [
 [[package]]
 name = "bromberg_sl2"
 version = "0.6.0"
-source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
+source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
 dependencies = [
  "digest 0.9.0",
  "lazy_static",
@@ -1188,7 +1188,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.5.4"
+version = "0.6.1"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1257,6 +1257,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "context_menu",
  "editor",
  "futures 0.3.25",
  "fuzzy",
@@ -8356,7 +8357,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.75.0"
+version = "0.76.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

assets/icons/ellipsis_14.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
+</svg>

assets/icons/leave_12.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
+</svg>

assets/keymaps/default.json πŸ”—

@@ -228,6 +228,7 @@
                     "replace_newest": true
                 }
             ],
+            "cmd-k cmd-i": "editor::Hover",
             "cmd-/": [
                 "editor::ToggleComments",
                 {
@@ -418,7 +419,7 @@
     {
         "bindings": {
             "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
-            "cmd-shift-c": "collab::ToggleCollaborationMenu",
+            "cmd-shift-c": "collab::ToggleContactsMenu",
             "cmd-alt-i": "zed::DebugElements"
         }
     },
@@ -456,7 +457,7 @@
         }
     },
     {
-        "context": "Dock",
+        "context": "Pane && docked",
         "bindings": {
             "shift-escape": "dock::HideDock",
             "cmd-escape": "dock::RemoveTabFromDock"

assets/keymaps/vim.json πŸ”—

@@ -27,6 +27,7 @@
             "h": "vim::Left",
             "backspace": "vim::Backspace",
             "j": "vim::Down",
+            "enter": "vim::NextLineStart",
             "k": "vim::Up",
             "l": "vim::Right",
             "$": "vim::EndOfLine",

assets/settings/default.json πŸ”—

@@ -83,7 +83,7 @@
     "hard_tabs": false,
     // How many columns a tab should occupy.
     "tab_size": 4,
-    // Control what info Zed sends to our servers
+    // Control what info is collected by Zed.
     "telemetry": {
         // Send debug info like crash reports.
         "diagnostics": true,

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

@@ -33,6 +33,19 @@ struct LspStatus {
     status: LanguageServerBinaryStatus,
 }
 
+struct PendingWork<'a> {
+    language_server_name: &'a str,
+    progress_token: &'a str,
+    progress: &'a LanguageServerProgress,
+}
+
+#[derive(Default)]
+struct Content {
+    icon: Option<&'static str>,
+    message: String,
+    action: Option<Box<dyn Action>>,
+}
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ActivityIndicator::show_error_message);
     cx.add_action(ActivityIndicator::dismiss_error_message);
@@ -69,6 +82,8 @@ impl ActivityIndicator {
             if let Some(auto_updater) = auto_updater.as_ref() {
                 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
             }
+            cx.observe_active_labeled_tasks(|_, cx| cx.notify())
+                .detach();
 
             Self {
                 statuses: Default::default(),
@@ -130,7 +145,7 @@ impl ActivityIndicator {
     fn pending_language_server_work<'a>(
         &self,
         cx: &'a AppContext,
-    ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
+    ) -> impl Iterator<Item = PendingWork<'a>> {
         self.project
             .read(cx)
             .language_server_statuses()
@@ -142,23 +157,29 @@ impl ActivityIndicator {
                     let mut pending_work = status
                         .pending_work
                         .iter()
-                        .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
+                        .map(|(token, progress)| PendingWork {
+                            language_server_name: status.name.as_str(),
+                            progress_token: token.as_str(),
+                            progress,
+                        })
                         .collect::<SmallVec<[_; 4]>>();
-                    pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
+                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
                     Some(pending_work)
                 }
             })
             .flatten()
     }
 
-    fn content_to_render(
-        &mut self,
-        cx: &mut RenderContext<Self>,
-    ) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
+    fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
         // Show any language server has pending activity.
         let mut pending_work = self.pending_language_server_work(cx);
-        if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
-            let mut message = lang_server_name.to_string();
+        if let Some(PendingWork {
+            language_server_name,
+            progress_token,
+            progress,
+        }) = pending_work.next()
+        {
+            let mut message = language_server_name.to_string();
 
             message.push_str(": ");
             if let Some(progress_message) = progress.message.as_ref() {
@@ -176,7 +197,11 @@ impl ActivityIndicator {
                 write!(&mut message, " + {} more", additional_work_count).unwrap();
             }
 
-            return (None, message, None);
+            return Content {
+                icon: None,
+                message,
+                action: None,
+            };
         }
 
         // Show any language server installation info.
@@ -199,19 +224,19 @@ impl ActivityIndicator {
         }
 
         if !downloading.is_empty() {
-            return (
-                Some(DOWNLOAD_ICON),
-                format!(
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
                     "Downloading {} language server{}...",
                     downloading.join(", "),
                     if downloading.len() > 1 { "s" } else { "" }
                 ),
-                None,
-            );
+                action: None,
+            };
         } else if !checking_for_update.is_empty() {
-            return (
-                Some(DOWNLOAD_ICON),
-                format!(
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
                     "Checking for updates to {} language server{}...",
                     checking_for_update.join(", "),
                     if checking_for_update.len() > 1 {
@@ -220,53 +245,61 @@ impl ActivityIndicator {
                         ""
                     }
                 ),
-                None,
-            );
+                action: None,
+            };
         } else if !failed.is_empty() {
-            return (
-                Some(WARNING_ICON),
-                format!(
+            return Content {
+                icon: Some(WARNING_ICON),
+                message: format!(
                     "Failed to download {} language server{}. Click to show error.",
                     failed.join(", "),
                     if failed.len() > 1 { "s" } else { "" }
                 ),
-                Some(Box::new(ShowErrorMessage)),
-            );
+                action: Some(Box::new(ShowErrorMessage)),
+            };
         }
 
         // Show any application auto-update info.
         if let Some(updater) = &self.auto_updater {
-            match &updater.read(cx).status() {
-                AutoUpdateStatus::Checking => (
-                    Some(DOWNLOAD_ICON),
-                    "Checking for Zed updates…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Downloading => (
-                    Some(DOWNLOAD_ICON),
-                    "Downloading Zed update…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Installing => (
-                    Some(DOWNLOAD_ICON),
-                    "Installing Zed update…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Updated => (
-                    None,
-                    "Click to restart and update Zed".to_string(),
-                    Some(Box::new(workspace::Restart)),
-                ),
-                AutoUpdateStatus::Errored => (
-                    Some(WARNING_ICON),
-                    "Auto update failed".to_string(),
-                    Some(Box::new(DismissErrorMessage)),
-                ),
+            return match &updater.read(cx).status() {
+                AutoUpdateStatus::Checking => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Checking for Zed updates…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Downloading => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Downloading Zed update…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Installing => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Installing Zed update…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Updated => Content {
+                    icon: None,
+                    message: "Click to restart and update Zed".to_string(),
+                    action: Some(Box::new(workspace::Restart)),
+                },
+                AutoUpdateStatus::Errored => Content {
+                    icon: Some(WARNING_ICON),
+                    message: "Auto update failed".to_string(),
+                    action: Some(Box::new(DismissErrorMessage)),
+                },
                 AutoUpdateStatus::Idle => Default::default(),
-            }
-        } else {
-            Default::default()
+            };
         }
+
+        if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
+            return Content {
+                icon: None,
+                message: most_recent_active_task.to_string(),
+                action: None,
+            };
+        }
+
+        Default::default()
     }
 }
 
@@ -280,7 +313,11 @@ impl View for ActivityIndicator {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let (icon, message, action) = self.content_to_render(cx);
+        let Content {
+            icon,
+            message,
+            action,
+        } = self.content_to_render(cx);
 
         let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
             let theme = &cx

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

@@ -284,6 +284,18 @@ impl ActiveCall {
         }
     }
 
+    pub fn unshare_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Some((room, _)) = self.room.as_ref() {
+            room.update(cx, |room, cx| room.unshare_project(project, cx))
+        } else {
+            Err(anyhow!("no active call"))
+        }
+    }
+
     pub fn set_location(
         &mut self,
         project: Option<&ModelHandle<Project>>,

crates/call/src/room.rs πŸ”—

@@ -55,6 +55,7 @@ pub struct Room {
     leave_when_empty: bool,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
+    follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
     subscriptions: Vec<client::Subscription>,
     pending_room_update: Option<Task<()>>,
     maintain_connection: Option<Task<Option<()>>>,
@@ -148,6 +149,7 @@ impl Room {
             pending_room_update: None,
             client,
             user_store,
+            follows_by_leader_id_project_id: Default::default(),
             maintain_connection: Some(maintain_connection),
         }
     }
@@ -275,14 +277,12 @@ impl Room {
     ) -> Result<()> {
         let mut client_status = client.status();
         loop {
-            let is_connected = client_status
-                .next()
-                .await
-                .map_or(false, |s| s.is_connected());
-
+            let _ = client_status.try_recv();
+            let is_connected = client_status.borrow().is_connected();
             // Even if we're initially connected, any future change of the status means we momentarily disconnected.
             if !is_connected || client_status.next().await.is_some() {
                 log::info!("detected client disconnection");
+
                 this.upgrade(&cx)
                     .ok_or_else(|| anyhow!("room was dropped"))?
                     .update(&mut cx, |this, cx| {
@@ -296,12 +296,7 @@ impl Room {
                     let client_reconnection = async {
                         let mut remaining_attempts = 3;
                         while remaining_attempts > 0 {
-                            log::info!(
-                                "waiting for client status change, remaining attempts {}",
-                                remaining_attempts
-                            );
-                            let Some(status) = client_status.next().await else { break };
-                            if status.is_connected() {
+                            if client_status.borrow().is_connected() {
                                 log::info!("client reconnected, attempting to rejoin room");
 
                                 let Some(this) = this.upgrade(&cx) else { break };
@@ -315,7 +310,15 @@ impl Room {
                                 } else {
                                     remaining_attempts -= 1;
                                 }
+                            } else if client_status.borrow().is_signed_out() {
+                                return false;
                             }
+
+                            log::info!(
+                                "waiting for client status change, remaining attempts {}",
+                                remaining_attempts
+                            );
+                            client_status.next().await;
                         }
                         false
                     }
@@ -337,18 +340,20 @@ impl Room {
                     }
                 }
 
-                // The client failed to re-establish a connection to the server
-                // or an error occurred while trying to re-join the room. Either way
-                // we leave the room and return an error.
-                if let Some(this) = this.upgrade(&cx) {
-                    log::info!("reconnection failed, leaving room");
-                    let _ = this.update(&mut cx, |this, cx| this.leave(cx));
-                }
-                return Err(anyhow!(
-                    "can't reconnect to room: client failed to re-establish connection"
-                ));
+                break;
             }
         }
+
+        // The client failed to re-establish a connection to the server
+        // or an error occurred while trying to re-join the room. Either way
+        // we leave the room and return an error.
+        if let Some(this) = this.upgrade(&cx) {
+            log::info!("reconnection failed, leaving room");
+            let _ = this.update(&mut cx, |this, cx| this.leave(cx));
+        }
+        Err(anyhow!(
+            "can't reconnect to room: client failed to re-establish connection"
+        ))
     }
 
     fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
@@ -457,6 +462,12 @@ impl Room {
         self.participant_user_ids.contains(&user_id)
     }
 
+    pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
+        self.follows_by_leader_id_project_id
+            .get(&(leader_id, project_id))
+            .map_or(&[], |v| v.as_slice())
+    }
+
     async fn handle_room_updated(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -487,11 +498,13 @@ impl Room {
             .iter()
             .map(|p| p.user_id)
             .collect::<Vec<_>>();
+
         let remote_participant_user_ids = room
             .participants
             .iter()
             .map(|p| p.user_id)
             .collect::<Vec<_>>();
+
         let (remote_participants, pending_participants) =
             self.user_store.update(cx, move |user_store, cx| {
                 (
@@ -499,6 +512,7 @@ impl Room {
                     user_store.get_users(pending_participant_user_ids, cx),
                 )
             });
+
         self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
             let (remote_participants, pending_participants) =
                 futures::join!(remote_participants, pending_participants);
@@ -620,6 +634,27 @@ impl Room {
                     }
                 }
 
+                this.follows_by_leader_id_project_id.clear();
+                for follower in room.followers {
+                    let project_id = follower.project_id;
+                    let (leader, follower) = match (follower.leader_id, follower.follower_id) {
+                        (Some(leader), Some(follower)) => (leader, follower),
+
+                        _ => {
+                            log::error!("Follower message {follower:?} missing some state");
+                            continue;
+                        }
+                    };
+
+                    let list = this
+                        .follows_by_leader_id_project_id
+                        .entry((leader, project_id))
+                        .or_insert(Vec::new());
+                    if !list.contains(&follower) {
+                        list.push(follower);
+                    }
+                }
+
                 this.pending_room_update.take();
                 if this.should_leave() {
                     log::info!("room is empty, leaving");
@@ -793,6 +828,20 @@ impl Room {
         })
     }
 
+    pub(crate) fn unshare_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let project_id = match project.read(cx).remote_id() {
+            Some(project_id) => project_id,
+            None => return Ok(()),
+        };
+
+        self.client.send(proto::UnshareProject { project_id })?;
+        project.update(cx, |this, cx| this.unshare(cx))
+    }
+
     pub(crate) fn set_location(
         &mut self,
         project: Option<&ModelHandle<Project>>,

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

@@ -66,7 +66,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
 pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
 pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
 
-actions!(client, [Authenticate]);
+actions!(client, [Authenticate, SignOut]);
 
 pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_global_action({
@@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
             .detach();
         }
     });
+    cx.add_global_action({
+        let client = client.clone();
+        move |_: &SignOut, cx| {
+            let client = client.clone();
+            cx.spawn(|cx| async move {
+                client.disconnect(&cx);
+            })
+            .detach();
+        }
+    });
 }
 
 pub struct Client {
@@ -169,6 +179,10 @@ impl Status {
     pub fn is_connected(&self) -> bool {
         matches!(self, Self::Connected { .. })
     }
+
+    pub fn is_signed_out(&self) -> bool {
+        matches!(self, Self::SignedOut | Self::UpgradeRequired)
+    }
 }
 
 struct ClientState {
@@ -1152,11 +1166,9 @@ impl Client {
         })
     }
 
-    pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
-        let conn_id = self.connection_id()?;
-        self.peer.disconnect(conn_id);
+    pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+        self.peer.teardown();
         self.set_status(Status::SignedOut, cx);
-        Ok(())
     }
 
     fn connection_id(&self) -> Result<ConnectionId> {

crates/collab/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.5.4"
+version = "0.6.1"
 publish = false
 
 [[bin]]

crates/collab/migrations.sqlite/20221109000000_test_schema.sql πŸ”—

@@ -143,3 +143,17 @@ CREATE TABLE "servers" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "environment" VARCHAR NOT NULL
 );
+
+CREATE TABLE "followers" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+    "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+    "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "leader_connection_id" INTEGER NOT NULL,
+    "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "follower_connection_id" INTEGER NOT NULL
+);
+CREATE UNIQUE INDEX 
+    "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

crates/collab/migrations/20230202155735_followers.sql πŸ”—

@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS "followers" (
+    "id" SERIAL PRIMARY KEY,
+    "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+    "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+    "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "leader_connection_id" INTEGER NOT NULL,
+    "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "follower_connection_id" INTEGER NOT NULL
+);
+
+CREATE UNIQUE INDEX 
+    "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

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

@@ -1,5 +1,6 @@
 mod access_token;
 mod contact;
+mod follower;
 mod language_server;
 mod project;
 mod project_collaborator;
@@ -157,7 +158,7 @@ impl Database {
         room_id: RoomId,
         new_server_id: ServerId,
     ) -> Result<RoomGuard<RefreshedRoom>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let stale_participant_filter = Condition::all()
                 .add(room_participant::Column::RoomId.eq(room_id))
                 .add(room_participant::Column::AnsweringConnectionId.is_not_null())
@@ -190,17 +191,18 @@ impl Database {
                     .filter(room_participant::Column::RoomId.eq(room_id))
                     .exec(&*tx)
                     .await?;
+                project::Entity::delete_many()
+                    .filter(project::Column::RoomId.eq(room_id))
+                    .exec(&*tx)
+                    .await?;
                 room::Entity::delete_by_id(room_id).exec(&*tx).await?;
             }
 
-            Ok((
-                room_id,
-                RefreshedRoom {
-                    room,
-                    stale_participant_user_ids,
-                    canceled_calls_to_user_ids,
-                },
-            ))
+            Ok(RefreshedRoom {
+                room,
+                stale_participant_user_ids,
+                canceled_calls_to_user_ids,
+            })
         })
         .await
     }
@@ -1129,18 +1131,16 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
         live_kit_room: &str,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+    ) -> Result<proto::Room> {
+        self.transaction(|tx| async move {
             let room = room::ActiveModel {
                 live_kit_room: ActiveValue::set(live_kit_room.into()),
                 ..Default::default()
             }
             .insert(&*tx)
             .await?;
-            let room_id = room.id;
-
             room_participant::ActiveModel {
-                room_id: ActiveValue::set(room_id),
+                room_id: ActiveValue::set(room.id),
                 user_id: ActiveValue::set(user_id),
                 answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
                 answering_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -1157,8 +1157,8 @@ impl Database {
             .insert(&*tx)
             .await?;
 
-            let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            let room = self.get_room(room.id, &tx).await?;
+            Ok(room)
         })
         .await
     }
@@ -1171,7 +1171,7 @@ impl Database {
         called_user_id: UserId,
         initial_project_id: Option<ProjectId>,
     ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             room_participant::ActiveModel {
                 room_id: ActiveValue::set(room_id),
                 user_id: ActiveValue::set(called_user_id),
@@ -1190,7 +1190,7 @@ impl Database {
             let room = self.get_room(room_id, &tx).await?;
             let incoming_call = Self::build_incoming_call(&room, called_user_id)
                 .ok_or_else(|| anyhow!("failed to build incoming call"))?;
-            Ok((room_id, (room, incoming_call)))
+            Ok((room, incoming_call))
         })
         .await
     }
@@ -1200,7 +1200,7 @@ impl Database {
         room_id: RoomId,
         called_user_id: UserId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             room_participant::Entity::delete_many()
                 .filter(
                     room_participant::Column::RoomId
@@ -1210,7 +1210,7 @@ impl Database {
                 .exec(&*tx)
                 .await?;
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            Ok(room)
         })
         .await
     }
@@ -1257,7 +1257,7 @@ impl Database {
         calling_connection: ConnectionId,
         called_user_id: UserId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -1276,14 +1276,13 @@ impl Database {
                 .one(&*tx)
                 .await?
                 .ok_or_else(|| anyhow!("no call to cancel"))?;
-            let room_id = participant.room_id;
 
             room_participant::Entity::delete(participant.into_active_model())
                 .exec(&*tx)
                 .await?;
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            Ok(room)
         })
         .await
     }
@@ -1294,7 +1293,7 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let result = room_participant::Entity::update_many()
                 .filter(
                     Condition::all()
@@ -1316,7 +1315,7 @@ impl Database {
                 Err(anyhow!("room does not exist or was already joined"))?
             } else {
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, room))
+                Ok(room)
             }
         })
         .await
@@ -1328,9 +1327,9 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<RejoinedRoom>> {
-        self.room_transaction(|tx| async {
+        let room_id = RoomId::from_proto(rejoin_room.id);
+        self.room_transaction(room_id, |tx| async {
             let tx = tx;
-            let room_id = RoomId::from_proto(rejoin_room.id);
             let participant_update = room_participant::Entity::update_many()
                 .filter(
                     Condition::all()
@@ -1549,14 +1548,11 @@ impl Database {
             }
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((
-                room_id,
-                RejoinedRoom {
-                    room,
-                    rejoined_projects,
-                    reshared_projects,
-                },
-            ))
+            Ok(RejoinedRoom {
+                room,
+                rejoined_projects,
+                reshared_projects,
+            })
         })
         .await
     }
@@ -1717,13 +1713,78 @@ impl Database {
         .await
     }
 
+    pub async fn follow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                project_id: ActiveValue::set(project_id),
+                leader_connection_server_id: ActiveValue::set(ServerId(
+                    leader_connection.owner_id as i32,
+                )),
+                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
+                follower_connection_server_id: ActiveValue::set(ServerId(
+                    follower_connection.owner_id as i32,
+                )),
+                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn unfollow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(follower::Column::ProjectId.eq(project_id))
+                        .add(
+                            follower::Column::LeaderConnectionServerId
+                                .eq(leader_connection.owner_id)
+                                .and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
+                        )
+                        .add(
+                            follower::Column::FollowerConnectionServerId
+                                .eq(follower_connection.owner_id)
+                                .and(
+                                    follower::Column::FollowerConnectionId
+                                        .eq(follower_connection.id),
+                                ),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
     pub async fn update_room_participant_location(
         &self,
         room_id: RoomId,
         connection: ConnectionId,
         location: proto::ParticipantLocation,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async {
+        self.room_transaction(room_id, |tx| async {
             let tx = tx;
             let location_kind;
             let location_project_id;
@@ -1769,7 +1830,7 @@ impl Database {
 
             if result.rows_affected == 1 {
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, room))
+                Ok(room)
             } else {
                 Err(anyhow!("could not update room participant location"))?
             }
@@ -1926,12 +1987,25 @@ impl Database {
                 }
             }
         }
+        drop(db_projects);
+
+        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
+        let mut followers = Vec::new();
+        while let Some(db_follower) = db_followers.next().await {
+            let db_follower = db_follower?;
+            followers.push(proto::Follower {
+                leader_id: Some(db_follower.leader_connection().into()),
+                follower_id: Some(db_follower.follower_connection().into()),
+                project_id: db_follower.project_id.to_proto(),
+            });
+        }
 
         Ok(proto::Room {
             id: db_room.id.to_proto(),
             live_kit_room: db_room.live_kit_room,
             participants: participants.into_values().collect(),
             pending_participants,
+            followers,
         })
     }
 
@@ -1963,7 +2037,7 @@ impl Database {
         connection: ConnectionId,
         worktrees: &[proto::WorktreeMetadata],
     ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -2024,7 +2098,7 @@ impl Database {
             .await?;
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, (project.id, room)))
+            Ok((project.id, room))
         })
         .await
     }
@@ -2034,7 +2108,8 @@ impl Database {
         project_id: ProjectId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
 
             let project = project::Entity::find_by_id(project_id)
@@ -2042,12 +2117,11 @@ impl Database {
                 .await?
                 .ok_or_else(|| anyhow!("project not found"))?;
             if project.host_connection()? == connection {
-                let room_id = project.room_id;
                 project::Entity::delete(project.into_active_model())
                     .exec(&*tx)
                     .await?;
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, (room, guest_connection_ids)))
+                Ok((room, guest_connection_ids))
             } else {
                 Err(anyhow!("cannot unshare a project hosted by another user"))?
             }
@@ -2061,7 +2135,8 @@ impl Database {
         connection: ConnectionId,
         worktrees: &[proto::WorktreeMetadata],
     ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let project = project::Entity::find_by_id(project_id)
                 .filter(
                     Condition::all()
@@ -2079,7 +2154,7 @@ impl Database {
 
             let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
             let room = self.get_room(project.room_id, &tx).await?;
-            Ok((project.room_id, (room, guest_connection_ids)))
+            Ok((room, guest_connection_ids))
         })
         .await
     }
@@ -2124,12 +2199,12 @@ impl Database {
         update: &proto::UpdateWorktree,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
-            let worktree_id = update.worktree_id as i64;
-
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
+            let _project = project::Entity::find_by_id(project_id)
                 .filter(
                     Condition::all()
                         .add(project::Column::HostConnectionId.eq(connection.id as i32))
@@ -2140,7 +2215,6 @@ impl Database {
                 .one(&*tx)
                 .await?
                 .ok_or_else(|| anyhow!("no such project"))?;
-            let room_id = project.room_id;
 
             // Update metadata.
             worktree::Entity::update(worktree::ActiveModel {
@@ -2220,7 +2294,7 @@ impl Database {
             }
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2230,9 +2304,10 @@ impl Database {
         update: &proto::UpdateDiagnosticSummary,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
-            let worktree_id = update.worktree_id as i64;
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let summary = update
                 .summary
                 .as_ref()
@@ -2274,7 +2349,7 @@ impl Database {
             .await?;
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((project.room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2284,8 +2359,9 @@ impl Database {
         update: &proto::StartLanguageServer,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let server = update
                 .server
                 .as_ref()
@@ -2319,7 +2395,7 @@ impl Database {
             .await?;
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((project.room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2329,7 +2405,8 @@ impl Database {
         project_id: ProjectId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<(Project, ReplicaId)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -2455,7 +2532,6 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
-            let room_id = project.room_id;
             let project = Project {
                 collaborators: collaborators
                     .into_iter()
@@ -2475,7 +2551,7 @@ impl Database {
                     })
                     .collect(),
             };
-            Ok((room_id, (project, replica_id as ReplicaId)))
+            Ok((project, replica_id as ReplicaId))
         })
         .await
     }
@@ -2485,7 +2561,8 @@ impl Database {
         project_id: ProjectId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<LeftProject>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let result = project_collaborator::Entity::delete_many()
                 .filter(
                     Condition::all()
@@ -2521,7 +2598,7 @@ impl Database {
                 host_connection_id: project.host_connection()?,
                 connection_ids,
             };
-            Ok((project.room_id, left_project))
+            Ok(left_project)
         })
         .await
     }
@@ -2531,11 +2608,8 @@ impl Database {
         project_id: ProjectId,
         connection_id: ConnectionId,
     ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
-        self.room_transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let collaborators = project_collaborator::Entity::find()
                 .filter(project_collaborator::Column::ProjectId.eq(project_id))
                 .all(&*tx)
@@ -2553,7 +2627,7 @@ impl Database {
                 .iter()
                 .any(|collaborator| collaborator.connection_id == connection_id)
             {
-                Ok((project.room_id, collaborators))
+                Ok(collaborators)
             } else {
                 Err(anyhow!("no such project"))?
             }
@@ -2566,11 +2640,8 @@ impl Database {
         project_id: ProjectId,
         connection_id: ConnectionId,
     ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let mut collaborators = project_collaborator::Entity::find()
                 .filter(project_collaborator::Column::ProjectId.eq(project_id))
                 .stream(&*tx)
@@ -2583,7 +2654,7 @@ impl Database {
             }
 
             if connection_ids.contains(&connection_id) {
-                Ok((project.room_id, connection_ids))
+                Ok(connection_ids)
             } else {
                 Err(anyhow!("no such project"))?
             }
@@ -2613,6 +2684,17 @@ impl Database {
         Ok(guest_connection_ids)
     }
 
+    async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
+            Ok(project.room_id)
+        })
+        .await
+    }
+
     // access tokens
 
     pub async fn create_access_token_hash(
@@ -2763,21 +2845,48 @@ impl Database {
         self.run(body).await
     }
 
-    async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
+    async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
     where
         F: Send + Fn(TransactionHandle) -> Fut,
-        Fut: Send + Future<Output = Result<(RoomId, T)>>,
+        Fut: Send + Future<Output = Result<T>>,
     {
-        let data = self
-            .optional_room_transaction(move |tx| {
-                let future = f(tx);
-                async {
-                    let data = future.await?;
-                    Ok(Some(data))
+        let body = async {
+            loop {
+                let lock = self.rooms.entry(room_id).or_default().clone();
+                let _guard = lock.lock_owned().await;
+                let (tx, result) = self.with_transaction(&f).await?;
+                match result {
+                    Ok(data) => {
+                        match tx.commit().await.map_err(Into::into) {
+                            Ok(()) => {
+                                return Ok(RoomGuard {
+                                    data,
+                                    _guard,
+                                    _not_send: PhantomData,
+                                });
+                            }
+                            Err(error) => {
+                                if is_serialization_error(&error) {
+                                    // Retry (don't break the loop)
+                                } else {
+                                    return Err(error);
+                                }
+                            }
+                        }
+                    }
+                    Err(error) => {
+                        tx.rollback().await?;
+                        if is_serialization_error(&error) {
+                            // Retry (don't break the loop)
+                        } else {
+                            return Err(error);
+                        }
+                    }
                 }
-            })
-            .await?;
-        Ok(data.unwrap())
+            }
+        };
+
+        self.run(body).await
     }
 
     async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
@@ -3011,6 +3120,7 @@ macro_rules! id_type {
 
 id_type!(AccessTokenId);
 id_type!(ContactId);
+id_type!(FollowerId);
 id_type!(RoomId);
 id_type!(RoomParticipantId);
 id_type!(ProjectId);

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

@@ -0,0 +1,51 @@
+use super::{FollowerId, ProjectId, RoomId, ServerId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+use serde::Serialize;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[sea_orm(table_name = "followers")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: FollowerId,
+    pub room_id: RoomId,
+    pub project_id: ProjectId,
+    pub leader_connection_server_id: ServerId,
+    pub leader_connection_id: i32,
+    pub follower_connection_server_id: ServerId,
+    pub follower_connection_id: i32,
+}
+
+impl Model {
+    pub fn leader_connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.leader_connection_server_id.0 as u32,
+            id: self.leader_connection_id as u32,
+        }
+    }
+
+    pub fn follower_connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.follower_connection_server_id.0 as u32,
+            id: self.follower_connection_id as u32,
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::room::Entity",
+        from = "Column::RoomId",
+        to = "super::room::Column::Id"
+    )]
+    Room,
+}
+
+impl Related<super::room::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Room.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -15,6 +15,8 @@ pub enum Relation {
     RoomParticipant,
     #[sea_orm(has_many = "super::project::Entity")]
     Project,
+    #[sea_orm(has_many = "super::follower::Entity")]
+    Follower,
 }
 
 impl Related<super::room_participant::Entity> for Entity {
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
     }
 }
 
+impl Related<super::follower::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Follower.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}

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

@@ -270,8 +270,11 @@ impl Server {
                         let mut live_kit_room = String::new();
                         let mut delete_live_kit_room = false;
 
-                        if let Ok(mut refreshed_room) =
-                            app_state.db.refresh_room(room_id, server_id).await
+                        if let Some(mut refreshed_room) = app_state
+                            .db
+                            .refresh_room(room_id, server_id)
+                            .await
+                            .trace_err()
                         {
                             tracing::info!(
                                 room_id = room_id.0,
@@ -1312,6 +1315,7 @@ async fn join_project(
         .filter(|collaborator| collaborator.connection_id != session.connection_id)
         .map(|collaborator| collaborator.to_proto())
         .collect::<Vec<_>>();
+
     let worktrees = project
         .worktrees
         .iter()
@@ -1724,6 +1728,7 @@ async fn follow(
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
+
     {
         let project_connection_ids = session
             .db()
@@ -1744,6 +1749,14 @@ async fn follow(
         .views
         .retain(|view| view.leader_id != Some(follower_id.into()));
     response.send(response_payload)?;
+
+    let room = session
+        .db()
+        .await
+        .follow(project_id, leader_id, follower_id)
+        .await?;
+    room_updated(&room, &session.peer);
+
     Ok(())
 }
 
@@ -1753,17 +1766,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
-    let project_connection_ids = session
+    let follower_id = session.connection_id;
+
+    if !session
         .db()
         .await
         .project_connection_ids(project_id, session.connection_id)
-        .await?;
-    if !project_connection_ids.contains(&leader_id) {
+        .await?
+        .contains(&leader_id)
+    {
         Err(anyhow!("no such peer"))?;
     }
+
     session
         .peer
         .forward_send(session.connection_id, leader_id, request)?;
+
+    let room = session
+        .db()
+        .await
+        .unfollow(project_id, leader_id, follower_id)
+        .await?;
+    room_updated(&room, &session.peer);
+
     Ok(())
 }
 

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

@@ -733,6 +733,14 @@ async fn test_server_restarts(
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
+    client_a
+        .fs
+        .insert_tree("/a", json!({ "a.txt": "a-contents" }))
+        .await;
+
+    // Invite client B to collaborate on a project
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+
     let client_b = server.create_client(cx_b, "user_b").await;
     let client_c = server.create_client(cx_c, "user_c").await;
     let client_d = server.create_client(cx_d, "user_d").await;
@@ -753,19 +761,19 @@ async fn test_server_restarts(
     // User A calls users B, C, and D.
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_b.user_id().unwrap(), None, cx)
+            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_c.user_id().unwrap(), None, cx)
+            call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_d.user_id().unwrap(), None, cx)
+            call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
@@ -1083,7 +1091,7 @@ async fn test_calls_on_multiple_connections(
     assert!(incoming_call_b2.next().await.unwrap().is_none());
 
     // User B disconnects the client that is not on the call. Everything should be fine.
-    client_b1.disconnect(&cx_b1.to_async()).unwrap();
+    client_b1.disconnect(&cx_b1.to_async());
     deterministic.advance_clock(RECEIVE_TIMEOUT);
     client_b1
         .authenticate_and_connect(false, &cx_b1.to_async())
@@ -3227,7 +3235,7 @@ async fn test_leaving_project(
     buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
 
     // Drop client B's connection and ensure client A and client C observe client B leaving.
-    client_b.disconnect(&cx_b.to_async()).unwrap();
+    client_b.disconnect(&cx_b.to_async());
     deterministic.advance_clock(RECONNECT_TIMEOUT);
     project_a.read_with(cx_a, |project, _| {
         assert_eq!(project.collaborators().len(), 1);
@@ -5772,7 +5780,7 @@ async fn test_contact_requests(
         .is_empty());
 
     async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
-        client.disconnect(&cx.to_async()).unwrap();
+        client.disconnect(&cx.to_async());
         client.clear_contacts(cx).await;
         client
             .authenticate_and_connect(false, &cx.to_async())
@@ -5786,6 +5794,7 @@ async fn test_following(
     deterministic: Arc<Deterministic>,
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
     cx_a.update(editor::init);
@@ -5794,9 +5803,13 @@ async fn test_following(
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
     server
         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
+        .await;
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
@@ -5827,8 +5840,10 @@ async fn test_following(
         .await
         .unwrap();
 
-    // Client A opens some editors.
     let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+
+    // Client A opens some editors.
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
@@ -5848,7 +5863,6 @@ async fn test_following(
         .unwrap();
 
     // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
     let editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -5858,29 +5872,97 @@ async fn test_following(
         .downcast::<Editor>()
         .unwrap();
 
-    let client_a_id = project_b.read_with(cx_b, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-    let client_b_id = project_a.read_with(cx_a, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
+    let peer_id_a = client_a.peer_id().unwrap();
+    let peer_id_b = client_b.peer_id().unwrap();
+    let peer_id_c = client_c.peer_id().unwrap();
 
-    // When client B starts following client A, all visible view states are replicated to client B.
+    // Client A updates their selections in those editors
     editor_a1.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
     });
     editor_a2.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
     });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
     workspace_b
         .update(cx_b, |workspace, cx| {
             workspace
-                .toggle_follow(&ToggleFollow(client_a_id), cx)
+                .toggle_follow(&ToggleFollow(peer_id_a), cx)
                 .unwrap()
         })
         .await
         .unwrap();
 
+    // Client A invites client C to the call.
+    active_call_a
+        .update(cx_a, |call, cx| {
+            call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
+        })
+        .await
+        .unwrap();
+    cx_c.foreground().run_until_parked();
+    let active_call_c = cx_c.read(ActiveCall::global);
+    active_call_c
+        .update(cx_c, |call, cx| call.accept_incoming(cx))
+        .await
+        .unwrap();
+    let project_c = client_c.build_remote_project(project_id, cx_c).await;
+    let workspace_c = client_c.build_workspace(&project_c, cx_c);
+    active_call_c
+        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+        .await
+        .unwrap();
+
+    // Client C also follows client A.
+    workspace_c
+        .update(cx_c, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(peer_id_a), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C unfollows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+    });
+
+    // All clients see that clients B is following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
     let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
         workspace
             .active_item(cx)
@@ -6033,14 +6115,14 @@ async fn test_following(
     workspace_a
         .update(cx_a, |workspace, cx| {
             workspace
-                .toggle_follow(&ToggleFollow(client_b_id), cx)
+                .toggle_follow(&ToggleFollow(peer_id_b), cx)
                 .unwrap()
         })
         .await
         .unwrap();
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        Some(client_b_id)
+        Some(peer_id_b)
     );
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -6112,7 +6194,7 @@ async fn test_following(
     );
 
     // Following interrupts when client B disconnects.
-    client_b.disconnect(&cx_b.to_async()).unwrap();
+    client_b.disconnect(&cx_b.to_async());
     deterministic.advance_clock(RECONNECT_TIMEOUT);
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),

crates/collab_ui/Cargo.toml πŸ”—

@@ -27,6 +27,7 @@ call = { path = "../call" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -1,33 +1,60 @@
-use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing};
-use call::{ActiveCall, ParticipantLocation};
-use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
+use crate::{
+    collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
+    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+    ToggleScreenSharing,
+};
+use call::{ActiveCall, ParticipantLocation, Room};
+use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore};
 use clock::ReplicaId;
 use contacts_popover::ContactsPopover;
+use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     actions,
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
+    impl_internal_actions,
     json::{self, ToJson},
-    Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+    CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
     Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use settings::Settings;
-use std::ops::Range;
-use theme::Theme;
+use std::{ops::Range, sync::Arc};
+use theme::{AvatarStyle, Theme};
+use util::ResultExt;
 use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
 
-actions!(collab, [ToggleCollaborationMenu, ShareProject]);
+actions!(
+    collab,
+    [
+        ToggleCollaboratorList,
+        ToggleContactsMenu,
+        ToggleUserMenu,
+        ShareProject,
+        UnshareProject,
+    ]
+);
+
+impl_internal_actions!(collab, [LeaveCall]);
+
+#[derive(Copy, Clone, PartialEq)]
+pub(crate) struct LeaveCall;
 
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
     cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
     cx.add_action(CollabTitlebarItem::share_project);
+    cx.add_action(CollabTitlebarItem::unshare_project);
+    cx.add_action(CollabTitlebarItem::leave_call);
+    cx.add_action(CollabTitlebarItem::toggle_user_menu);
 }
 
 pub struct CollabTitlebarItem {
     workspace: WeakViewHandle<Workspace>,
     user_store: ModelHandle<UserStore>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
+    user_menu: ViewHandle<ContextMenu>,
+    collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -47,27 +74,61 @@ impl View for CollabTitlebarItem {
             return Empty::new().boxed();
         };
 
+        let project = workspace.read(cx).project().read(cx);
+        let mut project_title = String::new();
+        for (i, name) in project.worktree_root_names(cx).enumerate() {
+            if i > 0 {
+                project_title.push_str(", ");
+            }
+            project_title.push_str(name);
+        }
+        if project_title.is_empty() {
+            project_title = "empty project".to_owned();
+        }
+
         let theme = cx.global::<Settings>().theme.clone();
 
-        let mut container = Flex::row();
+        let mut left_container = Flex::row();
+        let mut right_container = Flex::row();
 
-        container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
+        left_container.add_child(
+            Label::new(project_title, theme.workspace.titlebar.title.clone())
+                .contained()
+                .with_margin_right(theme.workspace.titlebar.item_spacing)
+                .aligned()
+                .left()
+                .boxed(),
+        );
 
-        if workspace.read(cx).client().status().borrow().is_connected() {
-            let project = workspace.read(cx).project().read(cx);
-            if project.is_shared()
-                || project.is_remote()
-                || ActiveCall::global(cx).read(cx).room().is_none()
-            {
-                container.add_child(self.render_toggle_contacts_button(&theme, cx));
-            } else {
-                container.add_child(self.render_share_button(&theme, cx));
-            }
+        let user = workspace.read(cx).user_store().read(cx).current_user();
+        let peer_id = workspace.read(cx).client().peer_id();
+        if let Some(((user, peer_id), room)) = user
+            .zip(peer_id)
+            .zip(ActiveCall::global(cx).read(cx).room().cloned())
+        {
+            left_container
+                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+
+            right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+            right_container
+                .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
+            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+        }
+
+        let status = workspace.read(cx).client().status();
+        let status = &*status.borrow();
+        if matches!(status, client::Status::Connected { .. }) {
+            right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
+        } else {
+            right_container.add_children(self.render_connection_status(status, cx));
         }
-        container.add_children(self.render_collaborators(&workspace, &theme, cx));
-        container.add_children(self.render_current_user(&workspace, &theme, cx));
-        container.add_children(self.render_connection_status(&workspace, cx));
-        container.boxed()
+
+        right_container.add_child(self.render_user_menu_button(&theme, cx));
+
+        Stack::new()
+            .with_child(left_container.boxed())
+            .with_child(right_container.aligned().right().boxed())
+            .boxed()
     }
 }
 
@@ -80,7 +141,7 @@ impl CollabTitlebarItem {
         let active_call = ActiveCall::global(cx);
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
-        subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
         subscriptions.push(cx.observe_window_activation(|this, active, cx| {
             this.window_activation_changed(active, cx)
         }));
@@ -112,6 +173,12 @@ impl CollabTitlebarItem {
             workspace: workspace.downgrade(),
             user_store: user_store.clone(),
             contacts_popover: None,
+            user_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
+            collaborator_list_popover: None,
             _subscriptions: subscriptions,
         }
     }
@@ -129,6 +196,13 @@ impl CollabTitlebarItem {
         }
     }
 
+    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
+        if ActiveCall::global(cx).read(cx).room().is_none() {
+            self.contacts_popover = None;
+        }
+        cx.notify();
+    }
+
     fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let active_call = ActiveCall::global(cx);
@@ -139,41 +213,135 @@ impl CollabTitlebarItem {
         }
     }
 
-    pub fn toggle_contacts_popover(
+    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let active_call = ActiveCall::global(cx);
+            let project = workspace.read(cx).project().clone();
+            active_call
+                .update(cx, |call, cx| call.unshare_project(project, cx))
+                .log_err();
+        }
+    }
+
+    pub fn toggle_collaborator_list_popover(
         &mut self,
-        _: &ToggleCollaborationMenu,
+        _: &ToggleCollaboratorList,
         cx: &mut ViewContext<Self>,
     ) {
-        match self.contacts_popover.take() {
+        match self.collaborator_list_popover.take() {
             Some(_) => {}
             None => {
                 if let Some(workspace) = self.workspace.upgrade(cx) {
-                    let project = workspace.read(cx).project().clone();
                     let user_store = workspace.read(cx).user_store().clone();
-                    let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                    let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
+
                     cx.subscribe(&view, |this, _, event, cx| {
                         match event {
-                            contacts_popover::Event::Dismissed => {
-                                this.contacts_popover = None;
+                            collaborator_list_popover::Event::Dismissed => {
+                                this.collaborator_list_popover = None;
                             }
                         }
 
                         cx.notify();
                     })
                     .detach();
-                    self.contacts_popover = Some(view);
+
+                    self.collaborator_list_popover = Some(view);
                 }
             }
         }
         cx.notify();
     }
 
+    pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
+        if self.contacts_popover.take().is_none() {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                let project = workspace.read(cx).project().clone();
+                let user_store = workspace.read(cx).user_store().clone();
+                let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                cx.subscribe(&view, |this, _, event, cx| {
+                    match event {
+                        contacts_popover::Event::Dismissed => {
+                            this.contacts_popover = None;
+                        }
+                    }
+
+                    cx.notify();
+                })
+                .detach();
+                self.contacts_popover = Some(view);
+            }
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
+        let theme = cx.global::<Settings>().theme.clone();
+        let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
+        let item_style = theme.context_menu.item.disabled_style().clone();
+        self.user_menu.update(cx, |user_menu, cx| {
+            let items = if let Some(user) = self.user_store.read(cx).current_user() {
+                vec![
+                    ContextMenuItem::Static(Box::new(move |_| {
+                        Flex::row()
+                            .with_children(user.avatar.clone().map(|avatar| {
+                                Self::render_face(
+                                    avatar,
+                                    avatar_style.clone(),
+                                    Color::transparent_black(),
+                                )
+                            }))
+                            .with_child(
+                                Label::new(user.github_login.clone(), item_style.label.clone())
+                                    .boxed(),
+                            )
+                            .contained()
+                            .with_style(item_style.container)
+                            .boxed()
+                    })),
+                    ContextMenuItem::Item {
+                        label: "Sign out".into(),
+                        action: Box::new(SignOut),
+                    },
+                ]
+            } else {
+                vec![ContextMenuItem::Item {
+                    label: "Sign in".into(),
+                    action: Box::new(Authenticate),
+                }]
+            };
+
+            user_menu.show(
+                vec2f(
+                    theme
+                        .workspace
+                        .titlebar
+                        .user_menu_button
+                        .default
+                        .button_width,
+                    theme.workspace.titlebar.height,
+                ),
+                AnchorCorner::TopRight,
+                items,
+                cx,
+            );
+        });
+    }
+
+    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.hang_up(cx))
+            .log_err();
+    }
+
     fn render_toggle_contacts_button(
         &self,
         theme: &Theme,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         let titlebar = &theme.workspace.titlebar;
+
         let badge = if self
             .user_store
             .read(cx)
@@ -194,9 +362,10 @@ impl CollabTitlebarItem {
                     .boxed(),
             )
         };
+
         Stack::new()
             .with_child(
-                MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
+                MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
                     let style = titlebar
                         .toggle_contacts_button
                         .style_for(state, self.contacts_popover.is_some());
@@ -214,39 +383,31 @@ impl CollabTitlebarItem {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
-                    cx.dispatch_action(ToggleCollaborationMenu);
+                    cx.dispatch_action(ToggleContactsMenu);
                 })
+                .with_tooltip::<ToggleContactsMenu, _>(
+                    0,
+                    "Show contacts menu".into(),
+                    Some(Box::new(ToggleContactsMenu)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
                 .aligned()
                 .boxed(),
             )
             .with_children(badge)
-            .with_children(self.contacts_popover.as_ref().map(|popover| {
-                Overlay::new(
-                    ChildView::new(popover, cx)
-                        .contained()
-                        .with_margin_top(titlebar.height)
-                        .with_margin_left(titlebar.toggle_contacts_button.default.button_width)
-                        .with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
-                        .boxed(),
-                )
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::BottomLeft)
-                .with_z_index(999)
-                .boxed()
-            }))
+            .with_children(self.render_contacts_popover_host(titlebar, cx))
             .boxed()
     }
 
     fn render_toggle_screen_sharing_button(
         &self,
         theme: &Theme,
+        room: &ModelHandle<Room>,
         cx: &mut RenderContext<Self>,
-    ) -> Option<ElementBox> {
-        let active_call = ActiveCall::global(cx);
-        let room = active_call.read(cx).room().cloned()?;
+    ) -> ElementBox {
         let icon;
         let tooltip;
-
         if room.read(cx).is_screen_sharing() {
             icon = "icons/disable_screen_sharing_12.svg";
             tooltip = "Stop Sharing Screen"
@@ -256,226 +417,368 @@ impl CollabTitlebarItem {
         }
 
         let titlebar = &theme.workspace.titlebar;
-        Some(
-            MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
-                let style = titlebar.call_control.style_for(state, false);
-                Svg::new(icon)
-                    .with_color(style.color)
-                    .constrained()
-                    .with_width(style.icon_width)
-                    .aligned()
-                    .constrained()
-                    .with_width(style.button_width)
-                    .with_height(style.button_width)
-                    .contained()
-                    .with_style(style.container)
-                    .boxed()
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, cx| {
-                cx.dispatch_action(ToggleScreenSharing);
-            })
-            .with_tooltip::<ToggleScreenSharing, _>(
-                0,
-                tooltip.into(),
-                Some(Box::new(ToggleScreenSharing)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .aligned()
-            .boxed(),
-        )
-    }
-
-    fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
-        enum Share {}
-
-        let titlebar = &theme.workspace.titlebar;
-        MouseEventHandler::<Share>::new(0, cx, |state, _| {
-            let style = titlebar.share_button.style_for(state, false);
-            Label::new("Share", style.text.clone())
+        MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
+            let style = titlebar.call_control.style_for(state, false);
+            Svg::new(icon)
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
                 .contained()
                 .with_style(style.container)
                 .boxed()
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
-        .with_tooltip::<Share, _>(
+        .on_click(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(ToggleScreenSharing);
+        })
+        .with_tooltip::<ToggleScreenSharing, _>(
             0,
-            "Share project with call participants".into(),
-            None,
+            tooltip.into(),
+            Some(Box::new(ToggleScreenSharing)),
             theme.tooltip.clone(),
             cx,
         )
         .aligned()
-        .contained()
-        .with_margin_left(theme.workspace.titlebar.avatar_margin)
         .boxed()
     }
 
+    fn render_in_call_share_unshare_button(
+        &self,
+        workspace: &ViewHandle<Workspace>,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        let project = workspace.read(cx).project();
+        if project.read(cx).is_remote() {
+            return None;
+        }
+
+        let is_shared = project.read(cx).is_shared();
+        let label = if is_shared { "Unshare" } else { "Share" };
+        let tooltip = if is_shared {
+            "Unshare project from call participants"
+        } else {
+            "Share project with call participants"
+        };
+
+        let titlebar = &theme.workspace.titlebar;
+
+        enum ShareUnshare {}
+        Some(
+            Stack::new()
+                .with_child(
+                    MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
+                        //TODO: Ensure this button has consistant width for both text variations
+                        let style = titlebar
+                            .share_button
+                            .style_for(state, self.contacts_popover.is_some());
+                        Label::new(label, style.text.clone())
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_click(MouseButton::Left, move |_, cx| {
+                        if is_shared {
+                            cx.dispatch_action(UnshareProject);
+                        } else {
+                            cx.dispatch_action(ShareProject);
+                        }
+                    })
+                    .with_tooltip::<ShareUnshare, _>(
+                        0,
+                        tooltip.to_owned(),
+                        None,
+                        theme.tooltip.clone(),
+                        cx,
+                    )
+                    .boxed(),
+                )
+                .aligned()
+                .contained()
+                .with_margin_left(theme.workspace.titlebar.item_spacing)
+                .boxed(),
+        )
+    }
+
+    fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+        let titlebar = &theme.workspace.titlebar;
+
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
+                    let style = titlebar.call_control.style_for(state, false);
+                    Svg::new("icons/ellipsis_14.svg")
+                        .with_color(style.color)
+                        .constrained()
+                        .with_width(style.icon_width)
+                        .aligned()
+                        .constrained()
+                        .with_width(style.button_width)
+                        .with_height(style.button_width)
+                        .contained()
+                        .with_style(style.container)
+                        .boxed()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(ToggleUserMenu);
+                })
+                .with_tooltip::<ToggleUserMenu, _>(
+                    0,
+                    "Toggle user menu".to_owned(),
+                    Some(Box::new(ToggleUserMenu)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .contained()
+                .with_margin_left(theme.workspace.titlebar.item_spacing)
+                .aligned()
+                .boxed(),
+            )
+            .with_child(ChildView::new(&self.user_menu, cx).boxed())
+            .boxed()
+    }
+
+    fn render_contacts_popover_host<'a>(
+        &'a self,
+        theme: &'a theme::Titlebar,
+        cx: &'a RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        self.contacts_popover.as_ref().map(|popover| {
+            Overlay::new(
+                ChildView::new(popover, cx)
+                    .contained()
+                    .with_margin_top(theme.height)
+                    .with_margin_left(theme.toggle_contacts_button.default.button_width)
+                    .with_margin_right(-theme.toggle_contacts_button.default.button_width)
+                    .boxed(),
+            )
+            .with_fit_mode(OverlayFitMode::SwitchAnchor)
+            .with_anchor_corner(AnchorCorner::BottomLeft)
+            .with_z_index(999)
+            .boxed()
+        })
+    }
+
     fn render_collaborators(
         &self,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
+        room: &ModelHandle<Room>,
         cx: &mut RenderContext<Self>,
     ) -> Vec<ElementBox> {
-        let active_call = ActiveCall::global(cx);
-        if let Some(room) = active_call.read(cx).room().cloned() {
-            let project = workspace.read(cx).project().read(cx);
-            let mut participants = room
-                .read(cx)
-                .remote_participants()
-                .values()
-                .cloned()
-                .collect::<Vec<_>>();
-            participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
-            participants
-                .into_iter()
-                .filter_map(|participant| {
-                    let project = workspace.read(cx).project().read(cx);
-                    let replica_id = project
-                        .collaborators()
-                        .get(&participant.peer_id)
-                        .map(|collaborator| collaborator.replica_id);
-                    let user = participant.user.clone();
-                    Some(self.render_avatar(
+        let project = workspace.read(cx).project().read(cx);
+
+        let mut participants = room
+            .read(cx)
+            .remote_participants()
+            .values()
+            .cloned()
+            .collect::<Vec<_>>();
+        participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
+
+        participants
+            .into_iter()
+            .filter_map(|participant| {
+                let project = workspace.read(cx).project().read(cx);
+                let replica_id = project
+                    .collaborators()
+                    .get(&participant.peer_id)
+                    .map(|collaborator| collaborator.replica_id);
+                let user = participant.user.clone();
+                Some(
+                    Container::new(self.render_face_pile(
                         &user,
                         replica_id,
-                        Some((
-                            participant.peer_id,
-                            &user.github_login,
-                            participant.location,
-                        )),
+                        participant.peer_id,
+                        Some(participant.location),
                         workspace,
                         theme,
                         cx,
                     ))
-                })
-                .collect()
-        } else {
-            Default::default()
-        }
+                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
+                    .boxed(),
+                )
+            })
+            .collect()
     }
 
     fn render_current_user(
         &self,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
+        user: &Arc<User>,
+        peer_id: PeerId,
         cx: &mut RenderContext<Self>,
-    ) -> Option<ElementBox> {
-        let user = workspace.read(cx).user_store().read(cx).current_user();
+    ) -> ElementBox {
         let replica_id = workspace.read(cx).project().read(cx).replica_id();
-        let status = *workspace.read(cx).client().status().borrow();
-        if let Some(user) = user {
-            Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
-        } else if matches!(status, client::Status::UpgradeRequired) {
-            None
-        } else {
-            Some(
-                MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
-                    let style = theme
-                        .workspace
-                        .titlebar
-                        .sign_in_prompt
-                        .style_for(state, false);
-                    Label::new("Sign in", style.text.clone())
-                        .contained()
-                        .with_style(style.container)
-                        .boxed()
-                })
-                .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
-                .with_cursor_style(CursorStyle::PointingHand)
-                .aligned()
-                .boxed(),
-            )
-        }
+        Container::new(self.render_face_pile(
+            user,
+            Some(replica_id),
+            peer_id,
+            None,
+            workspace,
+            theme,
+            cx,
+        ))
+        .with_margin_right(theme.workspace.titlebar.item_spacing)
+        .boxed()
     }
 
-    fn render_avatar(
+    fn render_face_pile(
         &self,
         user: &User,
         replica_id: Option<ReplicaId>,
-        peer: Option<(PeerId, &str, ParticipantLocation)>,
+        peer_id: PeerId,
+        location: Option<ParticipantLocation>,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
-        let is_followed = peer.map_or(false, |(peer_id, _, _)| {
-            workspace.read(cx).is_following(peer_id)
-        });
+        let project_id = workspace.read(cx).project().read(cx).remote_id();
+        let room = ActiveCall::global(cx).read(cx).room();
+        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
+        let followed_by_self = room
+            .and_then(|room| {
+                Some(
+                    is_being_followed
+                        && room
+                            .read(cx)
+                            .followers_for(peer_id, project_id?)
+                            .iter()
+                            .any(|&follower| {
+                                Some(follower) == workspace.read(cx).client().peer_id()
+                            }),
+                )
+            })
+            .unwrap_or(false);
 
-        let mut avatar_style;
-        if let Some((_, _, location)) = peer.as_ref() {
-            if let ParticipantLocation::SharedProject { project_id } = *location {
-                if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
-                    avatar_style = theme.workspace.titlebar.avatar;
-                } else {
-                    avatar_style = theme.workspace.titlebar.inactive_avatar;
-                }
-            } else {
-                avatar_style = theme.workspace.titlebar.inactive_avatar;
-            }
-        } else {
-            avatar_style = theme.workspace.titlebar.avatar;
-        }
+        let leader_style = theme.workspace.titlebar.leader_avatar;
+        let follower_style = theme.workspace.titlebar.follower_avatar;
 
-        let mut replica_color = None;
+        let mut background_color = theme
+            .workspace
+            .titlebar
+            .container
+            .background_color
+            .unwrap_or_default();
         if let Some(replica_id) = replica_id {
-            let color = theme.editor.replica_selection_style(replica_id).cursor;
-            replica_color = Some(color);
-            if is_followed {
-                avatar_style.border = Border::all(1.0, color);
+            if followed_by_self {
+                let selection = theme.editor.replica_selection_style(replica_id).selection;
+                background_color = Color::blend(selection, background_color);
+                background_color.a = 255;
             }
         }
 
-        let content = Stack::new()
+        let mut content = Stack::new()
             .with_children(user.avatar.as_ref().map(|avatar| {
-                Image::new(avatar.clone())
-                    .with_style(avatar_style)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_width)
-                    .aligned()
-                    .boxed()
-            }))
-            .with_children(replica_color.map(|replica_color| {
-                AvatarRibbon::new(replica_color)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_ribbon.width)
-                    .with_height(theme.workspace.titlebar.avatar_ribbon.height)
-                    .aligned()
-                    .bottom()
-                    .boxed()
+                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
+                    .with_child(Self::render_face(
+                        avatar.clone(),
+                        Self::location_style(workspace, location, leader_style, cx),
+                        background_color,
+                    ))
+                    .with_children(
+                        (|| {
+                            let project_id = project_id?;
+                            let room = room?.read(cx);
+                            let followers = room.followers_for(peer_id, project_id);
+
+                            Some(followers.into_iter().flat_map(|&follower| {
+                                let remote_participant =
+                                    room.remote_participant_for_peer_id(follower);
+
+                                let avatar = remote_participant
+                                    .and_then(|p| p.user.avatar.clone())
+                                    .or_else(|| {
+                                        if follower == workspace.read(cx).client().peer_id()? {
+                                            workspace
+                                                .read(cx)
+                                                .user_store()
+                                                .read(cx)
+                                                .current_user()?
+                                                .avatar
+                                                .clone()
+                                        } else {
+                                            None
+                                        }
+                                    })?;
+
+                                let location = remote_participant.map(|p| p.location);
+
+                                Some(Self::render_face(
+                                    avatar.clone(),
+                                    Self::location_style(workspace, location, follower_style, cx),
+                                    background_color,
+                                ))
+                            }))
+                        })()
+                        .into_iter()
+                        .flatten(),
+                    );
+
+                let mut container = face_pile
+                    .contained()
+                    .with_style(theme.workspace.titlebar.leader_selection);
+
+                if let Some(replica_id) = replica_id {
+                    if followed_by_self {
+                        let color = theme.editor.replica_selection_style(replica_id).selection;
+                        container = container.with_background_color(color);
+                    }
+                }
+
+                container.boxed()
             }))
-            .constrained()
-            .with_width(theme.workspace.titlebar.avatar_width)
-            .contained()
-            .with_margin_left(theme.workspace.titlebar.avatar_margin)
+            .with_children((|| {
+                let replica_id = replica_id?;
+                let color = theme.editor.replica_selection_style(replica_id).cursor;
+                Some(
+                    AvatarRibbon::new(color)
+                        .constrained()
+                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
+                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+                        .aligned()
+                        .bottom()
+                        .boxed(),
+                )
+            })())
             .boxed();
 
-        if let Some((peer_id, peer_github_login, location)) = peer {
+        if let Some(location) = location {
             if let Some(replica_id) = replica_id {
-                MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
+                content =
+                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
+                        content
+                    })
                     .with_cursor_style(CursorStyle::PointingHand)
                     .on_click(MouseButton::Left, move |_, cx| {
                         cx.dispatch_action(ToggleFollow(peer_id))
                     })
                     .with_tooltip::<ToggleFollow, _>(
                         peer_id.as_u64() as usize,
-                        if is_followed {
-                            format!("Unfollow {}", peer_github_login)
+                        if is_being_followed {
+                            format!("Unfollow {}", user.github_login)
                         } else {
-                            format!("Follow {}", peer_github_login)
+                            format!("Follow {}", user.github_login)
                         },
                         Some(Box::new(FollowNextCollaborator)),
                         theme.tooltip.clone(),
                         cx,
                     )
-                    .boxed()
+                    .boxed();
             } else if let ParticipantLocation::SharedProject { project_id } = location {
                 let user_id = user.id;
-                MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
-                    content
-                })
+                content = MouseEventHandler::<JoinProject>::new(
+                    peer_id.as_u64() as usize,
+                    cx,
+                    move |_, _| content,
+                )
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
                     cx.dispatch_action(JoinProject {
@@ -485,29 +788,63 @@ impl CollabTitlebarItem {
                 })
                 .with_tooltip::<JoinProject, _>(
                     peer_id.as_u64() as usize,
-                    format!("Follow {} into external project", peer_github_login),
+                    format!("Follow {} into external project", user.github_login),
                     Some(Box::new(FollowNextCollaborator)),
                     theme.tooltip.clone(),
                     cx,
                 )
-                .boxed()
+                .boxed();
+            }
+        }
+        content
+    }
+
+    fn location_style(
+        workspace: &ViewHandle<Workspace>,
+        location: Option<ParticipantLocation>,
+        mut style: AvatarStyle,
+        cx: &RenderContext<Self>,
+    ) -> AvatarStyle {
+        if let Some(location) = location {
+            if let ParticipantLocation::SharedProject { project_id } = location {
+                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
+                    style.image.grayscale = true;
+                }
             } else {
-                content
+                style.image.grayscale = true;
             }
-        } else {
-            content
         }
+
+        style
+    }
+
+    fn render_face(
+        avatar: Arc<ImageData>,
+        avatar_style: AvatarStyle,
+        background_color: Color,
+    ) -> ElementBox {
+        Image::new(avatar)
+            .with_style(avatar_style.image)
+            .aligned()
+            .contained()
+            .with_background_color(background_color)
+            .with_corner_radius(avatar_style.outer_corner_radius)
+            .constrained()
+            .with_width(avatar_style.outer_width)
+            .with_height(avatar_style.outer_width)
+            .aligned()
+            .boxed()
     }
 
     fn render_connection_status(
         &self,
-        workspace: &ViewHandle<Workspace>,
+        status: &client::Status,
         cx: &mut RenderContext<Self>,
     ) -> Option<ElementBox> {
         enum ConnectionStatusButton {}
 
         let theme = &cx.global::<Settings>().theme.clone();
-        match &*workspace.read(cx).client().status().borrow() {
+        match status {
             client::Status::ConnectionError
             | client::Status::ConnectionLost
             | client::Status::Reauthenticating { .. }

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

@@ -1,8 +1,10 @@
 mod collab_titlebar_item;
+mod collaborator_list_popover;
 mod contact_finder;
 mod contact_list;
 mod contact_notification;
 mod contacts_popover;
+mod face_pile;
 mod incoming_call_notification;
 mod notifications;
 mod project_shared_notification;
@@ -10,7 +12,7 @@ mod sharing_status_indicator;
 
 use anyhow::anyhow;
 use call::ActiveCall;
-pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
+pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, MutableAppContext, Task};
 use std::sync::Arc;
 use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
@@ -116,7 +118,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
                     });
 
                 if let Some(follow_peer_id) = follow_peer_id {
-                    if !workspace.is_following(follow_peer_id) {
+                    if !workspace.is_being_followed(follow_peer_id) {
                         workspace
                             .toggle_follow(&ToggleFollow(follow_peer_id), cx)
                             .map(|follow| follow.detach_and_log_err(cx));

crates/collab_ui/src/collaborator_list_popover.rs πŸ”—

@@ -0,0 +1,165 @@
+use call::ActiveCall;
+use client::UserStore;
+use gpui::Action;
+use gpui::{
+    actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
+};
+use settings::Settings;
+
+use crate::collab_titlebar_item::ToggleCollaboratorList;
+
+pub(crate) enum Event {
+    Dismissed,
+}
+
+enum Collaborator {
+    SelfUser { username: String },
+    RemoteUser { username: String },
+}
+
+actions!(collaborator_list_popover, [NoOp]);
+
+pub(crate) struct CollaboratorListPopover {
+    list_state: ListState,
+}
+
+impl Entity for CollaboratorListPopover {
+    type Event = Event;
+}
+
+impl View for CollaboratorListPopover {
+    fn ui_name() -> &'static str {
+        "CollaboratorListPopover"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
+
+        MouseEventHandler::<Self>::new(0, cx, |_, _| {
+            List::new(self.list_state.clone())
+                .contained()
+                .with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
+                .constrained()
+                .with_width(theme.contacts_popover.width)
+                .with_height(theme.contacts_popover.height)
+                .boxed()
+        })
+        .on_down_out(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(ToggleCollaboratorList);
+        })
+        .boxed()
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
+}
+
+impl CollaboratorListPopover {
+    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let active_call = ActiveCall::global(cx);
+
+        let mut collaborators = user_store
+            .read(cx)
+            .current_user()
+            .map(|u| Collaborator::SelfUser {
+                username: u.github_login.clone(),
+            })
+            .into_iter()
+            .collect::<Vec<_>>();
+
+        //TODO: What should the canonical sort here look like, consult contacts list implementation
+        if let Some(room) = active_call.read(cx).room() {
+            for participant in room.read(cx).remote_participants() {
+                collaborators.push(Collaborator::RemoteUser {
+                    username: participant.1.user.github_login.clone(),
+                });
+            }
+        }
+
+        Self {
+            list_state: ListState::new(
+                collaborators.len(),
+                Orientation::Top,
+                0.,
+                cx,
+                move |_, index, cx| match &collaborators[index] {
+                    Collaborator::SelfUser { username } => render_collaborator_list_entry(
+                        index,
+                        username,
+                        None::<NoOp>,
+                        None,
+                        Svg::new("icons/chevron_right_12.svg"),
+                        NoOp,
+                        "Leave call".to_owned(),
+                        cx,
+                    ),
+
+                    Collaborator::RemoteUser { username } => render_collaborator_list_entry(
+                        index,
+                        username,
+                        Some(NoOp),
+                        Some(format!("Follow {username}")),
+                        Svg::new("icons/x_mark_12.svg"),
+                        NoOp,
+                        format!("Remove {username} from call"),
+                        cx,
+                    ),
+                },
+            ),
+        }
+    }
+}
+
+fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
+    index: usize,
+    username: &str,
+    username_action: Option<UA>,
+    username_tooltip: Option<String>,
+    icon: Svg,
+    icon_action: IA,
+    icon_tooltip: String,
+    cx: &mut RenderContext<CollaboratorListPopover>,
+) -> ElementBox {
+    enum Username {}
+    enum UsernameTooltip {}
+    enum Icon {}
+    enum IconTooltip {}
+
+    let theme = &cx.global::<Settings>().theme;
+    let username_theme = theme.contact_list.contact_username.text.clone();
+    let tooltip_theme = theme.tooltip.clone();
+
+    let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
+        Label::new(username.to_owned(), username_theme.clone()).boxed()
+    })
+    .on_click(MouseButton::Left, move |_, cx| {
+        if let Some(username_action) = username_action.clone() {
+            cx.dispatch_action(username_action);
+        }
+    });
+
+    Flex::row()
+        .with_child(if let Some(username_tooltip) = username_tooltip {
+            username
+                .with_tooltip::<UsernameTooltip, _>(
+                    index,
+                    username_tooltip,
+                    None,
+                    tooltip_theme.clone(),
+                    cx,
+                )
+                .boxed()
+        } else {
+            username.boxed()
+        })
+        .with_child(
+            MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(icon_action.clone())
+                })
+                .with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
+                .boxed(),
+        )
+        .boxed()
+}

crates/collab_ui/src/contact_list.rs πŸ”—

@@ -1,3 +1,4 @@
+use super::collab_titlebar_item::LeaveCall;
 use crate::contacts_popover;
 use call::ActiveCall;
 use client::{proto::PeerId, Contact, User, UserStore};
@@ -18,22 +19,20 @@ use serde::Deserialize;
 use settings::Settings;
 use std::{mem, sync::Arc};
 use theme::IconButton;
-use util::ResultExt;
 use workspace::{JoinProject, OpenSharedScreen};
 
 impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
-impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
+impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ContactList::remove_contact);
     cx.add_action(ContactList::respond_to_contact_request);
-    cx.add_action(ContactList::clear_filter);
+    cx.add_action(ContactList::cancel);
     cx.add_action(ContactList::select_next);
     cx.add_action(ContactList::select_prev);
     cx.add_action(ContactList::confirm);
     cx.add_action(ContactList::toggle_expanded);
     cx.add_action(ContactList::call);
-    cx.add_action(ContactList::leave_call);
 }
 
 #[derive(Clone, PartialEq)]
@@ -45,9 +44,6 @@ struct Call {
     initial_project: Option<ModelHandle<Project>>,
 }
 
-#[derive(Copy, Clone, PartialEq)]
-struct LeaveCall;
-
 #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 enum Section {
     ActiveCall,
@@ -326,7 +322,7 @@ impl ContactList {
             .detach();
     }
 
-    fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         let did_clear = self.filter_editor.update(cx, |editor, cx| {
             if editor.buffer().read(cx).len(cx) > 0 {
                 editor.set_text("", cx);
@@ -335,6 +331,7 @@ impl ContactList {
                 false
             }
         });
+
         if !did_clear {
             cx.emit(Event::Dismissed);
         }
@@ -980,6 +977,7 @@ impl ContactList {
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         enum Header {}
+        enum LeaveCallContactList {}
 
         let header_style = theme
             .header_row
@@ -992,9 +990,9 @@ impl ContactList {
         };
         let leave_call = if section == Section::ActiveCall {
             Some(
-                MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
+                MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
                     let style = theme.leave_call.style_for(state, false);
-                    Label::new("Leave Session", style.text.clone())
+                    Label::new("Leave Call", style.text.clone())
                         .contained()
                         .with_style(style.container)
                         .boxed()
@@ -1283,12 +1281,6 @@ impl ContactList {
             })
             .detach_and_log_err(cx);
     }
-
-    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.hang_up(cx))
-            .log_err();
-    }
 }
 
 impl Entity for ContactList {
@@ -1302,7 +1294,7 @@ impl View for ContactList {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 
@@ -1334,7 +1326,7 @@ impl View for ContactList {
                         })
                         .with_tooltip::<AddContact, _>(
                             0,
-                            "Add contact".into(),
+                            "Search for new contact".into(),
                             None,
                             theme.tooltip.clone(),
                             cx,

crates/collab_ui/src/contacts_popover.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
+use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
 use client::UserStore;
 use gpui::{
     actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
@@ -155,7 +155,7 @@ impl View for ContactsPopover {
                 .boxed()
         })
         .on_down_out(MouseButton::Left, move |_, cx| {
-            cx.dispatch_action(ToggleCollaborationMenu);
+            cx.dispatch_action(ToggleContactsMenu);
         })
         .boxed()
     }

crates/collab_ui/src/face_pile.rs πŸ”—

@@ -0,0 +1,101 @@
+use std::ops::Range;
+
+use gpui::{
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::ToJson,
+    serde_json::{self, json},
+    Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
+};
+
+pub(crate) struct FacePile {
+    overlap: f32,
+    faces: Vec<ElementBox>,
+}
+
+impl FacePile {
+    pub fn new(overlap: f32) -> FacePile {
+        FacePile {
+            overlap,
+            faces: Vec::new(),
+        }
+    }
+}
+
+impl Element for FacePile {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+
+        let mut width = 0.;
+        for face in &mut self.faces {
+            width += face.layout(constraint, cx).x();
+        }
+        width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+
+        (Vector2F::new(width, constraint.max.y()), ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _layout: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) -> Self::PaintState {
+        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+        let origin_y = bounds.upper_right().y();
+        let mut origin_x = bounds.upper_right().x();
+
+        for face in self.faces.iter_mut().rev() {
+            let size = face.size();
+            origin_x -= size.x();
+            cx.paint_layer(None, |cx| {
+                face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
+            });
+            origin_x += self.overlap;
+        }
+
+        ()
+    }
+
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "FacePile",
+            "bounds": bounds.to_json()
+        })
+    }
+}
+
+impl Extend<ElementBox> for FacePile {
+    fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
+        self.faces.extend(children);
+    }
+}

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

@@ -5,7 +5,9 @@ use gpui::{
 };
 use menu::*;
 use settings::Settings;
-use std::{any::TypeId, time::Duration};
+use std::{any::TypeId, borrow::Cow, time::Duration};
+
+pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
 
 #[derive(Copy, Clone, PartialEq)]
 struct Clicked;
@@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
 
 pub enum ContextMenuItem {
     Item {
-        label: String,
+        label: Cow<'static, str>,
         action: Box<dyn Action>,
     },
+    Static(StaticItem),
     Separator,
 }
 
 impl ContextMenuItem {
-    pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
+    pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
         Self::Item {
-            label: label.to_string(),
+            label: label.into(),
             action: Box::new(action),
         }
     }
@@ -42,14 +45,14 @@ impl ContextMenuItem {
         Self::Separator
     }
 
-    fn is_separator(&self) -> bool {
-        matches!(self, Self::Separator)
+    fn is_action(&self) -> bool {
+        matches!(self, Self::Item { .. })
     }
 
     fn action_id(&self) -> Option<TypeId> {
         match self {
             ContextMenuItem::Item { action, .. } => Some(action.id()),
-            ContextMenuItem::Separator => None,
+            ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
         }
     }
 }
@@ -58,6 +61,7 @@ pub struct ContextMenu {
     show_count: usize,
     anchor_position: Vector2F,
     anchor_corner: AnchorCorner,
+    position_mode: OverlayPositionMode,
     items: Vec<ContextMenuItem>,
     selected_index: Option<usize>,
     visible: bool,
@@ -78,7 +82,7 @@ impl View for ContextMenu {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 
@@ -105,6 +109,7 @@ impl View for ContextMenu {
             .with_fit_mode(OverlayFitMode::SnapToWindow)
             .with_anchor_position(self.anchor_position)
             .with_anchor_corner(self.anchor_corner)
+            .with_position_mode(self.position_mode)
             .boxed()
     }
 
@@ -121,6 +126,7 @@ impl ContextMenu {
             show_count: 0,
             anchor_position: Default::default(),
             anchor_corner: AnchorCorner::TopLeft,
+            position_mode: OverlayPositionMode::Window,
             items: Default::default(),
             selected_index: Default::default(),
             visible: Default::default(),
@@ -188,13 +194,13 @@ impl ContextMenu {
     }
 
     fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.selected_index = self.items.iter().position(|item| !item.is_separator());
+        self.selected_index = self.items.iter().position(|item| item.is_action());
         cx.notify();
     }
 
     fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
         for (ix, item) in self.items.iter().enumerate().rev() {
-            if !item.is_separator() {
+            if item.is_action() {
                 self.selected_index = Some(ix);
                 cx.notify();
                 break;
@@ -205,7 +211,7 @@ impl ContextMenu {
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
-                if !item.is_separator() {
+                if item.is_action() {
                     self.selected_index = Some(ix);
                     cx.notify();
                     break;
@@ -219,7 +225,7 @@ impl ContextMenu {
     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
-                if !item.is_separator() {
+                if item.is_action() {
                     self.selected_index = Some(ix);
                     cx.notify();
                     break;
@@ -234,7 +240,7 @@ impl ContextMenu {
         &mut self,
         anchor_position: Vector2F,
         anchor_corner: AnchorCorner,
-        items: impl IntoIterator<Item = ContextMenuItem>,
+        items: Vec<ContextMenuItem>,
         cx: &mut ViewContext<Self>,
     ) {
         let mut items = items.into_iter().peekable();
@@ -254,6 +260,10 @@ impl ContextMenu {
         cx.notify();
     }
 
+    pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
+        self.position_mode = mode;
+    }
+
     fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
         let window_id = cx.window_id();
         let style = cx.global::<Settings>().theme.context_menu.clone();
@@ -273,6 +283,9 @@ impl ContextMenu {
                                     .with_style(style.container)
                                     .boxed()
                             }
+
+                            ContextMenuItem::Static(f) => f(cx),
+
                             ContextMenuItem::Separator => Empty::new()
                                 .collapsed()
                                 .contained()
@@ -302,6 +315,9 @@ impl ContextMenu {
                                 )
                                 .boxed()
                             }
+
+                            ContextMenuItem::Static(_) => Empty::new().boxed(),
+
                             ContextMenuItem::Separator => Empty::new()
                                 .collapsed()
                                 .constrained()
@@ -339,7 +355,7 @@ impl ContextMenu {
 
                                 Flex::row()
                                     .with_child(
-                                        Label::new(label.to_string(), style.label.clone())
+                                        Label::new(label.clone(), style.label.clone())
                                             .contained()
                                             .boxed(),
                                     )
@@ -366,6 +382,9 @@ impl ContextMenu {
                             .on_drag(MouseButton::Left, |_, _| {})
                             .boxed()
                         }
+
+                        ContextMenuItem::Static(f) => f(cx),
+
                         ContextMenuItem::Separator => Empty::new()
                             .constrained()
                             .with_height(1.)

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

@@ -5071,7 +5071,7 @@ impl Editor {
             GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
         });
 
-        cx.spawn(|workspace, mut cx| async move {
+        cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
             let definitions = definitions.await?;
             workspace.update(&mut cx, |workspace, cx| {
                 Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
@@ -5151,31 +5151,36 @@ impl Editor {
 
         let project = workspace.project().clone();
         let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
-        Some(cx.spawn(|workspace, mut cx| async move {
-            let locations = references.await?;
-            if locations.is_empty() {
-                return Ok(());
-            }
+        Some(cx.spawn_labeled(
+            "Finding All References...",
+            |workspace, mut cx| async move {
+                let locations = references.await?;
+                if locations.is_empty() {
+                    return Ok(());
+                }
 
-            workspace.update(&mut cx, |workspace, cx| {
-                let title = locations
-                    .first()
-                    .as_ref()
-                    .map(|location| {
-                        let buffer = location.buffer.read(cx);
-                        format!(
-                            "References to `{}`",
-                            buffer
-                                .text_for_range(location.range.clone())
-                                .collect::<String>()
-                        )
-                    })
-                    .unwrap();
-                Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
-            });
+                workspace.update(&mut cx, |workspace, cx| {
+                    let title = locations
+                        .first()
+                        .as_ref()
+                        .map(|location| {
+                            let buffer = location.buffer.read(cx);
+                            format!(
+                                "References to `{}`",
+                                buffer
+                                    .text_for_range(location.range.clone())
+                                    .collect::<String>()
+                            )
+                        })
+                        .unwrap();
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, cx,
+                    );
+                });
 
-            Ok(())
-        }))
+                Ok(())
+            },
+        ))
     }
 
     /// Opens a multibuffer with the given project locations in it
@@ -5454,21 +5459,20 @@ impl Editor {
             None => return None,
         };
 
-        Some(self.perform_format(project, cx))
+        Some(self.perform_format(project, FormatTrigger::Manual, cx))
     }
 
     fn perform_format(
         &mut self,
         project: ModelHandle<Project>,
+        trigger: FormatTrigger,
         cx: &mut ViewContext<'_, Self>,
     ) -> Task<Result<()>> {
         let buffer = self.buffer().clone();
         let buffers = buffer.read(cx).all_buffers();
 
         let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
-        let format = project.update(cx, |project, cx| {
-            project.format(buffers, true, FormatTrigger::Manual, cx)
-        });
+        let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
 
         cx.spawn(|_, mut cx| async move {
             let transaction = futures::select_biased! {
@@ -6428,17 +6432,13 @@ impl View for Editor {
             EditorMode::AutoHeight { .. } => "auto_height",
             EditorMode::Full => "full",
         };
-        context.map.insert("mode".into(), mode.into());
+        context.add_key("mode", mode);
         if self.pending_rename.is_some() {
-            context.set.insert("renaming".into());
+            context.add_identifier("renaming");
         }
         match self.context_menu.as_ref() {
-            Some(ContextMenu::Completions(_)) => {
-                context.set.insert("showing_completions".into());
-            }
-            Some(ContextMenu::CodeActions(_)) => {
-                context.set.insert("showing_code_actions".into());
-            }
+            Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
+            Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
             None => {}
         }
 

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

@@ -4193,7 +4193,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
 
-    let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
     fake_server
         .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
             assert_eq!(
@@ -4225,7 +4227,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
         futures::future::pending::<()>().await;
         unreachable!()
     });
-    let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project, FormatTrigger::Manual, cx)
+    });
     cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
     cx.foreground().start_waiting();
     format.await.unwrap();

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

@@ -14,7 +14,7 @@ use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
     SelectionGoal,
 };
-use project::{Item as _, Project, ProjectPath};
+use project::{FormatTrigger, Item as _, Project, ProjectPath};
 use rpc::proto::{self, update_view};
 use settings::Settings;
 use smallvec::SmallVec;
@@ -608,7 +608,7 @@ impl Item for Editor {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         self.report_event("save editor", cx);
-        let format = self.perform_format(project.clone(), cx);
+        let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
         let buffers = self.buffer().clone().read(cx).all_buffers();
         cx.as_mut().spawn(|mut cx| async move {
             format.await?;

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

@@ -39,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
             pane::init(cx);
         });
 
-        let params = cx.update(AppState::test);
+        let app_state = cx.update(AppState::test);
 
         let file_name = format!(
             "file.{}",
@@ -56,10 +56,10 @@ impl<'a> EditorLspTestContext<'a> {
             }))
             .await;
 
-        let project = Project::test(params.fs.clone(), [], cx).await;
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
-        params
+        app_state
             .fs
             .as_fake()
             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))

crates/feedback/src/feedback_editor.rs πŸ”—

@@ -13,7 +13,6 @@ use gpui::{
     elements::{ChildView, Flex, Label, ParentElement},
     serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
     MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle,
 };
 use isahc::Request;
 use language::Buffer;
@@ -24,7 +23,6 @@ use serde::Serialize;
 use workspace::{
     item::{Item, ItemHandle},
     searchable::{SearchableItem, SearchableItemHandle},
-    smallvec::SmallVec,
     AppState, Workspace,
 };
 
@@ -259,16 +257,10 @@ impl Item for FeedbackEditor {
         self.editor.for_each_project_item(cx, f)
     }
 
-    fn to_item_events(_: &Self::Event) -> SmallVec<[workspace::item::ItemEvent; 2]> {
-        SmallVec::new()
-    }
-
     fn is_singleton(&self, _: &AppContext) -> bool {
         true
     }
 
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
     fn can_save(&self, _: &AppContext) -> bool {
         true
     }
@@ -295,7 +287,7 @@ impl Item for FeedbackEditor {
         _: ModelHandle<Project>,
         _: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        unreachable!("reload should not have been called")
+        Task::Ready(Some(Ok(())))
     }
 
     fn clone_on_split(
@@ -322,20 +314,6 @@ impl Item for FeedbackEditor {
         ))
     }
 
-    fn serialized_item_kind() -> Option<&'static str> {
-        None
-    }
-
-    fn deserialize(
-        _: ModelHandle<Project>,
-        _: WeakViewHandle<Workspace>,
-        _: workspace::WorkspaceId,
-        _: workspace::ItemId,
-        _: &mut ViewContext<workspace::Pane>,
-    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        unreachable!()
-    }
-
     fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(handle.clone()))
     }

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

@@ -23,6 +23,7 @@ pub struct FileFinder {
     latest_search_id: usize,
     latest_search_did_cancel: bool,
     latest_search_query: String,
+    relative_to: Option<Arc<Path>>,
     matches: Vec<PathMatch>,
     selected: Option<(usize, Arc<Path>)>,
     cancel_flag: Arc<AtomicBool>,
@@ -90,7 +91,11 @@ impl FileFinder {
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
         workspace.toggle_modal(cx, |workspace, cx| {
             let project = workspace.project().clone();
-            let finder = cx.add_view(|cx| Self::new(project, cx));
+            let relative_to = workspace
+                .active_item(cx)
+                .and_then(|item| item.project_path(cx))
+                .map(|project_path| project_path.path.clone());
+            let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
             cx.subscribe(&finder, Self::on_event).detach();
             finder
         });
@@ -115,7 +120,11 @@ impl FileFinder {
         }
     }
 
-    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        relative_to: Option<Arc<Path>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let handle = cx.weak_handle();
         cx.observe(&project, Self::project_updated).detach();
         Self {
@@ -125,6 +134,7 @@ impl FileFinder {
             latest_search_id: 0,
             latest_search_did_cancel: false,
             latest_search_query: String::new(),
+            relative_to,
             matches: Vec::new(),
             selected: None,
             cancel_flag: Arc::new(AtomicBool::new(false)),
@@ -137,6 +147,7 @@ impl FileFinder {
     }
 
     fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let relative_to = self.relative_to.clone();
         let worktrees = self
             .project
             .read(cx)
@@ -165,6 +176,7 @@ impl FileFinder {
             let matches = fuzzy::match_path_sets(
                 candidate_sets.as_slice(),
                 &query,
+                relative_to,
                 false,
                 100,
                 &cancel_flag,
@@ -377,7 +389,7 @@ mod tests {
             Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
         });
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         let query = "hi".to_string();
         finder
@@ -453,7 +465,7 @@ mod tests {
             Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
         });
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder
             .update(cx, |f, cx| f.spawn_search("hi".into(), cx))
             .await;
@@ -479,7 +491,7 @@ mod tests {
             Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
         });
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -532,8 +544,9 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| {
             Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
         });
+
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         // Run a search that matches two files with the same relative path.
         finder
@@ -551,6 +564,48 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "dir1": { "a.txt": "" },
+                    "dir2": {
+                        "a.txt": "",
+                        "b.txt": ""
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| {
+            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
+        });
+
+        // When workspace has an active item, sort items which are closer to that item
+        // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
+        // so that one should be sorted earlier
+        let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
+        let (_, finder) =
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
+
+        finder
+            .update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
+            .await;
+
+        finder.read_with(cx, |f, _| {
+            assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
+            assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
+        });
+    }
+
     #[gpui::test]
     async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
         let app_state = cx.update(AppState::test);
@@ -573,7 +628,7 @@ mod tests {
             Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
         });
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder
             .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
             .await;

crates/fuzzy/src/matcher.rs πŸ”—

@@ -443,6 +443,7 @@ mod tests {
                 positions: Vec::new(),
                 path: candidate.path.clone(),
                 path_prefix: "".into(),
+                distance_to_relative_ancestor: usize::MAX,
             },
         );
 

crates/fuzzy/src/paths.rs πŸ”—

@@ -25,6 +25,9 @@ pub struct PathMatch {
     pub worktree_id: usize,
     pub path: Arc<Path>,
     pub path_prefix: Arc<str>,
+    /// Number of steps removed from a shared parent with the relative path
+    /// Used to order closer paths first in the search list
+    pub distance_to_relative_ancestor: usize,
 }
 
 pub trait PathMatchCandidateSet<'a>: Send + Sync {
@@ -78,6 +81,11 @@ impl Ord for PathMatch {
             .partial_cmp(&other.score)
             .unwrap_or(Ordering::Equal)
             .then_with(|| self.worktree_id.cmp(&other.worktree_id))
+            .then_with(|| {
+                other
+                    .distance_to_relative_ancestor
+                    .cmp(&self.distance_to_relative_ancestor)
+            })
             .then_with(|| self.path.cmp(&other.path))
     }
 }
@@ -85,6 +93,7 @@ impl Ord for PathMatch {
 pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     candidate_sets: &'a [Set],
     query: &str,
+    relative_to: Option<Arc<Path>>,
     smart_case: bool,
     max_results: usize,
     cancel_flag: &AtomicBool,
@@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     background
         .scoped(|scope| {
             for (segment_idx, results) in segment_results.iter_mut().enumerate() {
+                let relative_to = relative_to.clone();
                 scope.spawn(async move {
                     let segment_start = segment_idx * segment_size;
                     let segment_end = segment_start + segment_size;
@@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
                                     positions: Vec::new(),
                                     path: candidate.path.clone(),
                                     path_prefix: candidate_set.prefix(),
+                                    distance_to_relative_ancestor: relative_to.as_ref().map_or(
+                                        usize::MAX,
+                                        |relative_to| {
+                                            distance_between_paths(
+                                                candidate.path.as_ref(),
+                                                relative_to.as_ref(),
+                                            )
+                                        },
+                                    ),
                                 },
                             );
                         }
@@ -172,3 +191,13 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     }
     results
 }
+
+/// Compute the distance from a given path to some other path
+/// If there is no shared path, returns usize::MAX
+fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
+    let mut path_components = path.components();
+    let mut relative_components = relative_to.components();
+
+    while path_components.next() == relative_components.next() {}
+    path_components.count() + relative_components.count() + 1
+}

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

@@ -31,7 +31,7 @@ use uuid::Uuid;
 
 pub use action::*;
 use callback_collection::CallbackCollection;
-use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
+use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
 pub use menu::*;
 use platform::Event;
 #[cfg(any(test, feature = "test-support"))]
@@ -86,7 +86,7 @@ pub trait View: Entity + Sized {
     }
     fn default_keymap_context() -> keymap_matcher::KeymapContext {
         let mut cx = keymap_matcher::KeymapContext::default();
-        cx.set.insert(Self::ui_name().into());
+        cx.add_identifier(Self::ui_name());
         cx
     }
     fn debug_json(&self, _: &AppContext) -> serde_json::Value {
@@ -474,6 +474,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut MutableAppCon
 type KeystrokeCallback = Box<
     dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
 >;
+type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 
@@ -503,6 +504,7 @@ pub struct MutableAppContext {
     window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
     window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
     keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
+    active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>,
 
     #[allow(clippy::type_complexity)]
     presenters_and_platform_windows:
@@ -514,6 +516,8 @@ pub struct MutableAppContext {
     pending_flushes: usize,
     flushing_effects: bool,
     halt_action_dispatch: bool,
+    next_labeled_task_id: usize,
+    active_labeled_tasks: BTreeMap<usize, &'static str>,
 }
 
 impl MutableAppContext {
@@ -562,6 +566,7 @@ impl MutableAppContext {
             window_bounds_observations: Default::default(),
             keystroke_observations: Default::default(),
             action_dispatch_observations: Default::default(),
+            active_labeled_task_observations: Default::default(),
             presenters_and_platform_windows: Default::default(),
             foreground,
             pending_effects: VecDeque::new(),
@@ -570,6 +575,8 @@ impl MutableAppContext {
             pending_flushes: 0,
             flushing_effects: false,
             halt_action_dispatch: false,
+            next_labeled_task_id: 0,
+            active_labeled_tasks: Default::default(),
         }
     }
 
@@ -794,6 +801,12 @@ impl MutableAppContext {
         window.screen().display_uuid()
     }
 
+    pub fn active_labeled_tasks<'a>(
+        &'a self,
+    ) -> impl DoubleEndedIterator<Item = &'static str> + 'a {
+        self.active_labeled_tasks.values().cloned()
+    }
+
     pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
         let window_id = params.window_id;
         let view_id = params.view_id;
@@ -1160,6 +1173,19 @@ impl MutableAppContext {
         )
     }
 
+    pub fn observe_active_labeled_tasks<F>(&mut self, callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut MutableAppContext) -> bool,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        self.active_labeled_task_observations
+            .add_callback((), subscription_id, Box::new(callback));
+        Subscription::ActiveLabeledTasksObservation(
+            self.active_labeled_task_observations
+                .subscribe((), subscription_id),
+        )
+    }
+
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
         self.pending_effects.push_back(Effect::Deferred {
             callback: Box::new(callback),
@@ -2042,6 +2068,17 @@ impl MutableAppContext {
                             handled_by,
                             result,
                         } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
+                        Effect::ActiveLabeledTasksChanged => {
+                            self.handle_active_labeled_tasks_changed_effect()
+                        }
+                        Effect::ActiveLabeledTasksObservation {
+                            subscription_id,
+                            callback,
+                        } => self.active_labeled_task_observations.add_callback(
+                            (),
+                            subscription_id,
+                            callback,
+                        ),
                     }
                     self.pending_notifications.clear();
                     self.remove_dropped_entities();
@@ -2449,26 +2486,68 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_active_labeled_tasks_changed_effect(&mut self) {
+        self.active_labeled_task_observations
+            .clone()
+            .emit((), self, move |callback, this| {
+                callback(this);
+                true
+            });
+    }
+
     pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
         self.pending_effects
             .push_back(Effect::Focus { window_id, view_id });
     }
 
-    pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
+    fn spawn_internal<F, Fut, T>(&mut self, task_name: Option<&'static str>, f: F) -> Task<T>
     where
         F: FnOnce(AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = T>,
         T: 'static,
     {
+        let label_id = task_name.map(|task_name| {
+            let id = post_inc(&mut self.next_labeled_task_id);
+            self.active_labeled_tasks.insert(id, task_name);
+            self.pending_effects
+                .push_back(Effect::ActiveLabeledTasksChanged);
+            id
+        });
+
         let future = f(self.to_async());
         let cx = self.to_async();
         self.foreground.spawn(async move {
             let result = future.await;
-            cx.0.borrow_mut().flush_effects();
+            let mut cx = cx.0.borrow_mut();
+
+            if let Some(completed_label_id) = label_id {
+                cx.active_labeled_tasks.remove(&completed_label_id);
+                cx.pending_effects
+                    .push_back(Effect::ActiveLabeledTasksChanged);
+            }
+            cx.flush_effects();
             result
         })
     }
 
+    pub fn spawn_labeled<F, Fut, T>(&mut self, task_name: &'static str, f: F) -> Task<T>
+    where
+        F: FnOnce(AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = T>,
+        T: 'static,
+    {
+        self.spawn_internal(Some(task_name), f)
+    }
+
+    pub fn spawn<F, Fut, T>(&mut self, f: F) -> Task<T>
+    where
+        F: FnOnce(AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = T>,
+        T: 'static,
+    {
+        self.spawn_internal(None, f)
+    }
+
     pub fn to_async(&self) -> AsyncAppContext {
         AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
@@ -2907,6 +2986,11 @@ pub enum Effect {
         window_id: usize,
         callback: WindowShouldCloseSubscriptionCallback,
     },
+    ActiveLabeledTasksChanged,
+    ActiveLabeledTasksObservation {
+        subscription_id: usize,
+        callback: ActiveLabeledTasksCallback,
+    },
 }
 
 impl Debug for Effect {
@@ -3066,6 +3150,16 @@ impl Debug for Effect {
                 )
                 .field("result", result)
                 .finish(),
+            Effect::ActiveLabeledTasksChanged => {
+                f.debug_struct("Effect::ActiveLabeledTasksChanged").finish()
+            }
+            Effect::ActiveLabeledTasksObservation {
+                subscription_id,
+                callback: _,
+            } => f
+                .debug_struct("Effect::ActiveLabeledTasksObservation")
+                .field("subscription_id", subscription_id)
+                .finish(),
         }
     }
 }
@@ -3480,7 +3574,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         WeakModelHandle::new(self.model_id)
     }
 
-    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(ModelHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -3490,7 +3584,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         self.app.spawn(|cx| f(handle, cx))
     }
 
-    pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(WeakModelHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -3947,6 +4041,23 @@ impl<'a, T: View> ViewContext<'a, T> {
             })
     }
 
+    pub fn observe_active_labeled_tasks<F>(&mut self, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut T, &mut ViewContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app.observe_active_labeled_tasks(move |cx| {
+            if let Some(observer) = observer.upgrade(cx) {
+                observer.update(cx, |observer, cx| {
+                    callback(observer, cx);
+                });
+                true
+            } else {
+                false
+            }
+        })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -3993,7 +4104,17 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.halt_action_dispatch = false;
     }
 
-    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_labeled<F, Fut, S>(&mut self, task_label: &'static str, f: F) -> Task<S>
+    where
+        F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = S>,
+        S: 'static,
+    {
+        let handle = self.handle();
+        self.app.spawn_labeled(task_label, |cx| f(handle, cx))
+    }
+
+    pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -4003,7 +4124,7 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.spawn(|cx| f(handle, cx))
     }
 
-    pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -5121,6 +5242,9 @@ pub enum Subscription {
     KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
     ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
     ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
+    ActiveLabeledTasksObservation(
+        callback_collection::Subscription<(), ActiveLabeledTasksCallback>,
+    ),
 }
 
 impl Subscription {
@@ -5137,6 +5261,7 @@ impl Subscription {
             Subscription::KeystrokeObservation(subscription) => subscription.id(),
             Subscription::ReleaseObservation(subscription) => subscription.id(),
             Subscription::ActionObservation(subscription) => subscription.id(),
+            Subscription::ActiveLabeledTasksObservation(subscription) => subscription.id(),
         }
     }
 
@@ -5153,6 +5278,7 @@ impl Subscription {
             Subscription::WindowBoundsObservation(subscription) => subscription.detach(),
             Subscription::ReleaseObservation(subscription) => subscription.detach(),
             Subscription::ActionObservation(subscription) => subscription.detach(),
+            Subscription::ActiveLabeledTasksObservation(subscription) => subscription.detach(),
         }
     }
 }
@@ -5161,6 +5287,7 @@ impl Subscription {
 mod tests {
     use super::*;
     use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
+    use postage::{sink::Sink, stream::Stream};
     use serde::Deserialize;
     use smol::future::poll_once;
     use std::{
@@ -6512,12 +6639,12 @@ mod tests {
         let mut view_1 = View::new(1);
         let mut view_2 = View::new(2);
         let mut view_3 = View::new(3);
-        view_1.keymap_context.set.insert("a".into());
-        view_2.keymap_context.set.insert("a".into());
-        view_2.keymap_context.set.insert("b".into());
-        view_3.keymap_context.set.insert("a".into());
-        view_3.keymap_context.set.insert("b".into());
-        view_3.keymap_context.set.insert("c".into());
+        view_1.keymap_context.add_identifier("a");
+        view_2.keymap_context.add_identifier("a");
+        view_2.keymap_context.add_identifier("b");
+        view_3.keymap_context.add_identifier("a");
+        view_3.keymap_context.add_identifier("b");
+        view_3.keymap_context.add_identifier("c");
 
         let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1);
         let view_2 = cx.add_view(&view_1, |_| view_2);
@@ -6776,6 +6903,26 @@ mod tests {
         assert_eq!(presenter.borrow().rendered_views.len(), 1);
     }
 
+    #[crate::test(self)]
+    async fn test_labeled_tasks(cx: &mut TestAppContext) {
+        assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
+        let (mut sender, mut reciever) = postage::oneshot::channel::<()>();
+        let task = cx
+            .update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await }));
+
+        assert_eq!(
+            Some("Test Label"),
+            cx.update(|cx| cx.active_labeled_tasks().next())
+        );
+        sender
+            .send(())
+            .await
+            .expect("Could not send message to complete task");
+        task.await;
+
+        assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
+    }
+
     #[crate::test(self)]
     async fn test_window_activation(cx: &mut TestAppContext) {
         struct View(&'static str);

crates/gpui/src/app/action.rs πŸ”—

@@ -16,6 +16,14 @@ pub trait Action: 'static {
         Self: Sized;
 }
 
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("namespace", &self.namespace())
+            .field("name", &self.name())
+            .finish()
+    }
+}
 /// Define a set of unit struct types that all implement the `Action` trait.
 ///
 /// The first argument is a namespace that will be associated with each of

crates/gpui/src/app/menu.rs πŸ”—

@@ -11,9 +11,46 @@ pub enum MenuItem<'a> {
     Action {
         name: &'a str,
         action: Box<dyn Action>,
+        os_action: Option<OsAction>,
     },
 }
 
+impl<'a> MenuItem<'a> {
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+
+    pub fn submenu(menu: Menu<'a>) -> Self {
+        Self::Submenu(menu)
+    }
+
+    pub fn action(name: &'a str, action: impl Action) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: None,
+        }
+    }
+
+    pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: Some(os_action),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum OsAction {
+    Cut,
+    Copy,
+    Paste,
+    SelectAll,
+    Undo,
+    Redo,
+}
+
 impl MutableAppContext {
     pub fn set_menus(&mut self, menus: Vec<Menu>) {
         self.foreground_platform

crates/gpui/src/elements/flex.rs πŸ”—

@@ -308,7 +308,9 @@ impl Element for Flex {
                     }
                 }
             }
+
             child.paint(child_origin, visible_bounds, cx);
+
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
                 Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),

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

@@ -5,7 +5,7 @@ mod keystroke;
 
 use std::{any::TypeId, fmt::Debug};
 
-use collections::{BTreeMap, HashMap};
+use collections::HashMap;
 use smallvec::SmallVec;
 
 use crate::Action;
@@ -68,8 +68,8 @@ impl KeymapMatcher {
     ///         There exist bindings which are still waiting for more keys.
     ///     MatchResult::Complete(matches) =>
     ///         1 or more bindings have recieved the necessary key presses.
-    ///         The order of the matched actions is by order in the keymap file first and
-    ///         position of the matching view second.
+    ///         The order of the matched actions is by position of the matching first,
+    //          and order in the keymap second.
     pub fn push_keystroke(
         &mut self,
         keystroke: Keystroke,
@@ -80,8 +80,7 @@ impl KeymapMatcher {
         // and then the order the binding matched in the view tree second.
         // The key is the reverse position of the binding in the bindings list so that later bindings
         // match before earlier ones in the user's config
-        let mut matched_bindings: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
-            Default::default();
+        let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
 
         let first_keystroke = self.pending_keystrokes.is_empty();
         self.pending_keystrokes.push(keystroke.clone());
@@ -105,14 +104,11 @@ impl KeymapMatcher {
                 }
             }
 
-            for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
+            for binding in self.keymap.bindings().iter().rev() {
                 match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
                 {
                     BindingMatchResult::Complete(action) => {
-                        matched_bindings
-                            .entry(order)
-                            .or_default()
-                            .push((*view_id, action));
+                        matched_bindings.push((*view_id, action));
                     }
                     BindingMatchResult::Partial => {
                         self.pending_views
@@ -131,7 +127,7 @@ impl KeymapMatcher {
         if !matched_bindings.is_empty() {
             // Collect the sorted matched bindings into the final vec for ease of use
             // Matched bindings are in order by precedence
-            MatchResult::Matches(matched_bindings.into_values().flatten().collect())
+            MatchResult::Matches(matched_bindings)
         } else if any_pending {
             MatchResult::Pending
         } else {
@@ -225,15 +221,47 @@ mod tests {
 
     use super::*;
 
+    #[test]
+    fn test_keymap_and_view_ordering() -> Result<()> {
+        actions!(test, [EditorAction, ProjectPanelAction]);
+
+        let mut editor = KeymapContext::default();
+        editor.add_identifier("Editor");
+
+        let mut project_panel = KeymapContext::default();
+        project_panel.add_identifier("ProjectPanel");
+
+        // Editor 'deeper' in than project panel
+        let dispatch_path = vec![(2, editor), (1, project_panel)];
+
+        // But editor actions 'higher' up in keymap
+        let keymap = Keymap::new(vec![
+            Binding::new("left", EditorAction, Some("Editor")),
+            Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
+        ]);
+
+        let mut matcher = KeymapMatcher::new(keymap);
+
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![
+                (2, Box::new(EditorAction)),
+                (1, Box::new(ProjectPanelAction)),
+            ]),
+        );
+
+        Ok(())
+    }
+
     #[test]
     fn test_push_keystroke() -> Result<()> {
-        actions!(test, [B, AB, C, D, DA]);
+        actions!(test, [B, AB, C, D, DA, E, EF]);
 
         let mut context1 = KeymapContext::default();
-        context1.set.insert("1".into());
+        context1.add_identifier("1");
 
         let mut context2 = KeymapContext::default();
-        context2.set.insert("2".into());
+        context2.add_identifier("2");
 
         let dispatch_path = vec![(2, context2), (1, context1)];
 
@@ -286,6 +314,7 @@ mod tests {
             matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
             MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
         );
+
         // If none of the d action handlers consume the binding, a pending
         // binding may then be used
         assert_eq!(
@@ -366,22 +395,22 @@ mod tests {
         let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
+        context.add_identifier("a");
         assert!(!predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.set.insert("b".into());
+        context.add_identifier("a");
+        context.add_identifier("b");
         assert!(predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.map.insert("c".into(), "x".into());
+        context.add_identifier("a");
+        context.add_key("c", "x");
         assert!(!predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.map.insert("c".into(), "d".into());
+        context.add_identifier("a");
+        context.add_key("c", "d");
         assert!(predicate.eval(&[context]));
 
         let predicate = KeymapContextPredicate::parse("!a").unwrap();
@@ -421,10 +450,11 @@ mod tests {
         assert!(!predicate.eval(&contexts[6..]));
 
         fn context_set(names: &[&str]) -> KeymapContext {
-            KeymapContext {
-                set: names.iter().copied().map(str::to_string).collect(),
-                ..Default::default()
-            }
+            let mut keymap = KeymapContext::new();
+            names
+                .iter()
+                .for_each(|name| keymap.add_identifier(name.to_string()));
+            keymap
         }
     }
 
@@ -447,10 +477,10 @@ mod tests {
         ]);
 
         let mut context_a = KeymapContext::default();
-        context_a.set.insert("a".into());
+        context_a.add_identifier("a");
 
         let mut context_b = KeymapContext::default();
-        context_b.set.insert("b".into());
+        context_b.add_identifier("b");
 
         let mut matcher = KeymapMatcher::new(keymap);
 
@@ -495,7 +525,7 @@ mod tests {
         matcher.clear_pending();
 
         let mut context_c = KeymapContext::default();
-        context_c.set.insert("c".into());
+        context_c.add_identifier("c");
 
         // Pending keystrokes are maintained per-view
         assert_eq!(

crates/gpui/src/keymap_matcher/keymap_context.rs πŸ”—

@@ -1,13 +1,22 @@
+use std::borrow::Cow;
+
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet};
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct KeymapContext {
-    pub set: HashSet<String>,
-    pub map: HashMap<String, String>,
+    set: HashSet<Cow<'static, str>>,
+    map: HashMap<Cow<'static, str>, Cow<'static, str>>,
 }
 
 impl KeymapContext {
+    pub fn new() -> Self {
+        KeymapContext {
+            set: HashSet::default(),
+            map: HashMap::default(),
+        }
+    }
+
     pub fn extend(&mut self, other: &Self) {
         for v in &other.set {
             self.set.insert(v.clone());
@@ -16,6 +25,18 @@ impl KeymapContext {
             self.map.insert(k.clone(), v.clone());
         }
     }
+
+    pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
+        self.set.insert(identifier.into());
+    }
+
+    pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
+        &mut self,
+        key: S1,
+        value: S2,
+    ) {
+        self.map.insert(key.into(), value.into());
+    }
 }
 
 #[derive(Debug, Eq, PartialEq)]
@@ -46,12 +67,12 @@ impl KeymapContextPredicate {
             Self::Identifier(name) => (&context.set).contains(name.as_str()),
             Self::Equal(left, right) => context
                 .map
-                .get(left)
+                .get(left.as_str())
                 .map(|value| value == right)
                 .unwrap_or(false),
             Self::NotEqual(left, right) => context
                 .map
-                .get(left)
+                .get(left.as_str())
                 .map(|value| value != right)
                 .unwrap_or(true),
             Self::Not(pred) => !pred.eval(contexts),

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -98,6 +98,31 @@ unsafe fn build_classes() {
             sel!(handleGPUIMenuItem:),
             handle_menu_item as extern "C" fn(&mut Object, Sel, id),
         );
+        // Add menu item handlers so that OS save panels have the correct key commands
+        decl.add_method(
+            sel!(cut:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(copy:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(paste:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(selectAll:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(undo:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(redo:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
         decl.add_method(
             sel!(validateMenuItem:),
             validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
@@ -193,11 +218,25 @@ impl MacForegroundPlatform {
     ) -> id {
         match item {
             MenuItem::Separator => NSMenuItem::separatorItem(nil),
-            MenuItem::Action { name, action } => {
+            MenuItem::Action {
+                name,
+                action,
+                os_action,
+            } => {
+                // TODO
                 let keystrokes = keystroke_matcher
                     .bindings_for_action_type(action.as_any().type_id())
                     .find(|binding| binding.action().eq(action.as_ref()))
                     .map(|binding| binding.keystrokes());
+                let selector = match os_action {
+                    Some(crate::OsAction::Cut) => selector("cut:"),
+                    Some(crate::OsAction::Copy) => selector("copy:"),
+                    Some(crate::OsAction::Paste) => selector("paste:"),
+                    Some(crate::OsAction::SelectAll) => selector("selectAll:"),
+                    Some(crate::OsAction::Undo) => selector("undo:"),
+                    Some(crate::OsAction::Redo) => selector("redo:"),
+                    None => selector("handleGPUIMenuItem:"),
+                };
 
                 let item;
                 if let Some(keystrokes) = keystrokes {
@@ -218,7 +257,7 @@ impl MacForegroundPlatform {
                         item = NSMenuItem::alloc(nil)
                             .initWithTitle_action_keyEquivalent_(
                                 ns_string(name),
-                                selector("handleGPUIMenuItem:"),
+                                selector,
                                 ns_string(key_to_native(&keystroke.key).as_ref()),
                             )
                             .autorelease();
@@ -240,7 +279,7 @@ impl MacForegroundPlatform {
                         item = NSMenuItem::alloc(nil)
                             .initWithTitle_action_keyEquivalent_(
                                 ns_string(&name),
-                                selector("handleGPUIMenuItem:"),
+                                selector,
                                 ns_string(""),
                             )
                             .autorelease();
@@ -249,7 +288,7 @@ impl MacForegroundPlatform {
                     item = NSMenuItem::alloc(nil)
                         .initWithTitle_action_keyEquivalent_(
                             ns_string(name),
-                            selector("handleGPUIMenuItem:"),
+                            selector,
                             ns_string(""),
                         )
                         .autorelease();

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -737,6 +737,7 @@ impl platform::Window for Window {
             let title = ns_string(title);
             let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
             let _: () = msg_send![window, setTitle: title];
+            self.0.borrow().move_traffic_light();
         }
     }
 

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

@@ -126,7 +126,7 @@ impl<D: PickerDelegate> View for Picker<D> {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 

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

@@ -1314,7 +1314,7 @@ impl View for ProjectPanel {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 }

crates/rope/Cargo.toml πŸ”—

@@ -8,7 +8,7 @@ publish = false
 path = "src/rope.rs"
 
 [dependencies]
-bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "dac565a90e8f9245f48ff46225c915dc50f76920" }
+bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
 smallvec = { version = "1.6", features = ["union"] }
 sum_tree = { path = "../sum_tree" }
 arrayvec = "0.7.1"

crates/rpc/proto/zed.proto πŸ”—

@@ -16,7 +16,7 @@ message Envelope {
         Error error = 6;
         Ping ping = 7;
         Test test = 8;
-        
+
         CreateRoom create_room = 9;
         CreateRoomResponse create_room_response = 10;
         JoinRoom join_room = 11;
@@ -206,7 +206,8 @@ message Room {
     uint64 id = 1;
     repeated Participant participants = 2;
     repeated PendingParticipant pending_participants = 3;
-    string live_kit_room = 4;
+    repeated Follower followers = 4;
+    string live_kit_room = 5;
 }
 
 message Participant {
@@ -227,6 +228,12 @@ message ParticipantProject {
     repeated string worktree_root_names = 2;
 }
 
+message Follower {
+    PeerId leader_id = 1;
+    PeerId follower_id = 2;
+    uint64 project_id = 3;
+}
+
 message ParticipantLocation {
     oneof variant {
         SharedProject shared_project = 1;

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

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 46;
+pub const PROTOCOL_VERSION: u32 = 49;

crates/search/src/project_search.rs πŸ”—

@@ -248,15 +248,15 @@ impl Item for ProjectSearchView {
         tab_theme: &theme::Tab,
         cx: &gpui::AppContext,
     ) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        let search_theme = &settings.theme.search;
         Flex::row()
             .with_child(
                 Svg::new("icons/magnifying_glass_12.svg")
                     .with_color(tab_theme.label.text.color)
                     .constrained()
-                    .with_width(search_theme.tab_icon_width)
+                    .with_width(tab_theme.icon_width)
                     .aligned()
+                    .contained()
+                    .with_margin_right(tab_theme.spacing)
                     .boxed(),
             )
             .with_children(self.model.read(cx).active_query.as_ref().map(|query| {
@@ -264,8 +264,6 @@ impl Item for ProjectSearchView {
 
                 Label::new(query_text, tab_theme.label.clone())
                     .aligned()
-                    .contained()
-                    .with_margin_left(search_theme.tab_icon_spacing)
                     .boxed()
             }))
             .boxed()

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

@@ -21,7 +21,7 @@ use gpui::{
 use project::{LocalWorktree, Project};
 use serde::Deserialize;
 use settings::{Settings, TerminalBlink, WorkingDirectory};
-use smallvec::SmallVec;
+use smallvec::{smallvec, SmallVec};
 use smol::Timer;
 use terminal::{
     alacritty_terminal::{
@@ -469,53 +469,50 @@ impl View for TerminalView {
         let mut context = Self::default_keymap_context();
 
         let mode = self.terminal.read(cx).last_content.mode;
-        context.map.insert(
-            "screen".to_string(),
-            (if mode.contains(TermMode::ALT_SCREEN) {
+        context.add_key(
+            "screen",
+            if mode.contains(TermMode::ALT_SCREEN) {
                 "alt"
             } else {
                 "normal"
-            })
-            .to_string(),
+            },
         );
 
         if mode.contains(TermMode::APP_CURSOR) {
-            context.set.insert("DECCKM".to_string());
+            context.add_identifier("DECCKM");
         }
         if mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPAM".to_string());
-        }
-        //Note the ! here
-        if !mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPNM".to_string());
+            context.add_identifier("DECPAM");
+        } else {
+            context.add_identifier("DECPNM");
         }
         if mode.contains(TermMode::SHOW_CURSOR) {
-            context.set.insert("DECTCEM".to_string());
+            context.add_identifier("DECTCEM");
         }
         if mode.contains(TermMode::LINE_WRAP) {
-            context.set.insert("DECAWM".to_string());
+            context.add_identifier("DECAWM");
         }
         if mode.contains(TermMode::ORIGIN) {
-            context.set.insert("DECOM".to_string());
+            context.add_identifier("DECOM");
         }
         if mode.contains(TermMode::INSERT) {
-            context.set.insert("IRM".to_string());
+            context.add_identifier("IRM");
         }
         //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
         if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
-            context.set.insert("LNM".to_string());
+            context.add_identifier("LNM");
         }
         if mode.contains(TermMode::FOCUS_IN_OUT) {
-            context.set.insert("report_focus".to_string());
+            context.add_identifier("report_focus");
         }
         if mode.contains(TermMode::ALTERNATE_SCROLL) {
-            context.set.insert("alternate_scroll".to_string());
+            context.add_identifier("alternate_scroll");
         }
         if mode.contains(TermMode::BRACKETED_PASTE) {
-            context.set.insert("bracketed_paste".to_string());
+            context.add_identifier("bracketed_paste");
         }
         if mode.intersects(TermMode::MOUSE_MODE) {
-            context.set.insert("any_mouse_reporting".to_string());
+            context.add_identifier("any_mouse_reporting");
         }
         {
             let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
@@ -527,9 +524,7 @@ impl View for TerminalView {
             } else {
                 "off"
             };
-            context
-                .map
-                .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
+            context.add_key("mouse_reporting", mouse_reporting);
         }
         {
             let format = if mode.contains(TermMode::SGR_MOUSE) {
@@ -539,9 +534,7 @@ impl View for TerminalView {
             } else {
                 "normal"
             };
-            context
-                .map
-                .insert("mouse_format".to_string(), format.to_string());
+            context.add_key("mouse_format", format);
         }
         context
     }
@@ -589,11 +582,16 @@ impl Item for TerminalView {
 
         Flex::row()
             .with_child(
-                Label::new(title, tab_theme.label.clone())
+                gpui::elements::Svg::new("icons/terminal_12.svg")
+                    .with_color(tab_theme.label.text.color)
+                    .constrained()
+                    .with_width(tab_theme.icon_width)
                     .aligned()
                     .contained()
+                    .with_margin_right(tab_theme.spacing)
                     .boxed(),
             )
+            .with_child(Label::new(title, tab_theme.label.clone()).aligned().boxed())
             .boxed()
     }
 
@@ -616,43 +614,6 @@ impl Item for TerminalView {
         None
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
-        false
-    }
-
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save should not have been called");
-    }
-
-    fn save_as(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _abs_path: std::path::PathBuf,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
-    }
-
-    fn reload(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        gpui::Task::ready(Ok(()))
-    }
-
     fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
         self.has_bell()
     }
@@ -667,10 +628,10 @@ impl Item for TerminalView {
 
     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
         match event {
-            Event::BreadcrumbsChanged => smallvec::smallvec![ItemEvent::UpdateBreadcrumbs],
-            Event::TitleChanged | Event::Wakeup => smallvec::smallvec![ItemEvent::UpdateTab],
-            Event::CloseTerminal => smallvec::smallvec![ItemEvent::CloseItem],
-            _ => smallvec::smallvec![],
+            Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs],
+            Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab],
+            Event::CloseTerminal => smallvec![ItemEvent::CloseItem],
+            _ => smallvec![],
         }
     }
 

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

@@ -74,20 +74,32 @@ pub struct Titlebar {
     pub container: ContainerStyle,
     pub height: f32,
     pub title: TextStyle,
-    pub avatar_width: f32,
-    pub avatar_margin: f32,
+    pub item_spacing: f32,
+    pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
+    pub follower_avatar_overlap: f32,
+    pub leader_selection: ContainerStyle,
     pub offline_icon: OfflineIcon,
-    pub avatar: ImageStyle,
-    pub inactive_avatar: ImageStyle,
+    pub leader_avatar: AvatarStyle,
+    pub follower_avatar: AvatarStyle,
+    pub inactive_avatar_grayscale: bool,
     pub sign_in_prompt: Interactive<ContainedText>,
     pub outdated_warning: ContainedText,
     pub share_button: Interactive<ContainedText>,
     pub call_control: Interactive<IconButton>,
     pub toggle_contacts_button: Interactive<IconButton>,
+    pub user_menu_button: Interactive<IconButton>,
     pub toggle_contacts_badge: ContainerStyle,
 }
 
+#[derive(Copy, Clone, Deserialize, Default)]
+pub struct AvatarStyle {
+    #[serde(flatten)]
+    pub image: ImageStyle,
+    pub outer_width: f32,
+    pub outer_corner_radius: f32,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ContactsPopover {
     #[serde(flatten)]
@@ -246,8 +258,6 @@ pub struct Search {
     pub match_background: Color,
     pub match_index: ContainedText,
     pub results_status: TextStyle,
-    pub tab_icon_width: f32,
-    pub tab_icon_spacing: f32,
     pub dismiss_button: Interactive<IconButton>,
 }
 
@@ -381,7 +391,7 @@ pub struct InviteLink {
     pub icon: Icon,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Clone, Copy, Default)]
 pub struct Icon {
     #[serde(flatten)]
     pub container: ContainerStyle,

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

@@ -11,12 +11,8 @@ use gpui::{
 };
 use project::Project;
 use settings::Settings;
-use smallvec::SmallVec;
 use theme::{ColorScheme, Layer, Style, StyleSet};
-use workspace::{
-    item::{Item, ItemEvent},
-    register_deserializable_item, Pane, Workspace,
-};
+use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
 
 actions!(theme, [DeployThemeTestbench]);
 
@@ -314,47 +310,6 @@ impl Item for ThemeTestbench {
             .boxed()
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn can_save(&self, _: &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) -> SmallVec<[ItemEvent; 2]> {
-        SmallVec::new()
-    }
-
     fn serialized_item_kind() -> Option<&'static str> {
         Some("ThemeTestBench")
     }

crates/vim/src/motion.rs πŸ”—

@@ -36,6 +36,7 @@ pub enum Motion {
     Matching,
     FindForward { before: bool, text: Arc<str> },
     FindBackward { after: bool, text: Arc<str> },
+    NextLineStart,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -74,6 +75,7 @@ actions!(
         StartOfDocument,
         EndOfDocument,
         Matching,
+        NextLineStart,
     ]
 );
 impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
@@ -111,6 +113,7 @@ pub fn init(cx: &mut MutableAppContext) {
          &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
+    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
 }
 
 pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -138,15 +141,43 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
 impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
-        matches!(
-            self,
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine
-        )
+        match self {
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+            EndOfLine
+            | NextWordEnd { .. }
+            | Matching
+            | FindForward { .. }
+            | Left
+            | Backspace
+            | Right
+            | StartOfLine
+            | NextWordStart { .. }
+            | PreviousWordStart { .. }
+            | FirstNonWhitespace
+            | FindBackward { .. } => false,
+        }
     }
 
     pub fn infallible(&self) -> bool {
         use Motion::*;
-        matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
+        match self {
+            StartOfDocument | EndOfDocument | CurrentLine => true,
+            Down
+            | Up
+            | EndOfLine
+            | NextWordEnd { .. }
+            | Matching
+            | FindForward { .. }
+            | Left
+            | Backspace
+            | Right
+            | StartOfLine
+            | NextWordStart { .. }
+            | PreviousWordStart { .. }
+            | FirstNonWhitespace
+            | FindBackward { .. }
+            | NextLineStart => false,
+        }
     }
 
     pub fn inclusive(&self) -> bool {
@@ -160,7 +191,8 @@ impl Motion {
             | EndOfLine
             | NextWordEnd { .. }
             | Matching
-            | FindForward { .. } => true,
+            | FindForward { .. }
+            | NextLineStart => true,
             Left
             | Backspace
             | Right
@@ -214,6 +246,7 @@ impl Motion {
                 find_backward(map, point, *after, text.clone(), times),
                 SelectionGoal::None,
             ),
+            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -543,3 +576,8 @@ fn find_backward(
         })
         .unwrap_or(from)
 }
+
+fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
+    let new_row = (point.row() + times as u32).min(map.max_buffer_row());
+    map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
+}

crates/vim/src/normal.rs πŸ”—

@@ -473,6 +473,7 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut MutableAppContext) {
 
 #[cfg(test)]
 mod test {
+    use gpui::TestAppContext;
     use indoc::indoc;
 
     use crate::{
@@ -515,15 +516,15 @@ mod test {
         .await;
     }
 
-    // #[gpui::test]
-    // async fn test_enter(cx: &mut gpui::TestAppContext) {
-    //     let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
-    //     cx.assert_all(indoc! {"
-    //         ˇThe qˇuick broˇwn
-    //         Λ‡fox jumps"
-    //     })
-    //     .await;
-    // }
+    #[gpui::test]
+    async fn test_enter(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
+        cx.assert_all(indoc! {"
+            ˇThe qˇuick broˇwn
+            Λ‡fox jumps"
+        })
+        .await;
+    }
 
     #[gpui::test]
     async fn test_k(cx: &mut gpui::TestAppContext) {
@@ -1030,7 +1031,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_percent(cx: &mut gpui::TestAppContext) {
+    async fn test_percent(cx: &mut TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
         cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
         cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")

crates/vim/src/state.rs πŸ”—

@@ -73,34 +73,30 @@ impl VimState {
 
     pub fn keymap_context_layer(&self) -> KeymapContext {
         let mut context = KeymapContext::default();
-        context.map.insert(
-            "vim_mode".to_string(),
+        context.add_key(
+            "vim_mode",
             match self.mode {
                 Mode::Normal => "normal",
                 Mode::Visual { .. } => "visual",
                 Mode::Insert => "insert",
-            }
-            .to_string(),
+            },
         );
 
         if self.vim_controlled() {
-            context.set.insert("VimControl".to_string());
+            context.add_identifier("VimControl");
         }
 
         let active_operator = self.operator_stack.last();
 
         if let Some(active_operator) = active_operator {
             for context_flag in active_operator.context_flags().into_iter() {
-                context.set.insert(context_flag.to_string());
+                context.add_identifier(*context_flag);
             }
         }
 
-        context.map.insert(
-            "vim_operator".to_string(),
-            active_operator
-                .map(|op| op.id())
-                .unwrap_or_else(|| "none")
-                .to_string(),
+        context.add_key(
+            "vim_operator",
+            active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
         );
 
         context

crates/vim/test_data/test_enter.json πŸ”—

@@ -0,0 +1 @@
+[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

crates/workspace/src/item.rs πŸ”—

@@ -49,9 +49,11 @@ pub trait Item: View {
     }
     fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
         -> ElementBox;
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
-    fn is_singleton(&self, cx: &AppContext) -> bool;
-    fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
+    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
+    fn is_singleton(&self, _cx: &AppContext) -> bool {
+        false
+    }
+    fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
     fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
@@ -64,23 +66,31 @@ pub trait Item: View {
     fn has_conflict(&self, _: &AppContext) -> bool {
         false
     }
-    fn can_save(&self, cx: &AppContext) -> bool;
+    fn can_save(&self, _cx: &AppContext) -> bool {
+        false
+    }
     fn save(
         &mut self,
-        project: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        unimplemented!("save() must be implemented if can_save() returns true")
+    }
     fn save_as(
         &mut self,
-        project: ModelHandle<Project>,
-        abs_path: PathBuf,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<Project>,
+        _abs_path: PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        unimplemented!("save_as() must be implemented if can_save() returns true")
+    }
     fn reload(
         &mut self,
-        project: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        unimplemented!("reload() must be implemented if can_save() returns true")
+    }
     fn git_diff_recalc(
         &mut self,
         _project: ModelHandle<Project>,
@@ -88,7 +98,9 @@ pub trait Item: View {
     ) -> Task<Result<()>> {
         Task::ready(Ok(()))
     }
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]>;
+    fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        SmallVec::new()
+    }
     fn should_close_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -124,15 +136,21 @@ pub trait Item: View {
 
     fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
 
-    fn serialized_item_kind() -> Option<&'static str>;
+    fn serialized_item_kind() -> Option<&'static str> {
+        None
+    }
 
     fn deserialize(
-        project: ModelHandle<Project>,
-        workspace: WeakViewHandle<Workspace>,
-        workspace_id: WorkspaceId,
-        item_id: ItemId,
-        cx: &mut ViewContext<Pane>,
-    ) -> Task<Result<ViewHandle<Self>>>;
+        _project: ModelHandle<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        _workspace_id: WorkspaceId,
+        _item_id: ItemId,
+        _cx: &mut ViewContext<Pane>,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        unimplemented!(
+            "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
+        )
+    }
 }
 
 pub trait ItemHandle: 'static + fmt::Debug {

crates/workspace/src/pane.rs πŸ”—

@@ -21,6 +21,7 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     impl_actions, impl_internal_actions,
+    keymap_matcher::KeymapContext,
     platform::{CursorStyle, NavigationDirection},
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
     ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
@@ -1550,6 +1551,14 @@ impl View for Pane {
             }
         }
     }
+
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
+        let mut keymap = Self::default_keymap_context();
+        if self.docked.is_some() {
+            keymap.add_identifier("docked");
+        }
+        keymap
+    }
 }
 
 fn tab_bar_button<A: Action>(

crates/workspace/src/shared_screen.rs πŸ”—

@@ -1,23 +1,18 @@
 use crate::{
-    item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
+    item::{Item, ItemEvent},
+    ItemNavHistory, WorkspaceId,
 };
-use anyhow::{anyhow, Result};
 use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
 use futures::StreamExt;
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    AppContext, Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AppContext, Entity, MouseButton, RenderContext, Task, View, ViewContext,
 };
-use project::Project;
 use settings::Settings;
 use smallvec::SmallVec;
-use std::{
-    path::PathBuf,
-    sync::{Arc, Weak},
-};
+use std::sync::{Arc, Weak};
 
 pub enum Event {
     Close,
@@ -130,12 +125,6 @@ impl Item for SharedScreen {
             .boxed()
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        false
-    }
-
     fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
         self.nav_history = Some(history);
     }
@@ -149,52 +138,9 @@ impl Item for SharedScreen {
         Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
     }
 
-    fn can_save(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::save called on SharedScreen")))
-    }
-
-    fn save_as(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: PathBuf,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::save_as called on SharedScreen")))
-    }
-
-    fn reload(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
-    }
-
     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
         match event {
             Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
         }
     }
-
-    fn serialized_item_kind() -> Option<&'static str> {
-        None
-    }
-
-    fn deserialize(
-        _project: ModelHandle<Project>,
-        _workspace: WeakViewHandle<Workspace>,
-        _workspace_id: WorkspaceId,
-        _item_id: ItemId,
-        _cx: &mut ViewContext<Pane>,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        unreachable!("Shared screen can not be deserialized")
-    }
 }

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

@@ -837,7 +837,7 @@ impl Workspace {
         &self.project
     }
 
-    pub fn client(&self) -> &Arc<Client> {
+    pub fn client(&self) -> &Client {
         &self.client
     }
 
@@ -1589,13 +1589,17 @@ impl Workspace {
         }
 
         let item = pane.read(cx).active_item()?;
-        let new_pane = self.add_pane(cx);
-        if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
-            Pane::add_item(self, &new_pane, clone, true, true, None, cx);
-        }
-        self.center.split(&pane, &new_pane, direction).unwrap();
+        let maybe_pane_handle =
+            if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
+                let new_pane = self.add_pane(cx);
+                Pane::add_item(self, &new_pane, clone, true, true, None, cx);
+                self.center.split(&pane, &new_pane, direction).unwrap();
+                Some(new_pane)
+            } else {
+                None
+            };
         cx.notify();
-        Some(new_pane)
+        maybe_pane_handle
     }
 
     pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
@@ -1828,24 +1832,15 @@ impl Workspace {
         None
     }
 
-    pub fn is_following(&self, peer_id: PeerId) -> bool {
+    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
         self.follower_states_by_leader.contains_key(&peer_id)
     }
 
-    pub fn is_followed(&self, peer_id: PeerId) -> bool {
+    pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
         self.leader_state.followers.contains(&peer_id)
     }
 
     fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
-        let project = &self.project.read(cx);
-        let mut worktree_root_names = String::new();
-        for (i, name) in project.worktree_root_names(cx).enumerate() {
-            if i > 0 {
-                worktree_root_names.push_str(", ");
-            }
-            worktree_root_names.push_str(name);
-        }
-
         // TODO: There should be a better system in place for this
         // (https://github.com/zed-industries/zed/issues/1290)
         let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
@@ -1862,16 +1857,10 @@ impl Workspace {
             MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
                 Container::new(
                     Stack::new()
-                        .with_child(
-                            Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
-                                .aligned()
-                                .left()
-                                .boxed(),
-                        )
                         .with_children(
                             self.titlebar_item
                                 .as_ref()
-                                .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
+                                .map(|item| ChildView::new(item, cx).boxed()),
                         )
                         .boxed(),
                 )
@@ -2727,11 +2716,7 @@ impl View for Workspace {
     }
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut keymap = Self::default_keymap_context();
-        if self.active_pane() == self.dock_pane() {
-            keymap.set.insert("Dock".into());
-        }
-        keymap
+        Self::default_keymap_context()
     }
 }
 

crates/zed/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.75.0"
+version = "0.76.0"
 publish = false
 
 [lib]

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

@@ -1,4 +1,4 @@
-use gpui::{Menu, MenuItem};
+use gpui::{Menu, MenuItem, OsAction};
 
 #[cfg(target_os = "macos")]
 pub fn menus() -> Vec<Menu<'static>> {
@@ -6,363 +6,159 @@ pub fn menus() -> Vec<Menu<'static>> {
         Menu {
             name: "Zed",
             items: vec![
-                MenuItem::Action {
-                    name: "About Zed…",
-                    action: Box::new(super::About),
-                },
-                MenuItem::Action {
-                    name: "Check for Updates",
-                    action: Box::new(auto_update::Check),
-                },
-                MenuItem::Separator,
-                MenuItem::Submenu(Menu {
+                MenuItem::action("About Zed…", super::About),
+                MenuItem::action("Check for Updates", auto_update::Check),
+                MenuItem::separator(),
+                MenuItem::submenu(Menu {
                     name: "Preferences",
                     items: vec![
-                        MenuItem::Action {
-                            name: "Open Settings",
-                            action: Box::new(super::OpenSettings),
-                        },
-                        MenuItem::Action {
-                            name: "Open Key Bindings",
-                            action: Box::new(super::OpenKeymap),
-                        },
-                        MenuItem::Action {
-                            name: "Open Default Settings",
-                            action: Box::new(super::OpenDefaultSettings),
-                        },
-                        MenuItem::Action {
-                            name: "Open Default Key Bindings",
-                            action: Box::new(super::OpenDefaultKeymap),
-                        },
-                        MenuItem::Action {
-                            name: "Select Theme",
-                            action: Box::new(theme_selector::Toggle),
-                        },
+                        MenuItem::action("Open Settings", super::OpenSettings),
+                        MenuItem::action("Open Key Bindings", super::OpenKeymap),
+                        MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
+                        MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
+                        MenuItem::action("Select Theme", theme_selector::Toggle),
                     ],
                 }),
-                MenuItem::Action {
-                    name: "Install CLI",
-                    action: Box::new(super::InstallCommandLineInterface),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Hide Zed",
-                    action: Box::new(super::Hide),
-                },
-                MenuItem::Action {
-                    name: "Hide Others",
-                    action: Box::new(super::HideOthers),
-                },
-                MenuItem::Action {
-                    name: "Show All",
-                    action: Box::new(super::ShowAll),
-                },
-                MenuItem::Action {
-                    name: "Quit",
-                    action: Box::new(super::Quit),
-                },
+                MenuItem::action("Install CLI", super::InstallCommandLineInterface),
+                MenuItem::separator(),
+                MenuItem::action("Hide Zed", super::Hide),
+                MenuItem::action("Hide Others", super::HideOthers),
+                MenuItem::action("Show All", super::ShowAll),
+                MenuItem::action("Quit", super::Quit),
             ],
         },
         Menu {
             name: "File",
             items: vec![
-                MenuItem::Action {
-                    name: "New",
-                    action: Box::new(workspace::NewFile),
-                },
-                MenuItem::Action {
-                    name: "New Window",
-                    action: Box::new(workspace::NewWindow),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Open…",
-                    action: Box::new(workspace::Open),
-                },
-                MenuItem::Action {
-                    name: "Open Recent...",
-                    action: Box::new(recent_projects::OpenRecent),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Add Folder to Project…",
-                    action: Box::new(workspace::AddFolderToProject),
-                },
-                MenuItem::Action {
-                    name: "Save",
-                    action: Box::new(workspace::Save),
-                },
-                MenuItem::Action {
-                    name: "Save As…",
-                    action: Box::new(workspace::SaveAs),
-                },
-                MenuItem::Action {
-                    name: "Save All",
-                    action: Box::new(workspace::SaveAll),
-                },
-                MenuItem::Action {
-                    name: "Close Editor",
-                    action: Box::new(workspace::CloseActiveItem),
-                },
-                MenuItem::Action {
-                    name: "Close Window",
-                    action: Box::new(workspace::CloseWindow),
-                },
+                MenuItem::action("New", workspace::NewFile),
+                MenuItem::action("New Window", workspace::NewWindow),
+                MenuItem::separator(),
+                MenuItem::action("Open…", workspace::Open),
+                MenuItem::action("Open Recent...", recent_projects::OpenRecent),
+                MenuItem::separator(),
+                MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
+                MenuItem::action("Save", workspace::Save),
+                MenuItem::action("Save As…", workspace::SaveAs),
+                MenuItem::action("Save All", workspace::SaveAll),
+                MenuItem::action("Close Editor", workspace::CloseActiveItem),
+                MenuItem::action("Close Window", workspace::CloseWindow),
             ],
         },
         Menu {
             name: "Edit",
             items: vec![
-                MenuItem::Action {
-                    name: "Undo",
-                    action: Box::new(editor::Undo),
-                },
-                MenuItem::Action {
-                    name: "Redo",
-                    action: Box::new(editor::Redo),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Cut",
-                    action: Box::new(editor::Cut),
-                },
-                MenuItem::Action {
-                    name: "Copy",
-                    action: Box::new(editor::Copy),
-                },
-                MenuItem::Action {
-                    name: "Paste",
-                    action: Box::new(editor::Paste),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Find",
-                    action: Box::new(search::buffer_search::Deploy { focus: true }),
-                },
-                MenuItem::Action {
-                    name: "Find In Project",
-                    action: Box::new(workspace::NewSearch),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Toggle Line Comment",
-                    action: Box::new(editor::ToggleComments::default()),
-                },
-                MenuItem::Action {
-                    name: "Emoji & Symbols",
-                    action: Box::new(editor::ShowCharacterPalette),
-                },
+                MenuItem::os_action("Undo", editor::Undo, OsAction::Undo),
+                MenuItem::os_action("Redo", editor::Redo, OsAction::Redo),
+                MenuItem::separator(),
+                MenuItem::os_action("Cut", editor::Cut, OsAction::Cut),
+                MenuItem::os_action("Copy", editor::Copy, OsAction::Copy),
+                MenuItem::os_action("Paste", editor::Paste, OsAction::Paste),
+                MenuItem::separator(),
+                MenuItem::action("Find", search::buffer_search::Deploy { focus: true }),
+                MenuItem::action("Find In Project", workspace::NewSearch),
+                MenuItem::separator(),
+                MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()),
+                MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette),
             ],
         },
         Menu {
             name: "Selection",
             items: vec![
-                MenuItem::Action {
-                    name: "Select All",
-                    action: Box::new(editor::SelectAll),
-                },
-                MenuItem::Action {
-                    name: "Expand Selection",
-                    action: Box::new(editor::SelectLargerSyntaxNode),
-                },
-                MenuItem::Action {
-                    name: "Shrink Selection",
-                    action: Box::new(editor::SelectSmallerSyntaxNode),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Add Cursor Above",
-                    action: Box::new(editor::AddSelectionAbove),
-                },
-                MenuItem::Action {
-                    name: "Add Cursor Below",
-                    action: Box::new(editor::AddSelectionBelow),
-                },
-                MenuItem::Action {
-                    name: "Select Next Occurrence",
-                    action: Box::new(editor::SelectNext {
+                MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll),
+                MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode),
+                MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode),
+                MenuItem::separator(),
+                MenuItem::action("Add Cursor Above", editor::AddSelectionAbove),
+                MenuItem::action("Add Cursor Below", editor::AddSelectionBelow),
+                MenuItem::action(
+                    "Select Next Occurrence",
+                    editor::SelectNext {
                         replace_newest: false,
-                    }),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Move Line Up",
-                    action: Box::new(editor::MoveLineUp),
-                },
-                MenuItem::Action {
-                    name: "Move Line Down",
-                    action: Box::new(editor::MoveLineDown),
-                },
-                MenuItem::Action {
-                    name: "Duplicate Selection",
-                    action: Box::new(editor::DuplicateLine),
-                },
+                    },
+                ),
+                MenuItem::separator(),
+                MenuItem::action("Move Line Up", editor::MoveLineUp),
+                MenuItem::action("Move Line Down", editor::MoveLineDown),
+                MenuItem::action("Duplicate Selection", editor::DuplicateLine),
             ],
         },
         Menu {
             name: "View",
             items: vec![
-                MenuItem::Action {
-                    name: "Zoom In",
-                    action: Box::new(super::IncreaseBufferFontSize),
-                },
-                MenuItem::Action {
-                    name: "Zoom Out",
-                    action: Box::new(super::DecreaseBufferFontSize),
-                },
-                MenuItem::Action {
-                    name: "Reset Zoom",
-                    action: Box::new(super::ResetBufferFontSize),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Toggle Left Sidebar",
-                    action: Box::new(workspace::ToggleLeftSidebar),
-                },
-                MenuItem::Submenu(Menu {
+                MenuItem::action("Zoom In", super::IncreaseBufferFontSize),
+                MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
+                MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
+                MenuItem::separator(),
+                MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar),
+                MenuItem::submenu(Menu {
                     name: "Editor Layout",
                     items: vec![
-                        MenuItem::Action {
-                            name: "Split Up",
-                            action: Box::new(workspace::SplitUp),
-                        },
-                        MenuItem::Action {
-                            name: "Split Down",
-                            action: Box::new(workspace::SplitDown),
-                        },
-                        MenuItem::Action {
-                            name: "Split Left",
-                            action: Box::new(workspace::SplitLeft),
-                        },
-                        MenuItem::Action {
-                            name: "Split Right",
-                            action: Box::new(workspace::SplitRight),
-                        },
+                        MenuItem::action("Split Up", workspace::SplitUp),
+                        MenuItem::action("Split Down", workspace::SplitDown),
+                        MenuItem::action("Split Left", workspace::SplitLeft),
+                        MenuItem::action("Split Right", workspace::SplitRight),
                     ],
                 }),
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Project Panel",
-                    action: Box::new(project_panel::ToggleFocus),
-                },
-                MenuItem::Action {
-                    name: "Command Palette",
-                    action: Box::new(command_palette::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Diagnostics",
-                    action: Box::new(diagnostics::Deploy),
-                },
-                MenuItem::Separator,
+                MenuItem::separator(),
+                MenuItem::action("Project Panel", project_panel::ToggleFocus),
+                MenuItem::action("Command Palette", command_palette::Toggle),
+                MenuItem::action("Diagnostics", diagnostics::Deploy),
+                MenuItem::separator(),
             ],
         },
         Menu {
             name: "Go",
             items: vec![
-                MenuItem::Action {
-                    name: "Back",
-                    action: Box::new(workspace::GoBack { pane: None }),
-                },
-                MenuItem::Action {
-                    name: "Forward",
-                    action: Box::new(workspace::GoForward { pane: None }),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Go to File",
-                    action: Box::new(file_finder::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Symbol in Project",
-                    action: Box::new(project_symbols::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Symbol in Editor",
-                    action: Box::new(outline::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Definition",
-                    action: Box::new(editor::GoToDefinition),
-                },
-                MenuItem::Action {
-                    name: "Go to Type Definition",
-                    action: Box::new(editor::GoToTypeDefinition),
-                },
-                MenuItem::Action {
-                    name: "Find All References",
-                    action: Box::new(editor::FindAllReferences),
-                },
-                MenuItem::Action {
-                    name: "Go to Line/Column",
-                    action: Box::new(go_to_line::Toggle),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Next Problem",
-                    action: Box::new(editor::GoToDiagnostic),
-                },
-                MenuItem::Action {
-                    name: "Previous Problem",
-                    action: Box::new(editor::GoToPrevDiagnostic),
-                },
+                MenuItem::action("Back", workspace::GoBack { pane: None }),
+                MenuItem::action("Forward", workspace::GoForward { pane: None }),
+                MenuItem::separator(),
+                MenuItem::action("Go to File", file_finder::Toggle),
+                MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
+                MenuItem::action("Go to Symbol in Editor", outline::Toggle),
+                MenuItem::action("Go to Definition", editor::GoToDefinition),
+                MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition),
+                MenuItem::action("Find All References", editor::FindAllReferences),
+                MenuItem::action("Go to Line/Column", go_to_line::Toggle),
+                MenuItem::separator(),
+                MenuItem::action("Next Problem", editor::GoToDiagnostic),
+                MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic),
             ],
         },
         Menu {
             name: "Window",
             items: vec![
-                MenuItem::Action {
-                    name: "Minimize",
-                    action: Box::new(super::Minimize),
-                },
-                MenuItem::Action {
-                    name: "Zoom",
-                    action: Box::new(super::Zoom),
-                },
-                MenuItem::Separator,
+                MenuItem::action("Minimize", super::Minimize),
+                MenuItem::action("Zoom", super::Zoom),
+                MenuItem::separator(),
             ],
         },
         Menu {
             name: "Help",
             items: vec![
-                MenuItem::Action {
-                    name: "Command Palette",
-                    action: Box::new(command_palette::Toggle),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "View Telemetry Log",
-                    action: Box::new(crate::OpenTelemetryLog),
-                },
-                MenuItem::Action {
-                    name: "View Dependency Licenses",
-                    action: Box::new(crate::OpenLicenses),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Copy System Specs Into Clipboard",
-                    action: Box::new(feedback::CopySystemSpecsIntoClipboard),
-                },
-                MenuItem::Action {
-                    name: "File Bug Report",
-                    action: Box::new(feedback::FileBugReport),
-                },
-                MenuItem::Action {
-                    name: "Request Feature",
-                    action: Box::new(feedback::RequestFeature),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Documentation",
-                    action: Box::new(crate::OpenBrowser {
+                MenuItem::action("Command Palette", command_palette::Toggle),
+                MenuItem::separator(),
+                MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog),
+                MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
+                MenuItem::separator(),
+                MenuItem::action(
+                    "Copy System Specs Into Clipboard",
+                    feedback::CopySystemSpecsIntoClipboard,
+                ),
+                MenuItem::action("File Bug Report", feedback::FileBugReport),
+                MenuItem::action("Request Feature", feedback::RequestFeature),
+                MenuItem::separator(),
+                MenuItem::action(
+                    "Documentation",
+                    crate::OpenBrowser {
                         url: "https://zed.dev/docs".into(),
-                    }),
-                },
-                MenuItem::Action {
-                    name: "Zed Twitter",
-                    action: Box::new(crate::OpenBrowser {
+                    },
+                ),
+                MenuItem::action(
+                    "Zed Twitter",
+                    crate::OpenBrowser {
                         url: "https://twitter.com/zeddotdev".into(),
-                    }),
-                },
+                    },
+                ),
             ],
         },
     ]

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

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
 pub use client;
-use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
+use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
 use collections::VecDeque;
 pub use editor;
 use editor::{Editor, MultiBuffer};
@@ -99,9 +99,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         },
     );
     cx.add_action(
-        |workspace: &mut Workspace,
-         _: &ToggleCollaborationMenu,
-         cx: &mut ViewContext<Workspace>| {
+        |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
             if let Some(item) = workspace
                 .titlebar_item()
                 .and_then(|item| item.downcast::<CollabTitlebarItem>())

styles/package-lock.json πŸ”—

@@ -9,67 +9,83 @@
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {
-                "@types/chroma-js": "^2.1.3",
-                "@types/node": "^17.0.23",
+                "@types/chroma-js": "^2.4.0",
+                "@types/node": "^18.14.1",
+                "bezier-easing": "^2.1.0",
                 "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
+                "deepmerge": "^4.3.0",
                 "toml": "^3.0.0",
-                "ts-node": "^10.7.0"
-            }
-        },
-        "node_modules/@cspotcode/source-map-consumer": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
-            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
-            "engines": {
-                "node": ">= 12"
+                "ts-node": "^10.9.1"
             }
         },
         "node_modules/@cspotcode/source-map-support": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
-            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
             "dependencies": {
-                "@cspotcode/source-map-consumer": "0.8.0"
+                "@jridgewell/trace-mapping": "0.3.9"
             },
             "engines": {
                 "node": ">=12"
             }
         },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
         "node_modules/@tsconfig/node10": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
-            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
         },
         "node_modules/@tsconfig/node12": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
-            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
         },
         "node_modules/@tsconfig/node14": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
-            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
         },
         "node_modules/@tsconfig/node16": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
-            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
         },
         "node_modules/@types/chroma-js": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
-            "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
+            "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
         },
         "node_modules/@types/node": {
-            "version": "17.0.23",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+            "version": "18.14.1",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+            "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
         },
         "node_modules/acorn": {
-            "version": "8.7.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
-            "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
+            "version": "8.8.2",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
             "bin": {
                 "acorn": "bin/acorn"
             },
@@ -90,6 +106,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "node_modules/bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "node_modules/case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -111,6 +132,14 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "node_modules/deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -130,11 +159,11 @@
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
         "node_modules/ts-node": {
-            "version": "10.7.0",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
-            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
             "dependencies": {
-                "@cspotcode/source-map-support": "0.7.0",
+                "@cspotcode/source-map-support": "^0.8.0",
                 "@tsconfig/node10": "^1.0.7",
                 "@tsconfig/node12": "^1.0.7",
                 "@tsconfig/node14": "^1.0.0",
@@ -145,7 +174,7 @@
                 "create-require": "^1.1.0",
                 "diff": "^4.0.1",
                 "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.0",
+                "v8-compile-cache-lib": "^3.0.1",
                 "yn": "3.1.1"
             },
             "bin": {
@@ -172,9 +201,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "4.6.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "version": "4.9.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "peer": true,
             "bin": {
                 "tsc": "bin/tsc",
@@ -185,9 +214,9 @@
             }
         },
         "node_modules/v8-compile-cache-lib": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
-            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
         },
         "node_modules/yn": {
             "version": "3.1.1",
@@ -199,53 +228,67 @@
         }
     },
     "dependencies": {
-        "@cspotcode/source-map-consumer": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
-            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
-        },
         "@cspotcode/source-map-support": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
-            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "requires": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            }
+        },
+        "@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
+        },
+        "@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
+        },
+        "@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
             "requires": {
-                "@cspotcode/source-map-consumer": "0.8.0"
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
             }
         },
         "@tsconfig/node10": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
-            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
         },
         "@tsconfig/node12": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
-            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
         },
         "@tsconfig/node14": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
-            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
         },
         "@tsconfig/node16": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
-            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
         },
         "@types/chroma-js": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
-            "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
+            "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
         },
         "@types/node": {
-            "version": "17.0.23",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+            "version": "18.14.1",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+            "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
         },
         "acorn": {
-            "version": "8.7.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
-            "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
+            "version": "8.8.2",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="
         },
         "acorn-walk": {
             "version": "8.2.0",
@@ -257,6 +300,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -272,6 +320,11 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
+        },
         "diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -288,11 +341,11 @@
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
         "ts-node": {
-            "version": "10.7.0",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
-            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
             "requires": {
-                "@cspotcode/source-map-support": "0.7.0",
+                "@cspotcode/source-map-support": "^0.8.0",
                 "@tsconfig/node10": "^1.0.7",
                 "@tsconfig/node12": "^1.0.7",
                 "@tsconfig/node14": "^1.0.0",
@@ -303,20 +356,20 @@
                 "create-require": "^1.1.0",
                 "diff": "^4.0.1",
                 "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.0",
+                "v8-compile-cache-lib": "^3.0.1",
                 "yn": "3.1.1"
             }
         },
         "typescript": {
-            "version": "4.6.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "version": "4.9.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "peer": true
         },
         "v8-compile-cache-lib": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
-            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
         },
         "yn": {
             "version": "3.1.1",

styles/package.json πŸ”—

@@ -10,11 +10,19 @@
     "author": "",
     "license": "ISC",
     "dependencies": {
-        "@types/chroma-js": "^2.1.3",
-        "@types/node": "^17.0.23",
+        "@types/chroma-js": "^2.4.0",
+        "@types/node": "^18.14.1",
+        "bezier-easing": "^2.1.0",
         "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",
+        "deepmerge": "^4.3.0",
         "toml": "^3.0.0",
-        "ts-node": "^10.7.0"
+        "ts-node": "^10.9.1"
+    },
+    "prettier": {
+        "semi": false,
+        "printWidth": 80,
+        "htmlWhitespaceSensitivity": "strict",
+        "tabWidth": 4
     }
 }

styles/src/buildLicenses.ts πŸ”—

@@ -1,73 +1,87 @@
-import * as fs from "fs";
-import toml from "toml";
-import {
-  schemeMeta
-} from "./colorSchemes";
-import { Meta } from "./themes/common/colorScheme";
-import https from "https";
-import crypto from "crypto";
+import * as fs from "fs"
+import toml from "toml"
+import { schemeMeta } from "./colorSchemes"
+import { Meta } from "./themes/common/colorScheme"
+import https from "https"
+import crypto from "crypto"
 
 const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
 
 // Use the cargo-about configuration file as the source of truth for supported licenses.
 function parseAcceptedToml(file: string): string[] {
-  let buffer = fs.readFileSync(file).toString();
+    let buffer = fs.readFileSync(file).toString()
 
-  let obj = toml.parse(buffer);
+    let obj = toml.parse(buffer)
 
-  if (!Array.isArray(obj.accepted)) {
-    throw Error("Accepted license source is malformed")
-  }
+    if (!Array.isArray(obj.accepted)) {
+        throw Error("Accepted license source is malformed")
+    }
 
-  return obj.accepted
+    return obj.accepted
 }
 
-
 function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
-  for (let meta of schemeMeta) {
-    // FIXME: Add support for conjuctions and conditions
-    if (licenses.indexOf(meta.license.SPDX) < 0) {
-      throw Error(`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`)
+    for (let meta of schemeMeta) {
+        // FIXME: Add support for conjuctions and conditions
+        if (licenses.indexOf(meta.license.SPDX) < 0) {
+            throw Error(
+                `License for theme ${meta.name} (${meta.license.SPDX}) is not supported`
+            )
+        }
     }
-  }
 }
 
+function getLicenseText(
+    schemeMeta: Meta[],
+    callback: (meta: Meta, license_text: string) => void
+) {
+    for (let meta of schemeMeta) {
+        // The following copied from the example code on nodejs.org:
+        // https://nodejs.org/api/http.html#httpgetoptions-callback
+        https
+            .get(meta.license.https_url, (res) => {
+                const { statusCode } = res
 
-function getLicenseText(schemeMeta: Meta[], callback: (meta: Meta, license_text: string) => void) {
-  for (let meta of schemeMeta) {
-    // The following copied from the example code on nodejs.org: 
-    // https://nodejs.org/api/http.html#httpgetoptions-callback
-    https.get(meta.license.https_url, (res) => {
-      const { statusCode } = res;
+                if (statusCode < 200 || statusCode >= 300) {
+                    throw new Error(
+                        `Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
+                    )
+                }
 
-      if (statusCode < 200 || statusCode >= 300) {
-        throw new Error(`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`);
-      }
-
-      res.setEncoding('utf8');
-      let rawData = '';
-      res.on('data', (chunk) => { rawData += chunk; });
-      res.on('end', () => {
-        const hash = crypto.createHash('sha256').update(rawData).digest('hex');
-        if (meta.license.license_checksum == hash) {
-          callback(meta, rawData)
-        } else {
-          throw Error(`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`)
-        }
-      });
-    }).on('error', (e) => {
-      throw e
-    });
-  }
+                res.setEncoding("utf8")
+                let rawData = ""
+                res.on("data", (chunk) => {
+                    rawData += chunk
+                })
+                res.on("end", () => {
+                    const hash = crypto
+                        .createHash("sha256")
+                        .update(rawData)
+                        .digest("hex")
+                    if (meta.license.license_checksum == hash) {
+                        callback(meta, rawData)
+                    } else {
+                        throw Error(
+                            `Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`
+                        )
+                    }
+                })
+            })
+            .on("error", (e) => {
+                throw e
+            })
+    }
 }
 
 function writeLicense(schemeMeta: Meta, text: String) {
-  process.stdout.write(`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`)
+    process.stdout.write(
+        `## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
+    )
 }
 
-const accepted_licenses = parseAcceptedToml(accepted_licenses_file);
+const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
 checkLicenses(schemeMeta, accepted_licenses)
 
 getLicenseText(schemeMeta, (meta, text) => {
-  writeLicense(meta, text)
-});
+    writeLicense(meta, text)
+})

styles/src/buildThemes.ts πŸ”—

@@ -1,50 +1,52 @@
-import * as fs from "fs";
-import { tmpdir } from "os";
-import * as path from "path";
-import colorSchemes, {
-  staffColorSchemes,
-} from "./colorSchemes";
-import app from "./styleTree/app";
-import { ColorScheme } from "./themes/common/colorScheme";
-import snakeCase from "./utils/snakeCase";
+import * as fs from "fs"
+import { tmpdir } from "os"
+import * as path from "path"
+import colorSchemes, { staffColorSchemes } from "./colorSchemes"
+import app from "./styleTree/app"
+import { ColorScheme } from "./themes/common/colorScheme"
+import snakeCase from "./utils/snakeCase"
 
 const assetsDirectory = `${__dirname}/../../assets`
-const themeDirectory = `${assetsDirectory}/themes`;
-const staffDirectory = `${themeDirectory}/staff`;
+const themeDirectory = `${assetsDirectory}/themes`
+const staffDirectory = `${themeDirectory}/staff`
 
-const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"));
+const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
 
 // Clear existing themes
 function clearThemes(themeDirectory: string) {
-  if (!fs.existsSync(themeDirectory)) {
-    fs.mkdirSync(themeDirectory, { recursive: true });
-  } else {
-    for (const file of fs.readdirSync(themeDirectory)) {
-      if (file.endsWith(".json")) {
-        const name = file.replace(/\.json$/, "");
-        if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
-          fs.unlinkSync(path.join(themeDirectory, file));
+    if (!fs.existsSync(themeDirectory)) {
+        fs.mkdirSync(themeDirectory, { recursive: true })
+    } else {
+        for (const file of fs.readdirSync(themeDirectory)) {
+            if (file.endsWith(".json")) {
+                const name = file.replace(/\.json$/, "")
+                if (
+                    !colorSchemes.find(
+                        (colorScheme) => colorScheme.name === name
+                    )
+                ) {
+                    fs.unlinkSync(path.join(themeDirectory, file))
+                }
+            }
         }
-      }
     }
-  }
 }
 
-clearThemes(themeDirectory);
-clearThemes(staffDirectory);
+clearThemes(themeDirectory)
+clearThemes(staffDirectory)
 
 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, `${colorScheme.name}.json`);
-    let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
-    fs.writeFileSync(tempPath, styleTreeJSON);
-    fs.renameSync(tempPath, outPath);
-    console.log(`- ${outPath} created`);
-  }
+    for (let colorScheme of colorSchemes) {
+        let styleTree = snakeCase(app(colorScheme))
+        let styleTreeJSON = JSON.stringify(styleTree, null, 2)
+        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`)
+    }
 }
 
 // Write new themes to theme directory
-writeThemes(colorSchemes, themeDirectory);
-writeThemes(staffColorSchemes, staffDirectory);
+writeThemes(colorSchemes, themeDirectory)
+writeThemes(staffColorSchemes, staffDirectory)

styles/src/colorSchemes.ts πŸ”—

@@ -1,54 +1,54 @@
-import fs from "fs";
-import path from "path";
-import { ColorScheme, Meta } from "./themes/common/colorScheme";
+import fs from "fs"
+import path from "path"
+import { ColorScheme, Meta } from "./themes/common/colorScheme"
 
-const colorSchemes: ColorScheme[] = [];
-export default colorSchemes;
+const colorSchemes: ColorScheme[] = []
+export default colorSchemes
 
-const schemeMeta: Meta[] = [];
-export { schemeMeta };
+const schemeMeta: Meta[] = []
+export { schemeMeta }
 
-const staffColorSchemes: ColorScheme[] = [];
-export { staffColorSchemes };
+const staffColorSchemes: ColorScheme[] = []
+export { staffColorSchemes }
 
-const experimentalColorSchemes: ColorScheme[] = [];
-export { experimentalColorSchemes };
+const experimentalColorSchemes: ColorScheme[] = []
+export { experimentalColorSchemes }
 
-const themes_directory = path.resolve(`${__dirname}/themes`);
+const themes_directory = path.resolve(`${__dirname}/themes`)
 
-function for_all_color_schemes_in(themesPath: string, callback: (module: any, path: string) => void) {
-  for (const fileName of fs.readdirSync(themesPath)) {
-    if (fileName == "template.ts") continue;
-    const filePath = path.join(themesPath, fileName);
+function for_all_color_schemes_in(
+    themesPath: string,
+    callback: (module: any, path: string) => void
+) {
+    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);
-      callback(colorScheme, path.basename(filePath));
+        if (fs.statSync(filePath).isFile()) {
+            const colorScheme = require(filePath)
+            callback(colorScheme, path.basename(filePath))
+        }
     }
-  }
 }
 
 function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
-  for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
-    if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
-    if (colorScheme.light) colorSchemes.push(colorScheme.light);
-  })
+    for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
+        if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
+        if (colorScheme.light) colorSchemes.push(colorScheme.light)
+    })
 }
 
-fillColorSchemes(themes_directory, colorSchemes);
-fillColorSchemes(
-  path.resolve(`${themes_directory}/staff`),
-  staffColorSchemes
-);
+fillColorSchemes(themes_directory, colorSchemes)
+fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
 
 function fillMeta(themesPath: string, meta: Meta[]) {
-  for_all_color_schemes_in(themesPath, (colorScheme, path) => {
-    if (colorScheme.meta) {
-      meta.push(colorScheme.meta)
-    } else {
-      throw Error(`Public theme ${path} must have a meta field`)
-    }
-  })
+    for_all_color_schemes_in(themesPath, (colorScheme, path) => {
+        if (colorScheme.meta) {
+            meta.push(colorScheme.meta)
+        } else {
+            throw Error(`Public theme ${path} must have a meta field`)
+        }
+    })
 }
 
-fillMeta(themes_directory, schemeMeta);
+fillMeta(themes_directory, schemeMeta)

styles/src/common.ts πŸ”—

@@ -1,66 +1,45 @@
 export const fontFamilies = {
-  sans: "Zed Sans",
-  mono: "Zed Mono",
-};
+    sans: "Zed Sans",
+    mono: "Zed Mono",
+}
 
 export const fontSizes = {
-  "3xs": 8,
-  "2xs": 10,
-  xs: 12,
-  sm: 14,
-  md: 16,
-  lg: 18,
-  xl: 20,
-};
+    "3xs": 8,
+    "2xs": 10,
+    xs: 12,
+    sm: 14,
+    md: 16,
+    lg: 18,
+    xl: 20,
+}
 
 export type FontWeight =
-  | "thin"
-  | "extra_light"
-  | "light"
-  | "normal"
-  | "medium"
-  | "semibold"
-  | "bold"
-  | "extra_bold"
-  | "black";
+    | "thin"
+    | "extra_light"
+    | "light"
+    | "normal"
+    | "medium"
+    | "semibold"
+    | "bold"
+    | "extra_bold"
+    | "black"
 export const fontWeights: { [key: string]: FontWeight } = {
-  thin: "thin",
-  extra_light: "extra_light",
-  light: "light",
-  normal: "normal",
-  medium: "medium",
-  semibold: "semibold",
-  bold: "bold",
-  extra_bold: "extra_bold",
-  black: "black",
-};
+    thin: "thin",
+    extra_light: "extra_light",
+    light: "light",
+    normal: "normal",
+    medium: "medium",
+    semibold: "semibold",
+    bold: "bold",
+    extra_bold: "extra_bold",
+    black: "black",
+}
 
 export const sizes = {
-  px: 1,
-  xs: 2,
-  sm: 4,
-  md: 6,
-  lg: 8,
-  xl: 12,
-};
-
-// export const colors = {
-//   neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
-//   rose: colorRamp("#F43F5EFF"),
-//   red: colorRamp("#EF4444FF"),
-//   orange: colorRamp("#F97316FF"),
-//   amber: colorRamp("#F59E0BFF"),
-//   yellow: colorRamp("#EAB308FF"),
-//   lime: colorRamp("#84CC16FF"),
-//   green: colorRamp("#22C55EFF"),
-//   emerald: colorRamp("#10B981FF"),
-//   teal: colorRamp("#14B8A6FF"),
-//   cyan: colorRamp("#06BBD4FF"),
-//   sky: colorRamp("#0EA5E9FF"),
-//   blue: colorRamp("#3B82F6FF"),
-//   indigo: colorRamp("#6366F1FF"),
-//   violet: colorRamp("#8B5CF6FF"),
-//   purple: colorRamp("#A855F7FF"),
-//   fuschia: colorRamp("#D946E4FF"),
-//   pink: colorRamp("#EC4899FF"),
-// }
+    px: 1,
+    xs: 2,
+    sm: 4,
+    md: 6,
+    lg: 8,
+    xl: 12,
+}

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

@@ -1,72 +1,72 @@
-import { text } from "./components";
-import contactFinder from "./contactFinder";
-import contactsPopover from "./contactsPopover";
-import commandPalette from "./commandPalette";
-import editor from "./editor";
-import projectPanel from "./projectPanel";
-import search from "./search";
-import picker from "./picker";
-import workspace from "./workspace";
-import contextMenu from "./contextMenu";
-import sharedScreen from "./sharedScreen";
-import projectDiagnostics from "./projectDiagnostics";
-import contactNotification from "./contactNotification";
-import updateNotification from "./updateNotification";
-import simpleMessageNotification from "./simpleMessageNotification";
-import projectSharedNotification from "./projectSharedNotification";
-import tooltip from "./tooltip";
-import terminal from "./terminal";
-import contactList from "./contactList";
-import incomingCallNotification from "./incomingCallNotification";
-import { ColorScheme } from "../themes/common/colorScheme";
-import feedback from "./feedback";
+import { text } from "./components"
+import contactFinder from "./contactFinder"
+import contactsPopover from "./contactsPopover"
+import commandPalette from "./commandPalette"
+import editor from "./editor"
+import projectPanel from "./projectPanel"
+import search from "./search"
+import picker from "./picker"
+import workspace from "./workspace"
+import contextMenu from "./contextMenu"
+import sharedScreen from "./sharedScreen"
+import projectDiagnostics from "./projectDiagnostics"
+import contactNotification from "./contactNotification"
+import updateNotification from "./updateNotification"
+import simpleMessageNotification from "./simpleMessageNotification"
+import projectSharedNotification from "./projectSharedNotification"
+import tooltip from "./tooltip"
+import terminal from "./terminal"
+import contactList from "./contactList"
+import incomingCallNotification from "./incomingCallNotification"
+import { ColorScheme } from "../themes/common/colorScheme"
+import feedback from "./feedback"
 
 export default function app(colorScheme: ColorScheme): Object {
-  return {
-    meta: {
-      name: colorScheme.name,
-      isLight: colorScheme.isLight,
-    },
-    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),
-    sharedScreen: sharedScreen(colorScheme),
-    breadcrumbs: {
-      ...text(colorScheme.highest, "sans", "variant"),
-      padding: {
-        left: 6,
-      },
-    },
-    updateNotification: updateNotification(colorScheme),
-    simpleMessageNotification: simpleMessageNotification(colorScheme),
-    tooltip: tooltip(colorScheme),
-    terminal: terminal(colorScheme),
-    feedback: feedback(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"),
-      },
-    },
-  };
+    return {
+        meta: {
+            name: colorScheme.name,
+            isLight: colorScheme.isLight,
+        },
+        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),
+        sharedScreen: sharedScreen(colorScheme),
+        breadcrumbs: {
+            ...text(colorScheme.highest, "sans", "variant"),
+            padding: {
+                left: 6,
+            },
+        },
+        updateNotification: updateNotification(colorScheme),
+        simpleMessageNotification: simpleMessageNotification(colorScheme),
+        tooltip: tooltip(colorScheme),
+        terminal: terminal(colorScheme),
+        feedback: feedback(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/commandPalette.ts πŸ”—

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

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

@@ -1,210 +1,210 @@
-import { fontFamilies, fontSizes, FontWeight } from "../common";
-import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
+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);
+    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);
+    return [
+        "default",
+        "active",
+        "disabled",
+        "hovered",
+        "pressed",
+        "inverted",
+    ].includes(key)
 }
 function getStyle(
-  layer: Layer,
-  possibleStyleSetOrStyle?: any,
-  possibleStyle?: any
+    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];
+    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, style?: Styles): string
 export function background(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function background(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).background;
+    return getStyle(layer, styleSetOrStyles, style).background
 }
 
-export function borderColor(layer: Layer, style?: Styles): string;
+export function borderColor(layer: Layer, style?: Styles): string
 export function borderColor(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function borderColor(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).border;
+    return getStyle(layer, styleSetOrStyles, style).border
 }
 
-export function foreground(layer: Layer, style?: Styles): string;
+export function foreground(layer: Layer, style?: Styles): string
 export function foreground(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function foreground(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).foreground;
+    return getStyle(layer, styleSetOrStyles, style).foreground
 }
 
 interface Text {
-  family: keyof typeof fontFamilies;
-  color: string;
-  size: number;
-  weight?: FontWeight;
-  underline?: boolean;
+    family: keyof typeof fontFamilies
+    color: string
+    size: number
+    weight?: FontWeight
+    underline?: boolean
 }
 
 interface TextProperties {
-  size?: keyof typeof fontSizes;
-  weight?: FontWeight;
-  underline?: boolean;
-  color?: string;
+    size?: keyof typeof fontSizes
+    weight?: FontWeight
+    underline?: boolean
+    color?: string
 }
 
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  styleSet: StyleSets,
-  style: Styles,
-  properties?: TextProperties
-): Text;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    styleSet: StyleSets,
+    style: Styles,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  styleSet: StyleSets,
-  properties?: TextProperties
-): 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;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    style: Styles,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  properties?: TextProperties
-): 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
+    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"];
-  let color = properties?.color || style.foreground;
-
-  return {
-    family: fontFamilies[fontFamily],
-    ...properties,
-    color,
-    size,
-  };
+    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+
+    if (typeof styleSetStyleOrProperties === "object") {
+        properties = styleSetStyleOrProperties
+    }
+    if (typeof styleOrProperties === "object") {
+        properties = styleOrProperties
+    }
+
+    let size = fontSizes[properties?.size || "sm"]
+    let color = properties?.color || style.foreground
+
+    return {
+        family: fontFamilies[fontFamily],
+        ...properties,
+        color,
+        size,
+    }
 }
 
 export interface Border {
-  color: string;
-  width: number;
-  top?: boolean;
-  bottom?: boolean;
-  left?: boolean;
-  right?: boolean;
-  overlay?: boolean;
+    color: string
+    width: number
+    top?: boolean
+    bottom?: boolean
+    left?: boolean
+    right?: boolean
+    overlay?: boolean
 }
 
 export interface BorderProperties {
-  width?: number;
-  top?: boolean;
-  bottom?: boolean;
-  left?: boolean;
-  right?: boolean;
-  overlay?: boolean;
+    width?: number
+    top?: boolean
+    bottom?: boolean
+    left?: boolean
+    right?: boolean
+    overlay?: boolean
 }
 
 export function border(
-  layer: Layer,
-  styleSet: StyleSets,
-  style: Styles,
-  properties?: BorderProperties
-): Border;
+    layer: Layer,
+    styleSet: StyleSets,
+    style: Styles,
+    properties?: BorderProperties
+): Border
 export function border(
-  layer: Layer,
-  styleSet: StyleSets,
-  properties?: BorderProperties
-): 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;
+    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
+    layer: Layer,
+    styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
+    styleOrProperties?: Styles | BorderProperties,
+    properties?: BorderProperties
 ): Border {
-  let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
-
-  if (typeof styleSetStyleOrProperties === "object") {
-    properties = styleSetStyleOrProperties;
-  }
-  if (typeof styleOrProperties === "object") {
-    properties = styleOrProperties;
-  }
-
-  return {
-    color: style.border,
-    width: 1,
-    ...properties,
-  };
+    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+
+    if (typeof styleSetStyleOrProperties === "object") {
+        properties = styleSetStyleOrProperties
+    }
+    if (typeof styleOrProperties === "object") {
+        properties = styleOrProperties
+    }
+
+    return {
+        color: style.border,
+        width: 1,
+        ...properties,
+    }
 }

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

@@ -1,70 +1,70 @@
-import picker from "./picker";
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, foreground, text } from "./components";
+import picker from "./picker"
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, text } from "./components"
 
 export default function contactFinder(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
+    let layer = colorScheme.middle
 
-  const sideMargin = 6;
-  const contactButton = {
-    background: background(layer, "variant"),
-    color: foreground(layer, "variant"),
-    iconWidth: 8,
-    buttonWidth: 16,
-    cornerRadius: 8,
-  };
+    const sideMargin = 6
+    const contactButton = {
+        background: background(layer, "variant"),
+        color: foreground(layer, "variant"),
+        iconWidth: 8,
+        buttonWidth: 16,
+        cornerRadius: 8,
+    }
 
-  const pickerStyle = picker(colorScheme);
-  const pickerInput = {
-    background: background(layer, "on"),
-    cornerRadius: 6,
-    text: text(layer, "mono",),
-    placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
-    selection: colorScheme.players[0],
-    border: border(layer),
-    padding: {
-      bottom: 4,
-      left: 8,
-      right: 8,
-      top: 4,
-    },
-    margin: {
-      left: sideMargin,
-      right: sideMargin,
+    const pickerStyle = picker(colorScheme)
+    const pickerInput = {
+        background: background(layer, "on"),
+        cornerRadius: 6,
+        text: text(layer, "mono"),
+        placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
+        selection: colorScheme.players[0],
+        border: border(layer),
+        padding: {
+            bottom: 4,
+            left: 8,
+            right: 8,
+            top: 4,
+        },
+        margin: {
+            left: sideMargin,
+            right: sideMargin,
+        },
     }
-  };
 
-  return {
-    picker: {
-      emptyContainer: {},
-      item: {
-        ...pickerStyle.item,
-        margin: { left: sideMargin, right: sideMargin },
-      },
-      noMatches: pickerStyle.noMatches,
-      inputEditor: pickerInput,
-      emptyInputEditor: pickerInput
-    },
-    rowHeight: 28,
-    contactAvatar: {
-      cornerRadius: 10,
-      width: 18,
-    },
-    contactUsername: {
-      padding: {
-        left: 8,
-      },
-    },
-    contactButton: {
-      ...contactButton,
-      hover: {
-        background: background(layer, "variant", "hovered"),
-      },
-    },
-    disabledContactButton: {
-      ...contactButton,
-      background: background(layer, "disabled"),
-      color: foreground(layer, "disabled"),
-    },
-  };
+    return {
+        picker: {
+            emptyContainer: {},
+            item: {
+                ...pickerStyle.item,
+                margin: { left: sideMargin, right: sideMargin },
+            },
+            noMatches: pickerStyle.noMatches,
+            inputEditor: pickerInput,
+            emptyInputEditor: pickerInput,
+        },
+        rowHeight: 28,
+        contactAvatar: {
+            cornerRadius: 10,
+            width: 18,
+        },
+        contactUsername: {
+            padding: {
+                left: 8,
+            },
+        },
+        contactButton: {
+            ...contactButton,
+            hover: {
+                background: background(layer, "variant", "hovered"),
+            },
+        },
+        disabledContactButton: {
+            ...contactButton,
+            background: background(layer, "disabled"),
+            color: foreground(layer, "disabled"),
+        },
+    }
 }

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

@@ -1,186 +1,182 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, borderColor, foreground, text } from "./components"
 
 export default function contactsPanel(colorScheme: ColorScheme) {
-  const nameMargin = 8;
-  const sidePadding = 12;
+    const nameMargin = 8
+    const sidePadding = 12
 
-  let layer = colorScheme.middle;
+    let layer = colorScheme.middle
 
-  const contactButton = {
-    background: background(layer, "on"),
-    color: foreground(layer, "on"),
-    iconWidth: 8,
-    buttonWidth: 16,
-    cornerRadius: 8,
-  };
-  const projectRow = {
-    guestAvatarSpacing: 4,
-    height: 24,
-    guestAvatar: {
-      cornerRadius: 8,
-      width: 14,
-    },
-    name: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: {
-        left: nameMargin,
-        right: 6,
-      },
-    },
-    guests: {
-      margin: {
-        left: nameMargin,
-        right: nameMargin,
-      },
-    },
-    padding: {
-      left: sidePadding,
-      right: sidePadding,
-    },
-  };
+    const contactButton = {
+        background: background(layer, "on"),
+        color: foreground(layer, "on"),
+        iconWidth: 8,
+        buttonWidth: 16,
+        cornerRadius: 8,
+    }
+    const projectRow = {
+        guestAvatarSpacing: 4,
+        height: 24,
+        guestAvatar: {
+            cornerRadius: 8,
+            width: 14,
+        },
+        name: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: nameMargin,
+                right: 6,
+            },
+        },
+        guests: {
+            margin: {
+                left: nameMargin,
+                right: nameMargin,
+            },
+        },
+        padding: {
+            left: sidePadding,
+            right: sidePadding,
+        },
+    }
 
-  return {
-    background: background(layer),
-    padding: { top: 12, bottom: 0 },
-    userQueryEditor: {
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      text: text(layer, "mono", "on"),
-      placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
-      selection: colorScheme.players[0],
-      border: border(layer, "on"),
-      padding: {
-        bottom: 4,
-        left: 8,
-        right: 8,
-        top: 4,
-      },
-      margin: {
-        left: 6,
-      },
-    },
-    userQueryEditorHeight: 33,
-    addContactButton: {
-      margin: { left: 6, right: 12 },
-      color: foreground(layer, "on"),
-      buttonWidth: 28,
-      iconWidth: 16,
-    },
-    rowHeight: 28,
-    sectionIconSize: 8,
-    headerRow: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: { top: 14 },
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      active: {
-        ...text(layer, "mono", "active", { size: "sm" }),
-        background: background(layer, "active"),
-      },
-    },
-    leaveCall: {
-      background: background(layer),
-      border: border(layer),
-      cornerRadius: 6,
-      margin: {
-        top: 1,
-      },
-      padding: {
-        top: 1,
-        bottom: 1,
-        left: 7,
-        right: 7,
-      },
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      hover: {
-        ...text(layer, "sans", "hovered", { size: "xs" }),
-        background: background(layer, "hovered"),
-        border: border(layer, "hovered"),
-      },
-    },
-    contactRow: {
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      active: {
-        background: background(layer, "active"),
-      },
-    },
-    contactAvatar: {
-      cornerRadius: 10,
-      width: 18,
-    },
-    contactStatusFree: {
-      cornerRadius: 4,
-      padding: 4,
-      margin: { top: 12, left: 12 },
-      background: foreground(layer, "positive"),
-    },
-    contactStatusBusy: {
-      cornerRadius: 4,
-      padding: 4,
-      margin: { top: 12, left: 12 },
-      background: foreground(layer, "negative"),
-    },
-    contactUsername: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: {
-        left: nameMargin,
-      },
-    },
-    contactButtonSpacing: nameMargin,
-    contactButton: {
-      ...contactButton,
-      hover: {
-        background: background(layer, "hovered"),
-      },
-    },
-    disabledButton: {
-      ...contactButton,
-      background: background(layer, "on"),
-      color: foreground(layer, "on"),
-    },
-    callingIndicator: {
-      ...text(layer, "mono", "variant", { size: "xs" }),
-    },
-    treeBranch: {
-      color: borderColor(layer),
-      width: 1,
-      hover: {
-        color: borderColor(layer),
-      },
-      active: {
-        color: borderColor(layer),
-      },
-    },
-    projectRow: {
-      ...projectRow,
-      background: background(layer),
-      icon: {
-        margin: { left: nameMargin },
-        color: foreground(layer, "variant"),
-        width: 12,
-      },
-      name: {
-        ...projectRow.name,
-        ...text(layer, "mono", { size: "sm" }),
-      },
-      hover: {
-        background: background(layer, "hovered"),
-      },
-      active: {
-        background: background(layer, "active"),
-      },
-    },
-  };
+    return {
+        background: background(layer),
+        padding: { top: 12, bottom: 0 },
+        userQueryEditor: {
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            text: text(layer, "mono", "on"),
+            placeholderText: text(layer, "mono", "on", "disabled", {
+                size: "xs",
+            }),
+            selection: colorScheme.players[0],
+            border: border(layer, "on"),
+            padding: {
+                bottom: 4,
+                left: 8,
+                right: 8,
+                top: 4,
+            },
+            margin: {
+                left: 6,
+            },
+        },
+        userQueryEditorHeight: 33,
+        addContactButton: {
+            margin: { left: 6, right: 12 },
+            color: foreground(layer, "on"),
+            buttonWidth: 28,
+            iconWidth: 16,
+        },
+        rowHeight: 28,
+        sectionIconSize: 8,
+        headerRow: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: { top: 14 },
+            padding: {
+                left: sidePadding,
+                right: sidePadding,
+            },
+            active: {
+                ...text(layer, "mono", "active", { size: "sm" }),
+                background: background(layer, "active"),
+            },
+        },
+        leaveCall: {
+            background: background(layer),
+            border: border(layer),
+            cornerRadius: 6,
+            margin: {
+                top: 1,
+            },
+            padding: {
+                top: 1,
+                bottom: 1,
+                left: 7,
+                right: 7,
+            },
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            hover: {
+                ...text(layer, "sans", "hovered", { size: "xs" }),
+                background: background(layer, "hovered"),
+                border: border(layer, "hovered"),
+            },
+        },
+        contactRow: {
+            padding: {
+                left: sidePadding,
+                right: sidePadding,
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+        },
+        contactAvatar: {
+            cornerRadius: 10,
+            width: 18,
+        },
+        contactStatusFree: {
+            cornerRadius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "positive"),
+        },
+        contactStatusBusy: {
+            cornerRadius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "negative"),
+        },
+        contactUsername: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: nameMargin,
+            },
+        },
+        contactButtonSpacing: nameMargin,
+        contactButton: {
+            ...contactButton,
+            hover: {
+                background: background(layer, "hovered"),
+            },
+        },
+        disabledButton: {
+            ...contactButton,
+            background: background(layer, "on"),
+            color: foreground(layer, "on"),
+        },
+        callingIndicator: {
+            ...text(layer, "mono", "variant", { size: "xs" }),
+        },
+        treeBranch: {
+            color: borderColor(layer),
+            width: 1,
+            hover: {
+                color: borderColor(layer),
+            },
+            active: {
+                color: borderColor(layer),
+            },
+        },
+        projectRow: {
+            ...projectRow,
+            background: background(layer),
+            icon: {
+                margin: { left: nameMargin },
+                color: foreground(layer, "variant"),
+                width: 12,
+            },
+            name: {
+                ...projectRow.name,
+                ...text(layer, "mono", { size: "sm" }),
+            },
+            hover: {
+                background: background(layer, "hovered"),
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+        },
+    }
 }

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

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

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

@@ -1,29 +1,29 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function contactsPopover(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  const sidePadding = 12;
-  return {
-    background: background(layer),
-    cornerRadius: 6,
-    padding: { top: 6 },
-    margin: { top: -6 },
-    shadow: colorScheme.popoverShadow,
-    border: border(layer),
-    width: 300,
-    height: 400,
-    inviteRowHeight: 28,
-    inviteRow: {
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      border: border(layer, { top: true }),
-      text: text(layer, "sans", "variant", { size: "sm" }),
-      hover: {
-        text: text(layer, "sans", "hovered", { size: "sm" }),
-      },
-    },
-  }
+    let layer = colorScheme.middle
+    const sidePadding = 12
+    return {
+        background: background(layer),
+        cornerRadius: 6,
+        padding: { top: 6 },
+        margin: { top: -6 },
+        shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        width: 300,
+        height: 400,
+        inviteRowHeight: 28,
+        inviteRow: {
+            padding: {
+                left: sidePadding,
+                right: sidePadding,
+            },
+            border: border(layer, { top: true }),
+            text: text(layer, "sans", "variant", { size: "sm" }),
+            hover: {
+                text: text(layer, "sans", "hovered", { size: "sm" }),
+            },
+        },
+    }
 }

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

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

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

@@ -1,285 +1,317 @@
-import { fontWeights } from "../common";
-import { withOpacity } from "../utils/color";
-import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme";
+import { fontWeights } from "../common"
+import { withOpacity } from "../utils/color"
 import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
-import hoverPopover from "./hoverPopover";
+    ColorScheme,
+    Layer,
+    StyleSets,
+    Syntax,
+    ThemeSyntax,
+} from "../themes/common/colorScheme"
+import { background, border, borderColor, foreground, text } from "./components"
+import hoverPopover from "./hoverPopover"
+
+import deepmerge from "deepmerge"
 
 export default function editor(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
 
-  const autocompleteItem = {
-    cornerRadius: 6,
-    padding: {
-      bottom: 2,
-      left: 6,
-      right: 6,
-      top: 2,
-    },
-  };
+    const autocompleteItem = {
+        cornerRadius: 6,
+        padding: {
+            bottom: 2,
+            left: 6,
+            right: 6,
+            top: 2,
+        },
+    }
 
-  function diagnostic(layer: Layer, styleSet: StyleSets) {
-    return {
-      textScaleFactor: 0.857,
-      header: {
-        border: border(layer, {
-          top: true,
-        }),
-      },
-      message: {
-        text: text(layer, "sans", styleSet, "default", { size: "sm" }),
-        highlightText: text(layer, "sans", styleSet, "default", {
-          size: "sm",
-          weight: "bold",
-        }),
-      },
-    };
-  }
+    function diagnostic(layer: Layer, styleSet: StyleSets) {
+        return {
+            textScaleFactor: 0.857,
+            header: {
+                border: border(layer, {
+                    top: true,
+                }),
+            },
+            message: {
+                text: text(layer, "sans", styleSet, "default", { size: "sm" }),
+                highlightText: text(layer, "sans", styleSet, "default", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        }
+    }
+
+    const defaultSyntax: Syntax = {
+        primary: {
+            color: colorScheme.ramps.neutral(1).hex(),
+            weight: fontWeights.normal,
+        },
+        "variable.special": {
+            // Highlights for self, this, etc
+            color: colorScheme.ramps.blue(0.7).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,
+        },
+    }
+
+    function createSyntax(colorScheme: ColorScheme): Syntax {
+        if (!colorScheme.syntax) {
+            return defaultSyntax
+        }
 
-  const syntax = {
-    primary: {
-      color: colorScheme.ramps.neutral(1).hex(),
-      weight: fontWeights.normal,
-    },
-    "variable.special": {
-      // Highlights for self, this, etc
-      color: colorScheme.ramps.blue(0.7).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 deepmerge<Syntax, Partial<ThemeSyntax>>(
+            defaultSyntax,
+            colorScheme.syntax,
+            {
+                arrayMerge: (destinationArray, sourceArray) => [
+                    ...destinationArray,
+                    ...sourceArray,
+                ],
+            }
+        )
+    }
 
-  return {
-    textColor: syntax.primary.color,
-    background: background(layer),
-    activeLineBackground: withOpacity(background(layer, "on"), 0.75),
-    highlightedLineBackground: background(layer, "on"),
-    codeActions: {
-      indicator: foreground(layer, "variant"),
-      verticalScale: 0.55,
-    },
-    diff: {
-      deleted: foreground(layer, "negative"),
-      modified: foreground(layer, "warning"),
-      inserted: foreground(layer, "positive"),
-      removedWidthEm: 0.275,
-      widthEm: 0.16,
-      cornerRadius: 0.05,
-    },
-    /** Highlights matching occurences of what is under the cursor
-     * as well as matched brackets
-     */
-    documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
-    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,
-    lineNumber: withOpacity(foreground(layer), 0.35),
-    lineNumberActive: foreground(layer),
-    renameFade: 0.6,
-    unnecessaryCodeFade: 0.5,
-    selection: colorScheme.players[0],
-    guestSelections: [
-      colorScheme.players[1],
-      colorScheme.players[2],
-      colorScheme.players[3],
-      colorScheme.players[4],
-      colorScheme.players[5],
-      colorScheme.players[6],
-      colorScheme.players[7],
-    ],
-    autocomplete: {
-      background: background(colorScheme.middle),
-      cornerRadius: 8,
-      padding: 4,
-      margin: {
-        left: -14,
-      },
-      border: border(colorScheme.middle),
-      shadow: colorScheme.popoverShadow,
-      matchHighlight: foreground(colorScheme.middle, "accent"),
-      item: autocompleteItem,
-      hoveredItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
-        background: background(colorScheme.middle, "hovered"),
-      },
-      selectedItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(colorScheme.middle, "accent", "active"),
-        background: background(colorScheme.middle, "active"),
-      },
-    },
-    diagnosticHeader: {
-      background: background(colorScheme.middle),
-      iconWidthFactor: 1.5,
-      textScaleFactor: 0.857,
-      border: border(colorScheme.middle, {
-        bottom: true,
-        top: true,
-      }),
-      code: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 10,
-        },
-      },
-      message: {
-        highlightText: text(colorScheme.middle, "sans", {
-          size: "sm",
-          weight: "bold",
-        }),
-        text: text(colorScheme.middle, "sans", { size: "sm" }),
-      },
-    },
-    diagnosticPathHeader: {
-      background: background(colorScheme.middle),
-      textScaleFactor: 0.857,
-      filename: text(colorScheme.middle, "mono", { size: "sm" }),
-      path: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 12,
-        },
-      },
-    },
-    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: syntax.linkUri.color,
-      underline: syntax.linkUri.underline,
-    },
-    jumpIcon: {
-      color: foreground(layer, "on"),
-      iconWidth: 20,
-      buttonWidth: 20,
-      cornerRadius: 6,
-      padding: {
-        top: 6,
-        bottom: 6,
-        left: 6,
-        right: 6,
-      },
-      hover: {
-        background: background(layer, "on", "hovered"),
-      },
-    },
-    scrollbar: {
-      width: 12,
-      minHeightFactor: 1.0,
-      track: {
-        border: border(layer, "variant", { left: true }),
-      },
-      thumb: {
-        background: withOpacity(background(layer, "inverted"), 0.4),
-        border: {
-          width: 1,
-          color: borderColor(layer, "variant"),
-        },
-      },
-    },
-    compositionMark: {
-      underline: {
-        thickness: 1.0,
-        color: borderColor(layer),
-      },
-    },
-    syntax,
-  };
+    const syntax = createSyntax(colorScheme)
+
+    return {
+        textColor: syntax.primary.color,
+        background: background(layer),
+        activeLineBackground: withOpacity(background(layer, "on"), 0.75),
+        highlightedLineBackground: background(layer, "on"),
+        codeActions: {
+            indicator: foreground(layer, "variant"),
+            verticalScale: 0.55,
+        },
+        diff: {
+            deleted: foreground(layer, "negative"),
+            modified: foreground(layer, "warning"),
+            inserted: foreground(layer, "positive"),
+            removedWidthEm: 0.275,
+            widthEm: 0.16,
+            cornerRadius: 0.05,
+        },
+        /** Highlights matching occurences of what is under the cursor
+         * as well as matched brackets
+         */
+        documentHighlightReadBackground: withOpacity(
+            foreground(layer, "accent"),
+            0.1
+        ),
+        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,
+        lineNumber: withOpacity(foreground(layer), 0.35),
+        lineNumberActive: foreground(layer),
+        renameFade: 0.6,
+        unnecessaryCodeFade: 0.5,
+        selection: colorScheme.players[0],
+        guestSelections: [
+            colorScheme.players[1],
+            colorScheme.players[2],
+            colorScheme.players[3],
+            colorScheme.players[4],
+            colorScheme.players[5],
+            colorScheme.players[6],
+            colorScheme.players[7],
+        ],
+        autocomplete: {
+            background: background(colorScheme.middle),
+            cornerRadius: 8,
+            padding: 4,
+            margin: {
+                left: -14,
+            },
+            border: border(colorScheme.middle),
+            shadow: colorScheme.popoverShadow,
+            matchHighlight: foreground(colorScheme.middle, "accent"),
+            item: autocompleteItem,
+            hoveredItem: {
+                ...autocompleteItem,
+                matchHighlight: foreground(
+                    colorScheme.middle,
+                    "accent",
+                    "hovered"
+                ),
+                background: background(colorScheme.middle, "hovered"),
+            },
+            selectedItem: {
+                ...autocompleteItem,
+                matchHighlight: foreground(
+                    colorScheme.middle,
+                    "accent",
+                    "active"
+                ),
+                background: background(colorScheme.middle, "active"),
+            },
+        },
+        diagnosticHeader: {
+            background: background(colorScheme.middle),
+            iconWidthFactor: 1.5,
+            textScaleFactor: 0.857,
+            border: border(colorScheme.middle, {
+                bottom: true,
+                top: true,
+            }),
+            code: {
+                ...text(colorScheme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 10,
+                },
+            },
+            message: {
+                highlightText: text(colorScheme.middle, "sans", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+                text: text(colorScheme.middle, "sans", { size: "sm" }),
+            },
+        },
+        diagnosticPathHeader: {
+            background: background(colorScheme.middle),
+            textScaleFactor: 0.857,
+            filename: text(colorScheme.middle, "mono", { size: "sm" }),
+            path: {
+                ...text(colorScheme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 12,
+                },
+            },
+        },
+        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: syntax.linkUri.color,
+            underline: syntax.linkUri.underline,
+        },
+        jumpIcon: {
+            color: foreground(layer, "on"),
+            iconWidth: 20,
+            buttonWidth: 20,
+            cornerRadius: 6,
+            padding: {
+                top: 6,
+                bottom: 6,
+                left: 6,
+                right: 6,
+            },
+            hover: {
+                background: background(layer, "on", "hovered"),
+            },
+        },
+        scrollbar: {
+            width: 12,
+            minHeightFactor: 1.0,
+            track: {
+                border: border(layer, "variant", { left: true }),
+            },
+            thumb: {
+                background: withOpacity(background(layer, "inverted"), 0.4),
+                border: {
+                    width: 1,
+                    color: borderColor(layer, "variant"),
+                },
+            },
+        },
+        compositionMark: {
+            underline: {
+                thickness: 1.0,
+                color: borderColor(layer),
+            },
+        },
+        syntax,
+    }
 }

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

@@ -1,39 +1,38 @@
-
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function feedback(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
 
-  return {
-    submit_button: {
-      ...text(layer, "mono", "on"),
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      border: border(layer, "on"),
-      margin: {
-        right: 4,
-      },
-      padding: {
-        bottom: 2,
-        left: 10,
-        right: 10,
-        top: 2,
-      },
-      clicked: {
-        ...text(layer, "mono", "on", "pressed"),
-        background: background(layer, "on", "pressed"),
-        border: border(layer, "on", "pressed"),
-      },
-      hover: {
-        ...text(layer, "mono", "on", "hovered"),
-        background: background(layer, "on", "hovered"),
-        border: border(layer, "on", "hovered"),
-      },
-    },
-    button_margin: 8,
-    info_text_default: text(layer, "sans", "default", { size: "xs" }),
-    link_text_default: text(layer, "sans", "default", { size: "xs", underline: true }),
-    link_text_hover: text(layer, "sans", "hovered", { size: "xs", underline: true })
-  };
+    return {
+        submit_button: {
+            ...text(layer, "mono", "on"),
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            border: border(layer, "on"),
+            margin: {
+                right: 4,
+            },
+            padding: {
+                bottom: 2,
+                left: 10,
+                right: 10,
+                top: 2,
+            },
+            clicked: {
+                ...text(layer, "mono", "on", "pressed"),
+                background: background(layer, "on", "pressed"),
+                border: border(layer, "on", "pressed"),
+            },
+            hover: {
+                ...text(layer, "mono", "on", "hovered"),
+                background: background(layer, "on", "hovered"),
+                border: border(layer, "on", "hovered"),
+            },
+        },
+        button_margin: 8,
+        info_text_default: text(layer, "sans", "default", { size: "xs" }),
+        link_text_default: text(layer, "sans", "default", { size: "xs", underline: true }),
+        link_text_hover: text(layer, "sans", "hovered", { size: "xs", underline: true })
+    }
 }

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

@@ -1,45 +1,45 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function HoverPopover(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  let baseContainer = {
-    background: background(layer),
-    cornerRadius: 8,
-    padding: {
-      left: 8,
-      right: 8,
-      top: 4,
-      bottom: 4,
-    },
-    shadow: colorScheme.popoverShadow,
-    border: border(layer),
-    margin: {
-      left: -8,
-    },
-  };
+    let layer = colorScheme.middle
+    let baseContainer = {
+        background: background(layer),
+        cornerRadius: 8,
+        padding: {
+            left: 8,
+            right: 8,
+            top: 4,
+            bottom: 4,
+        },
+        shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        margin: {
+            left: -8,
+        },
+    }
 
-  return {
-    container: baseContainer,
-    infoContainer: {
-      ...baseContainer,
-      background: background(layer, "accent"),
-      border: border(layer, "accent"),
-    },
-    warningContainer: {
-      ...baseContainer,
-      background: background(layer, "warning"),
-      border: border(layer, "warning"),
-    },
-    errorContainer: {
-      ...baseContainer,
-      background: background(layer, "negative"),
-      border: border(layer, "negative"),
-    },
-    block_style: {
-      padding: { top: 4 },
-    },
-    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
-  };
+    return {
+        container: baseContainer,
+        infoContainer: {
+            ...baseContainer,
+            background: background(layer, "accent"),
+            border: border(layer, "accent"),
+        },
+        warningContainer: {
+            ...baseContainer,
+            background: background(layer, "warning"),
+            border: border(layer, "warning"),
+        },
+        errorContainer: {
+            ...baseContainer,
+            background: background(layer, "negative"),
+            border: border(layer, "negative"),
+        },
+        block_style: {
+            padding: { top: 4 },
+        },
+        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,45 +1,53 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
-export default function incomingCallNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
-  const avatarSize = 48;
-  return {
-    windowHeight: 74,
-    windowWidth: 380,
-    background: background(layer),
-    callerContainer: {
-      padding: 12,
-    },
-    callerAvatar: {
-      height: avatarSize,
-      width: avatarSize,
-      cornerRadius: avatarSize / 2,
-    },
-    callerMetadata: {
-      margin: { left: 10 },
-    },
-    callerUsername: {
-      ...text(layer, "sans", { size: "sm", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    callerMessage: {
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      margin: { top: -3 },
-    },
-    worktreeRoots: {
-      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    buttonWidth: 96,
-    acceptButton: {
-      background: background(layer, "accent"),
-      border: border(layer, { left: true, bottom: true }),
-      ...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
-    },
-    declineButton: {
-      border: border(layer, { left: true }),
-      ...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
-    },
-  };
+export default function incomingCallNotification(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
+    const avatarSize = 48
+    return {
+        windowHeight: 74,
+        windowWidth: 380,
+        background: background(layer),
+        callerContainer: {
+            padding: 12,
+        },
+        callerAvatar: {
+            height: avatarSize,
+            width: avatarSize,
+            cornerRadius: avatarSize / 2,
+        },
+        callerMetadata: {
+            margin: { left: 10 },
+        },
+        callerUsername: {
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        callerMessage: {
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktreeRoots: {
+            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        buttonWidth: 96,
+        acceptButton: {
+            background: background(layer, "accent"),
+            border: border(layer, { left: true, bottom: true }),
+            ...text(layer, "sans", "positive", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+        declineButton: {
+            border: border(layer, { left: true }),
+            ...text(layer, "sans", "negative", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+    }
 }

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

@@ -1,78 +1,78 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function picker(colorScheme: ColorScheme) {
-  let layer = colorScheme.lowest;
-  const container = {
-    background: background(layer),
-    border: border(layer),
-    shadow: colorScheme.modalShadow,
-    cornerRadius: 12,
-    padding: {
-      bottom: 4,
+    let layer = colorScheme.lowest
+    const container = {
+        background: background(layer),
+        border: border(layer),
+        shadow: colorScheme.modalShadow,
+        cornerRadius: 12,
+        padding: {
+            bottom: 4,
+        },
     }
-  };
-  const inputEditor = {
-    placeholderText: text(layer, "sans", "on", "disabled"),
-    selection: colorScheme.players[0],
-    text: text(layer, "mono", "on"),
-    border: border(layer, { bottom: true }),
-    padding: {
-      bottom: 8,
-      left: 16,
-      right: 16,
-      top: 8,
-    },
-    margin: {
-      bottom: 4,
-    },
-  };
-  const emptyInputEditor = { ...inputEditor };
-  delete emptyInputEditor.border;
-  delete emptyInputEditor.margin;
+    const inputEditor = {
+        placeholderText: text(layer, "sans", "on", "disabled"),
+        selection: colorScheme.players[0],
+        text: text(layer, "mono", "on"),
+        border: border(layer, { bottom: true }),
+        padding: {
+            bottom: 8,
+            left: 16,
+            right: 16,
+            top: 8,
+        },
+        margin: {
+            bottom: 4,
+        },
+    }
+    const emptyInputEditor = { ...inputEditor }
+    delete emptyInputEditor.border
+    delete emptyInputEditor.margin
 
-  return {
-    ...container,
-    emptyContainer: {
-      ...container,
-      padding: {}
-    },
-    item: {
-      padding: {
-        bottom: 4,
-        left: 12,
-        right: 12,
-        top: 4,
-      },
-      margin: {
-        top: 1,
-        left: 4,
-        right: 4,
-      },
-      cornerRadius: 8,
-      text: text(layer, "sans", "variant"),
-      highlightText: text(layer, "sans", "accent", { weight: "bold" }),
-      active: {
-        background: background(layer, "base", "active"),
-        text: text(layer, "sans", "base", "active"),
-        highlightText: text(layer, "sans", "accent", {
-          weight: "bold",
-        }),
-      },
-      hover: {
-        background: background(layer, "hovered"),
-      },
-    },
-    inputEditor,
-    emptyInputEditor,
-    noMatches: {
-      text: text(layer, "sans", "variant"),
-      padding: {
-        bottom: 8,
-        left: 16,
-        right: 16,
-        top: 8,
-      },
-    },
-  };
+    return {
+        ...container,
+        emptyContainer: {
+            ...container,
+            padding: {},
+        },
+        item: {
+            padding: {
+                bottom: 4,
+                left: 12,
+                right: 12,
+                top: 4,
+            },
+            margin: {
+                top: 1,
+                left: 4,
+                right: 4,
+            },
+            cornerRadius: 8,
+            text: text(layer, "sans", "variant"),
+            highlightText: text(layer, "sans", "accent", { weight: "bold" }),
+            active: {
+                background: background(layer, "base", "active"),
+                text: text(layer, "sans", "base", "active"),
+                highlightText: text(layer, "sans", "accent", {
+                    weight: "bold",
+                }),
+            },
+            hover: {
+                background: background(layer, "hovered"),
+            },
+        },
+        inputEditor,
+        emptyInputEditor,
+        noMatches: {
+            text: text(layer, "sans", "variant"),
+            padding: {
+                bottom: 8,
+                left: 16,
+                right: 16,
+                top: 8,
+            },
+        },
+    }
 }

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

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

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

@@ -1,60 +1,60 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, foreground, text } from "./components"
 
 export default function projectPanel(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  
-  let baseEntry = {
-    height: 24,
-    iconColor: foreground(layer, "variant"),
-    iconSize: 8,
-    iconSpacing: 8,
-  }
+    let layer = colorScheme.middle
 
-  let entry = {
-    ...baseEntry,
-    text: text(layer, "mono", "variant", { size: "sm" }),
-    hover: {
-      background: background(layer, "variant", "hovered"),
-    },
-    active: {
-      background: background(layer, "active"),
-      text: text(layer, "mono", "active", { size: "sm" }),
-    },
-    activeHover: {
-      background: background(layer, "active"),
-      text: text(layer, "mono", "active", { size: "sm" }),
-    },
-  };
+    let baseEntry = {
+        height: 24,
+        iconColor: foreground(layer, "variant"),
+        iconSize: 8,
+        iconSpacing: 8,
+    }
 
-  return {
-    background: background(layer),
-    padding: { left: 12, right: 12, top: 6, bottom: 6 },
-    indentWidth: 8,
-    entry,
-    draggedEntry: {
-      ...baseEntry,
-      text: text(layer, "mono", "on", { size: "sm" }),
-      background: withOpacity(background(layer, "on"), 0.9),
-      border: border(layer),
-    },
-    ignoredEntry: {
-      ...entry,
-      text: text(layer, "mono", "disabled"),
-    },
-    cutEntry: {
-      ...entry,
-      text: text(layer, "mono", "disabled"),
-      active: {
-        background: background(layer, "active"),
-        text: text(layer, "mono", "disabled", { size: "sm" }),
-      },
-    },
-    filenameEditor: {
-      background: background(layer, "on"),
-      text: text(layer, "mono", "on", { size: "sm" }),
-      selection: colorScheme.players[0],
-    },
-  };
+    let entry = {
+        ...baseEntry,
+        text: text(layer, "mono", "variant", { size: "sm" }),
+        hover: {
+            background: background(layer, "variant", "hovered"),
+        },
+        active: {
+            background: background(layer, "active"),
+            text: text(layer, "mono", "active", { size: "sm" }),
+        },
+        activeHover: {
+            background: background(layer, "active"),
+            text: text(layer, "mono", "active", { size: "sm" }),
+        },
+    }
+
+    return {
+        background: background(layer),
+        padding: { left: 12, right: 12, top: 6, bottom: 6 },
+        indentWidth: 8,
+        entry,
+        draggedEntry: {
+            ...baseEntry,
+            text: text(layer, "mono", "on", { size: "sm" }),
+            background: withOpacity(background(layer, "on"), 0.9),
+            border: border(layer),
+        },
+        ignoredEntry: {
+            ...entry,
+            text: text(layer, "mono", "disabled"),
+        },
+        cutEntry: {
+            ...entry,
+            text: text(layer, "mono", "disabled"),
+            active: {
+                background: background(layer, "active"),
+                text: text(layer, "mono", "disabled", { size: "sm" }),
+            },
+        },
+        filenameEditor: {
+            background: background(layer, "on"),
+            text: text(layer, "mono", "on", { size: "sm" }),
+            selection: colorScheme.players[0],
+        },
+    }
 }

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

@@ -1,46 +1,54 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, 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(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
 
-  const avatarSize = 48;
-  return {
-    windowHeight: 74,
-    windowWidth: 380,
-    background: background(layer),
-    ownerContainer: {
-      padding: 12,
-    },
-    ownerAvatar: {
-      height: avatarSize,
-      width: avatarSize,
-      cornerRadius: avatarSize / 2,
-    },
-    ownerMetadata: {
-      margin: { left: 10 },
-    },
-    ownerUsername: {
-      ...text(layer, "sans", { size: "sm", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    message: {
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      margin: { top: -3 },
-    },
-    worktreeRoots: {
-      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    buttonWidth: 96,
-    openButton: {
-      background: background(layer, "accent"),
-      border: border(layer, { left: true, bottom: true, }),
-      ...text(layer, "sans", "accent", { size: "xs", weight: "extra_bold" })
-    },
-    dismissButton: {
-      border: border(layer, { left: true }),
-      ...text(layer, "sans", "variant", { size: "xs", weight: "extra_bold" })
-    },
-  };
+    const avatarSize = 48
+    return {
+        windowHeight: 74,
+        windowWidth: 380,
+        background: background(layer),
+        ownerContainer: {
+            padding: 12,
+        },
+        ownerAvatar: {
+            height: avatarSize,
+            width: avatarSize,
+            cornerRadius: avatarSize / 2,
+        },
+        ownerMetadata: {
+            margin: { left: 10 },
+        },
+        ownerUsername: {
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        message: {
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktreeRoots: {
+            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        buttonWidth: 96,
+        openButton: {
+            background: background(layer, "accent"),
+            border: border(layer, { left: true, bottom: true }),
+            ...text(layer, "sans", "accent", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+        dismissButton: {
+            border: border(layer, { left: true }),
+            ...text(layer, "sans", "variant", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+    }
 }

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

@@ -1,96 +1,94 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, foreground, text } from "./components"
 
 export default function search(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
 
-  // Search input
-  const editor = {
-    background: background(layer),
-    cornerRadius: 8,
-    minWidth: 200,
-    maxWidth: 500,
-    placeholderText: text(layer, "mono", "disabled"),
-    selection: colorScheme.players[0],
-    text: text(layer, "mono", "default"),
-    border: border(layer),
-    margin: {
-      right: 12,
-    },
-    padding: {
-      top: 3,
-      bottom: 3,
-      left: 12,
-      right: 8,
-    },
-  };
+    // Search input
+    const editor = {
+        background: background(layer),
+        cornerRadius: 8,
+        minWidth: 200,
+        maxWidth: 500,
+        placeholderText: text(layer, "mono", "disabled"),
+        selection: colorScheme.players[0],
+        text: text(layer, "mono", "default"),
+        border: border(layer),
+        margin: {
+            right: 12,
+        },
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 12,
+            right: 8,
+        },
+    }
 
-  return {
-    // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
-    matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
-    tabIconSpacing: 8,
-    tabIconWidth: 14,
-    optionButton: {
-      ...text(layer, "mono", "on"),
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      border: border(layer, "on"),
-      margin: {
-        right: 4,
-      },
-      padding: {
-        bottom: 2,
-        left: 10,
-        right: 10,
-        top: 2,
-      },
-      active: {
-        ...text(layer, "mono", "on", "inverted"),
-        background: background(layer, "on", "inverted"),
-        border: border(layer, "on", "inverted"),
-      },
-      clicked: {
-        ...text(layer, "mono", "on", "pressed"),
-        background: background(layer, "on", "pressed"),
-        border: border(layer, "on", "pressed"),
-      },
-      hover: {
-        ...text(layer, "mono", "on", "hovered"),
-        background: background(layer, "on", "hovered"),
-        border: border(layer, "on", "hovered"),
-      },
-    },
-    editor,
-    invalidEditor: {
-      ...editor,
-      border: border(layer, "negative"),
-    },
-    matchIndex: {
-      ...text(layer, "mono", "variant"),
-      padding: 6,
-    },
-    optionButtonGroup: {
-      padding: {
-        left: 12,
-        right: 12,
-      },
-    },
-    resultsStatus: {
-      ...text(layer, "mono", "on"),
-      size: 18,
-    },
-    dismissButton: {
-      color: foreground(layer, "variant"),
-      iconWidth: 12,
-      buttonWidth: 14,
-      padding: {
-        left: 10,
-        right: 10,
-      },
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+    return {
+        // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
+        matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
+        optionButton: {
+            ...text(layer, "mono", "on"),
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            border: border(layer, "on"),
+            margin: {
+                right: 4,
+            },
+            padding: {
+                bottom: 2,
+                left: 10,
+                right: 10,
+                top: 2,
+            },
+            active: {
+                ...text(layer, "mono", "on", "inverted"),
+                background: background(layer, "on", "inverted"),
+                border: border(layer, "on", "inverted"),
+            },
+            clicked: {
+                ...text(layer, "mono", "on", "pressed"),
+                background: background(layer, "on", "pressed"),
+                border: border(layer, "on", "pressed"),
+            },
+            hover: {
+                ...text(layer, "mono", "on", "hovered"),
+                background: background(layer, "on", "hovered"),
+                border: border(layer, "on", "hovered"),
+            },
+        },
+        editor,
+        invalidEditor: {
+            ...editor,
+            border: border(layer, "negative"),
+        },
+        matchIndex: {
+            ...text(layer, "mono", "variant"),
+            padding: 6,
+        },
+        optionButtonGroup: {
+            padding: {
+                left: 12,
+                right: 12,
+            },
+        },
+        resultsStatus: {
+            ...text(layer, "mono", "on"),
+            size: 18,
+        },
+        dismissButton: {
+            color: foreground(layer, "variant"),
+            iconWidth: 12,
+            buttonWidth: 14,
+            padding: {
+                left: 10,
+                right: 10,
+            },
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

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

@@ -1,9 +1,9 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background } from "./components"
 
 export default function sharedScreen(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
-  return {
-    background: background(layer)
-  }
+    let layer = colorScheme.highest
+    return {
+        background: background(layer),
+    }
 }

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

@@ -1,31 +1,33 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { foreground, text } from "./components"
 
-const headerPadding = 8;
+const headerPadding = 8
 
-export default function simpleMessageNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
-  return {
-    message: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, right: headerPadding },
-    },
-    actionMessage: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, top: 6, bottom: 6 },
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-    dismissButton: {
-      color: foreground(layer),
-      iconWidth: 8,
-      iconHeight: 8,
-      buttonWidth: 8,
-      buttonHeight: 8,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+export default function simpleMessageNotification(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
+    return {
+        message: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, right: headerPadding },
+        },
+        actionMessage: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, top: 6, bottom: 6 },
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+        dismissButton: {
+            color: foreground(layer),
+            iconWidth: 8,
+            iconHeight: 8,
+            buttonWidth: 8,
+            buttonHeight: 8,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

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

@@ -1,118 +1,118 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, text } from "./components"
 
 export default function statusBar(colorScheme: ColorScheme) {
-  let layer = colorScheme.lowest;
+    let layer = colorScheme.lowest
 
-  const statusContainer = {
-    cornerRadius: 6,
-    padding: { top: 3, bottom: 3, left: 6, right: 6 },
-  };
-
-  const diagnosticStatusContainer = {
-    cornerRadius: 6,
-    padding: { top: 1, bottom: 1, left: 6, right: 6 },
-  };
+    const statusContainer = {
+        cornerRadius: 6,
+        padding: { top: 3, bottom: 3, left: 6, right: 6 },
+    }
 
-  return {
-    height: 30,
-    itemSpacing: 8,
-    padding: {
-      top: 1,
-      bottom: 1,
-      left: 6,
-      right: 6,
-    },
-    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(layer, "sans"),
-      iconColor: foreground(layer),
-      hover: {
-        message: text(layer, "sans"),
-        iconColor: foreground(layer),
-        background: background(layer),
-      },
-    },
-    diagnosticMessage: {
-      ...text(layer, "sans"),
-      hover: text(layer, "sans", "hovered"),
-    },
-    feedback: {
-      ...text(layer, "sans", "variant"),
-      hover: text(layer, "sans", "hovered"),
-    },
-    diagnosticSummary: {
-      height: 20,
-      iconWidth: 16,
-      iconSpacing: 2,
-      summarySpacing: 6,
-      text: text(layer, "sans", { size: "sm" }),
-      iconColorOk: foreground(layer, "variant"),
-      iconColorWarning: foreground(layer, "warning"),
-      iconColorError: foreground(layer, "negative"),
-      containerOk: {
+    const diagnosticStatusContainer = {
         cornerRadius: 6,
-        padding: { top: 3, bottom: 3, left: 7, right: 7 },
-      },
-      containerWarning: {
-        ...diagnosticStatusContainer,
-        background: background(layer, "warning"),
-        border: border(layer, "warning"),
-      },
-      containerError: {
-        ...diagnosticStatusContainer,
-        background: background(layer, "negative"),
-        border: border(layer, "negative"),
-      },
-      hover: {
-        iconColorOk: foreground(layer, "on"),
-        containerOk: {
-          cornerRadius: 6,
-          padding: { top: 3, bottom: 3, left: 7, right: 7 },
-          background: background(layer, "on", "hovered"),
+        padding: { top: 1, bottom: 1, left: 6, right: 6 },
+    }
+
+    return {
+        height: 30,
+        itemSpacing: 8,
+        padding: {
+            top: 1,
+            bottom: 1,
+            left: 6,
+            right: 6,
+        },
+        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(layer, "sans"),
+            iconColor: foreground(layer),
+            hover: {
+                message: text(layer, "sans"),
+                iconColor: foreground(layer),
+                background: background(layer),
+            },
         },
-        containerWarning: {
-          ...diagnosticStatusContainer,
-          background: background(layer, "warning", "hovered"),
-          border: border(layer, "warning", "hovered"),
+        diagnosticMessage: {
+            ...text(layer, "sans"),
+            hover: text(layer, "sans", "hovered"),
         },
-        containerError: {
-          ...diagnosticStatusContainer,
-          background: background(layer, "negative", "hovered"),
-          border: border(layer, "negative", "hovered"),
+        feedback: {
+            ...text(layer, "sans", "variant"),
+            hover: text(layer, "sans", "hovered"),
         },
-      },
-    },
-    sidebarButtons: {
-      groupLeft: {},
-      groupRight: {},
-      item: {
-        ...statusContainer,
-        iconSize: 16,
-        iconColor: foreground(layer, "variant"),
-        hover: {
-          iconColor: foreground(layer, "hovered"),
-          background: background(layer, "variant"),
+        diagnosticSummary: {
+            height: 20,
+            iconWidth: 16,
+            iconSpacing: 2,
+            summarySpacing: 6,
+            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: background(layer, "warning"),
+                border: border(layer, "warning"),
+            },
+            containerError: {
+                ...diagnosticStatusContainer,
+                background: background(layer, "negative"),
+                border: border(layer, "negative"),
+            },
+            hover: {
+                iconColorOk: foreground(layer, "on"),
+                containerOk: {
+                    cornerRadius: 6,
+                    padding: { top: 3, bottom: 3, left: 7, right: 7 },
+                    background: background(layer, "on", "hovered"),
+                },
+                containerWarning: {
+                    ...diagnosticStatusContainer,
+                    background: background(layer, "warning", "hovered"),
+                    border: border(layer, "warning", "hovered"),
+                },
+                containerError: {
+                    ...diagnosticStatusContainer,
+                    background: background(layer, "negative", "hovered"),
+                    border: border(layer, "negative", "hovered"),
+                },
+            },
         },
-        active: {
-          iconColor: foreground(layer, "active"),
-          background: background(layer, "active"),
+        sidebarButtons: {
+            groupLeft: {},
+            groupRight: {},
+            item: {
+                ...statusContainer,
+                iconSize: 16,
+                iconColor: foreground(layer, "variant"),
+                hover: {
+                    iconColor: foreground(layer, "hovered"),
+                    background: background(layer, "variant"),
+                },
+                active: {
+                    iconColor: foreground(layer, "active"),
+                    background: background(layer, "active"),
+                },
+            },
+            badge: {
+                cornerRadius: 3,
+                padding: 2,
+                margin: { bottom: -1, right: -1 },
+                border: border(layer),
+                background: background(layer, "accent"),
+            },
         },
-      },
-      badge: {
-        cornerRadius: 3,
-        padding: 2,
-        margin: { bottom: -1, right: -1 },
-        border: border(layer),
-        background: background(layer, "accent"),
-      },
-    },
-  };
+    }
 }

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

@@ -1,103 +1,103 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { text, border, background, foreground } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { text, border, background, foreground } from "./components"
 
 export default function tabBar(colorScheme: ColorScheme) {
-  const height = 32;
+    const height = 32
 
-  let activeLayer = colorScheme.highest;
-  let layer = colorScheme.middle;
+    let activeLayer = colorScheme.highest
+    let layer = colorScheme.middle
 
-  const tab = {
-    height,
-    text: text(layer, "sans", "variant", { size: "sm" }),
-    background: background(layer),
-    border: border(layer, {
-      right: true,
-      bottom: true,
-      overlay: true,
-    }),
-    padding: {
-      left: 8,
-      right: 12,
-    },
-    spacing: 8,
+    const tab = {
+        height,
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        background: background(layer),
+        border: border(layer, {
+            right: true,
+            bottom: true,
+            overlay: true,
+        }),
+        padding: {
+            left: 8,
+            right: 12,
+        },
+        spacing: 8,
 
-    // Close icons
-    iconWidth: 8,
-    iconClose: foreground(layer, "variant"),
-    iconCloseActive: foreground(layer, "hovered"),
+        // Close icons
+        iconWidth: 14,
+        iconClose: foreground(layer, "variant"),
+        iconCloseActive: foreground(layer, "hovered"),
 
-    // Indicators
-    iconConflict: foreground(layer, "warning"),
-    iconDirty: foreground(layer, "accent"),
+        // 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: 8 },
-      ...text(layer, "sans", "disabled", { size: "2xs" }),
-    },
-  };
+        // When two tabs of the same name are open, a label appears next to them
+        description: {
+            margin: { left: 8 },
+            ...text(layer, "sans", "disabled", { size: "2xs" }),
+        },
+    }
 
-  const activePaneActiveTab = {
-    ...tab,
-    background: background(activeLayer),
-    text: text(activeLayer, "sans", "active", { size: "sm" }),
-    border: {
-      ...tab.border,
-      bottom: false,
-    },
-  };
+    const activePaneActiveTab = {
+        ...tab,
+        background: background(activeLayer),
+        text: text(activeLayer, "sans", "active", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
 
-  const inactivePaneInactiveTab = {
-    ...tab,
-    background: background(layer),
-    text: text(layer, "sans", "variant", { size: "sm" }),
-  };
+    const inactivePaneInactiveTab = {
+        ...tab,
+        background: background(layer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+    }
 
-  const inactivePaneActiveTab = {
-    ...tab,
-    background: background(activeLayer),
-    text: text(layer, "sans", "variant", { size: "sm" }),
-    border: {
-      ...tab.border,
-      bottom: false,
-    },
-  };
+    const inactivePaneActiveTab = {
+        ...tab,
+        background: background(activeLayer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
 
-  const draggedTab = {
-    ...activePaneActiveTab,
-    background: withOpacity(tab.background, 0.9),
-    border: undefined as any,
-    shadow: colorScheme.popoverShadow,
-  };
+    const draggedTab = {
+        ...activePaneActiveTab,
+        background: withOpacity(tab.background, 0.9),
+        border: undefined as any,
+        shadow: colorScheme.popoverShadow,
+    }
 
-  return {
-    height,
-    background: background(layer),
-    activePane: {
-      activeTab: activePaneActiveTab,
-      inactiveTab: tab,
-    },
-    inactivePane: {
-      activeTab: inactivePaneActiveTab,
-      inactiveTab: inactivePaneInactiveTab,
-    },
-    draggedTab,
-    paneButton: {
-      color: foreground(layer, "variant"),
-      iconWidth: 12,
-      buttonWidth: activePaneActiveTab.height,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-    paneButtonContainer: {
-      background: tab.background,
-      border: {
-        ...tab.border,
-        right: false,
-      },
-    },
-  };
+    return {
+        height,
+        background: background(layer),
+        activePane: {
+            activeTab: activePaneActiveTab,
+            inactiveTab: tab,
+        },
+        inactivePane: {
+            activeTab: inactivePaneActiveTab,
+            inactiveTab: inactivePaneInactiveTab,
+        },
+        draggedTab,
+        paneButton: {
+            color: foreground(layer, "variant"),
+            iconWidth: 12,
+            buttonWidth: activePaneActiveTab.height,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+        paneButtonContainer: {
+            background: tab.background,
+            border: {
+                ...tab.border,
+                right: false,
+            },
+        },
+    }
 }

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

@@ -1,52 +1,52 @@
-import { ColorScheme } from "../themes/common/colorScheme";
+import { ColorScheme } from "../themes/common/colorScheme"
 
 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
-   */
-  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
+     * 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
      */
-    foreground: colorScheme.ramps.neutral(1).hex(),
-    /**
-     * Default color for the rectangle background of a cell
-     */
-    background: colorScheme.ramps.neutral(0).hex(),
-    modalBackground: colorScheme.ramps.neutral(0.1).hex(),
-    /**
-     * Default color for the cursor
-     */
-    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(),
-  };
+    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: colorScheme.ramps.neutral(1).hex(),
+        /**
+         * Default color for the rectangle background of a cell
+         */
+        background: colorScheme.ramps.neutral(0).hex(),
+        modalBackground: colorScheme.ramps.neutral(0.1).hex(),
+        /**
+         * Default color for the cursor
+         */
+        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,23 +1,23 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function tooltip(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  return {
-    background: background(layer),
-    border: border(layer),
-    padding: { top: 4, bottom: 4, left: 8, right: 8 },
-    margin: { top: 6, left: 6 },
-    shadow: colorScheme.popoverShadow,
-    cornerRadius: 6,
-    text: text(layer, "sans", { size: "xs" }),
-    keystroke: {
-      background: background(layer, "on"),
-      cornerRadius: 4,
-      margin: { left: 6 },
-      padding: { left: 4, right: 4 },
-      ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
-    },
-    maxTextWidth: 200,
-  };
+    let layer = colorScheme.middle
+    return {
+        background: background(layer),
+        border: border(layer),
+        padding: { top: 4, bottom: 4, left: 8, right: 8 },
+        margin: { top: 6, left: 6 },
+        shadow: colorScheme.popoverShadow,
+        cornerRadius: 6,
+        text: text(layer, "sans", { size: "xs" }),
+        keystroke: {
+            background: background(layer, "on"),
+            cornerRadius: 4,
+            margin: { left: 6 },
+            padding: { left: 4, right: 4 },
+            ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
+        },
+        maxTextWidth: 200,
+    }
 }

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

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

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

@@ -1,235 +1,264 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
-import statusBar from "./statusBar";
-import tabBar from "./tabBar";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, borderColor, foreground, text } from "./components"
+import statusBar from "./statusBar"
+import tabBar from "./tabBar"
 
 export default function workspace(colorScheme: ColorScheme) {
-  const layer = colorScheme.lowest;
-  const titlebarPadding = 6;
-  const titlebarButton = {
-    cornerRadius: 6,
-    padding: {
-      top: 1,
-      bottom: 1,
-      left: 8,
-      right: 8,
-    },
-    ...text(layer, "sans", "variant", { size: "xs" }),
-    background: background(layer, "variant"),
-    border: border(layer),
-    hover: {
-      ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
-      background: background(layer, "variant", "hovered"),
-      border: border(layer, "variant", "hovered"),
-    },
-  };
-  const avatarWidth = 18;
+    const layer = colorScheme.lowest
+    const itemSpacing = 8
+    const titlebarButton = {
+        cornerRadius: 6,
+        padding: {
+            top: 1,
+            bottom: 1,
+            left: 8,
+            right: 8,
+        },
+        ...text(layer, "sans", "variant", { size: "xs" }),
+        background: background(layer, "variant"),
+        border: border(layer),
+        hover: {
+            ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
+            background: background(layer, "variant", "hovered"),
+            border: border(layer, "variant", "hovered"),
+        },
+        clicked: {
+            ...text(layer, "sans", "variant", "pressed", { size: "xs" }),
+            background: background(layer, "variant", "pressed"),
+            border: border(layer, "variant", "pressed"),
+        },
+        active: {
+            ...text(layer, "sans", "variant", "active", { size: "xs" }),
+            background: background(layer, "variant", "active"),
+            border: border(layer, "variant", "active"),
+        },
+    }
+    const avatarWidth = 18
+    const avatarOuterWidth = avatarWidth + 4
+    const followerAvatarWidth = 14
+    const followerAvatarOuterWidth = followerAvatarWidth + 4
 
-  return {
-    background: background(layer),
-    joiningProjectAvatar: {
-      cornerRadius: 40,
-      width: 80,
-    },
-    joiningProjectMessage: {
-      padding: 12,
-      ...text(layer, "sans", { size: "lg" }),
-    },
-    externalLocationMessage: {
-      background: background(colorScheme.middle, "accent"),
-      border: border(colorScheme.middle, "accent"),
-      cornerRadius: 6,
-      padding: 12,
-      margin: { bottom: 8, right: 8 },
-      ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
-    },
-    leaderBorderOpacity: 0.7,
-    leaderBorderWidth: 2.0,
-    tabBar: tabBar(colorScheme),
-    modal: {
-      margin: {
-        bottom: 52,
-        top: 52,
-      },
-      cursor: "Arrow",
-    },
-    sidebar: {
-      initialSize: 240,
-      border: border(layer, { left: true, right: true }),
-    },
-    paneDivider: {
-      color: borderColor(layer),
-      width: 1,
-    },
-    statusBar: statusBar(colorScheme),
-    titlebar: {
-      avatarWidth,
-      avatarMargin: 8,
-      height: 33, // 32px + 1px for overlaid border
-      background: background(layer),
-      border: border(layer, { bottom: true, overlay: true }),
-      padding: {
-        left: 80,
-        right: titlebarPadding,
-      },
+    return {
+        background: background(layer),
+        joiningProjectAvatar: {
+            cornerRadius: 40,
+            width: 80,
+        },
+        joiningProjectMessage: {
+            padding: 12,
+            ...text(layer, "sans", { size: "lg" }),
+        },
+        externalLocationMessage: {
+            background: background(colorScheme.middle, "accent"),
+            border: border(colorScheme.middle, "accent"),
+            cornerRadius: 6,
+            padding: 12,
+            margin: { bottom: 8, right: 8 },
+            ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
+        },
+        leaderBorderOpacity: 0.7,
+        leaderBorderWidth: 2.0,
+        tabBar: tabBar(colorScheme),
+        modal: {
+            margin: {
+                bottom: 52,
+                top: 52,
+            },
+            cursor: "Arrow",
+        },
+        sidebar: {
+            initialSize: 240,
+            border: border(layer, { left: true, right: true }),
+        },
+        paneDivider: {
+            color: borderColor(layer),
+            width: 1,
+        },
+        statusBar: statusBar(colorScheme),
+        titlebar: {
+            itemSpacing,
+            facePileSpacing: 2,
+            height: 33, // 32px + 1px for overlaid border
+            background: background(layer),
+            border: border(layer, { bottom: true, overlay: true }),
+            padding: {
+                left: 80,
+                right: itemSpacing,
+            },
 
-      // Project
-      title: text(layer, "sans", "variant"),
+            // Project
+            title: text(layer, "sans", "variant"),
 
-      // Collaborators
-      avatar: {
-        cornerRadius: avatarWidth / 2,
-        border: {
-          color: "#00000088",
-          width: 1,
-        },
-      },
-      inactiveAvatar: {
-        cornerRadius: avatarWidth / 2,
-        border: {
-          color: "#00000088",
-          width: 1,
-        },
-        grayscale: true,
-      },
-      avatarRibbon: {
-        height: 3,
-        width: 12,
-        // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
-      },
+            // Collaborators
+            leaderAvatar: {
+                width: avatarWidth,
+                outerWidth: avatarOuterWidth,
+                cornerRadius: avatarWidth / 2,
+                outerCornerRadius: avatarOuterWidth / 2,
+            },
+            followerAvatar: {
+                width: followerAvatarWidth,
+                outerWidth: followerAvatarOuterWidth,
+                cornerRadius: followerAvatarWidth / 2,
+                outerCornerRadius: followerAvatarOuterWidth / 2,
+            },
+            inactiveAvatarGrayscale: true,
+            followerAvatarOverlap: 8,
+            leaderSelection: {
+                margin: {
+                    top: 4,
+                    bottom: 4,
+                },
+                padding: {
+                    left: 2,
+                    right: 2,
+                    top: 4,
+                    bottom: 4,
+                },
+                cornerRadius: 6,
+            },
+            avatarRibbon: {
+                height: 3,
+                width: 12,
+                // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
+            },
 
-      // Sign in buttom
-      // FlatButton, Variant
-      signInPrompt: {
-        ...titlebarButton
-      },
+            // Sign in buttom
+            // FlatButton, Variant
+            signInPrompt: {
+                ...titlebarButton,
+            },
 
-      // Offline Indicator
-      offlineIcon: {
-        color: foreground(layer, "variant"),
-        width: 16,
-        margin: {
-          left: titlebarPadding,
-        },
-        padding: {
-          right: 4,
+            // Offline Indicator
+            offlineIcon: {
+                color: foreground(layer, "variant"),
+                width: 16,
+                margin: {
+                    left: itemSpacing,
+                },
+                padding: {
+                    right: 4,
+                },
+            },
+
+            // Notice that the collaboration server is out of date
+            outdatedWarning: {
+                ...text(layer, "sans", "warning", { size: "xs" }),
+                background: withOpacity(background(layer, "warning"), 0.3),
+                border: border(layer, "warning"),
+                margin: {
+                    left: itemSpacing,
+                },
+                padding: {
+                    left: 8,
+                    right: 8,
+                },
+                cornerRadius: 6,
+            },
+            callControl: {
+                cornerRadius: 6,
+                color: foreground(layer, "variant"),
+                iconWidth: 12,
+                buttonWidth: 20,
+                hover: {
+                    background: background(layer, "variant", "hovered"),
+                    color: foreground(layer, "variant", "hovered"),
+                },
+            },
+            toggleContactsButton: {
+                margin: { left: itemSpacing },
+                cornerRadius: 6,
+                color: foreground(layer, "variant"),
+                iconWidth: 8,
+                buttonWidth: 20,
+                active: {
+                    background: background(layer, "variant", "active"),
+                    color: foreground(layer, "variant", "active"),
+                },
+                clicked: {
+                    background: background(layer, "variant", "pressed"),
+                    color: foreground(layer, "variant", "pressed"),
+                },
+                hover: {
+                    background: background(layer, "variant", "hovered"),
+                    color: foreground(layer, "variant", "hovered"),
+                },
+            },
+            userMenuButton: {
+                buttonWidth: 20,
+                iconWidth: 12,
+                ...titlebarButton,
+            },
+            toggleContactsBadge: {
+                cornerRadius: 3,
+                padding: 2,
+                margin: { top: 3, left: 3 },
+                border: border(layer),
+                background: foreground(layer, "accent"),
+            },
+            shareButton: {
+                ...titlebarButton,
+            },
         },
-      },
 
-      // Notice that the collaboration server is out of date
-      outdatedWarning: {
-        ...text(layer, "sans", "warning", { size: "xs" }),
-        background: withOpacity(background(layer, "warning"), 0.3),
-        border: border(layer, "warning"),
-        margin: {
-          left: titlebarPadding,
+        toolbar: {
+            height: 34,
+            background: background(colorScheme.highest),
+            border: border(colorScheme.highest, { bottom: true }),
+            itemSpacing: 8,
+            navButton: {
+                color: foreground(colorScheme.highest, "on"),
+                iconWidth: 12,
+                buttonWidth: 24,
+                cornerRadius: 6,
+                hover: {
+                    color: foreground(colorScheme.highest, "on", "hovered"),
+                    background: background(
+                        colorScheme.highest,
+                        "on",
+                        "hovered"
+                    ),
+                },
+                disabled: {
+                    color: foreground(colorScheme.highest, "on", "disabled"),
+                },
+            },
+            padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
-        padding: {
-          left: 8,
-          right: 8,
+        breadcrumbs: {
+            ...text(layer, "mono", "variant"),
+            padding: { left: 6 },
         },
-        cornerRadius: 6,
-      },
-      callControl: {
-        cornerRadius: 6,
-        color: foreground(layer, "variant"),
-        iconWidth: 12,
-        buttonWidth: 20,
-        hover: {
-          background: background(layer, "variant", "hovered"),
-          color: foreground(layer, "variant", "hovered"),
+        disconnectedOverlay: {
+            ...text(layer, "sans"),
+            background: withOpacity(background(layer), 0.8),
         },
-      },
-      toggleContactsButton: {
-        margin: { left: 6 },
-        cornerRadius: 6,
-        color: foreground(layer, "variant"),
-        iconWidth: 8,
-        buttonWidth: 20,
-        active: {
-          background: background(layer, "variant", "active"),
-          color: foreground(layer, "variant", "active"),
+        notification: {
+            margin: { top: 10 },
+            background: background(colorScheme.middle),
+            cornerRadius: 6,
+            padding: 12,
+            border: border(colorScheme.middle),
+            shadow: colorScheme.popoverShadow,
         },
-        hover: {
-          background: background(layer, "variant", "hovered"),
-          color: foreground(layer, "variant", "hovered"),
-        },
-      },
-      toggleContactsBadge: {
-        cornerRadius: 3,
-        padding: 2,
-        margin: { top: 3, left: 3 },
-        border: border(layer),
-        background: foreground(layer, "accent"),
-      },
-      shareButton: {
-        ...titlebarButton
-      }
-    },
-
-    toolbar: {
-      height: 34,
-      background: background(colorScheme.highest),
-      border: border(colorScheme.highest, { bottom: true }),
-      itemSpacing: 8,
-      navButton: {
-        color: foreground(colorScheme.highest, "on"),
-        iconWidth: 12,
-        buttonWidth: 24,
-        cornerRadius: 6,
-        hover: {
-          color: foreground(colorScheme.highest, "on", "hovered"),
-          background: background(colorScheme.highest, "on", "hovered"),
-        },
-        disabled: {
-          color: foreground(colorScheme.highest, "on", "disabled"),
-        },
-      },
-      padding: { left: 8, right: 8, top: 4, bottom: 4 },
-    },
-    breadcrumbs: {
-      ...text(layer, "mono", "variant"),
-      padding: { left: 6 },
-    },
-    disconnectedOverlay: {
-      ...text(layer, "sans"),
-      background: withOpacity(background(layer), 0.8),
-    },
-    notification: {
-      margin: { top: 10 },
-      background: background(colorScheme.middle),
-      cornerRadius: 6,
-      padding: 12,
-      border: border(colorScheme.middle),
-      shadow: colorScheme.popoverShadow,
-    },
-    notifications: {
-      width: 400,
-      margin: { right: 10, bottom: 10 },
-    },
-    dock: {
-      initialSizeRight: 640,
-      initialSizeBottom: 480,
-      wash_color: withOpacity(background(colorScheme.highest), 0.5),
-      panel: {
-        border: border(colorScheme.middle),
-      },
-      maximized: {
-        margin: 32,
-        border: border(colorScheme.highest, { overlay: true }),
-        shadow: colorScheme.modalShadow,
-      },
-    },
-    dropTargetOverlayColor: withOpacity(
-      foreground(layer, "variant"),
-      0.5
-    ),
-  };
+        notifications: {
+            width: 400,
+            margin: { right: 10, bottom: 10 },
+        },
+        dock: {
+            initialSizeRight: 640,
+            initialSizeBottom: 480,
+            wash_color: withOpacity(background(colorScheme.highest), 0.5),
+            panel: {
+                border: border(colorScheme.middle),
+            },
+            maximized: {
+                margin: 32,
+                border: border(colorScheme.highest, { overlay: true }),
+                shadow: colorScheme.modalShadow,
+            },
+        },
+        dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
+    }
 }

styles/src/system/lib/convert.ts πŸ”—

@@ -0,0 +1,11 @@
+/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */
+export function percentageToNormalized(value: number) {
+    const normalized = value / 100
+    return normalized
+}
+
+/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */
+export function normalizedToPercetage(value: number) {
+    const percentage = value * 100
+    return percentage
+}

styles/src/system/lib/curve.ts πŸ”—

@@ -0,0 +1,26 @@
+import bezier from "bezier-easing"
+import { Curve } from "../ref/curves"
+
+/**
+ * Formats our Curve data structure into a bezier easing function.
+ * @param {Curve} curve - The curve to format.
+ * @param {Boolean} inverted - Whether or not to invert the curve.
+ * @returns {EasingFunction} The formatted easing function.
+ */
+export function curve(curve: Curve, inverted?: Boolean) {
+    if (inverted) {
+        return bezier(
+            curve.value[3],
+            curve.value[2],
+            curve.value[1],
+            curve.value[0]
+        )
+    }
+
+    return bezier(
+        curve.value[0],
+        curve.value[1],
+        curve.value[2],
+        curve.value[3]
+    )
+}

styles/src/system/lib/generate.ts πŸ”—

@@ -0,0 +1,159 @@
+import bezier from "bezier-easing"
+import chroma from "chroma-js"
+import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types"
+import { percentageToNormalized } from "./convert"
+import { curve } from "./curve"
+
+// Re-export interface in a more standard format
+export type EasingFunction = bezier.EasingFunction
+
+/**
+ * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
+ *
+ * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
+ * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
+ * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
+ * @param {ColorFamilyConfig} family - Configuration for the color family.
+ * @param {number} step - The current step.
+ * @param {number} steps - The total number of steps in the color scale.
+ *
+ * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
+ */
+function generateColor(
+    hueEasing: EasingFunction,
+    saturationEasing: EasingFunction,
+    lightnessEasing: EasingFunction,
+    family: ColorFamilyConfig,
+    step: number,
+    steps: number
+) {
+    const { hue, saturation, lightness } = family.color
+
+    const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start
+    const stepSaturation =
+        saturationEasing(step / steps) * (saturation.end - saturation.start) +
+        saturation.start
+    const stepLightness =
+        lightnessEasing(step / steps) * (lightness.end - lightness.start) +
+        lightness.start
+
+    const color = chroma.hsl(
+        stepHue,
+        percentageToNormalized(stepSaturation),
+        percentageToNormalized(stepLightness)
+    )
+
+    const contrast = {
+        black: {
+            value: chroma.contrast(color, "black"),
+            aaPass: chroma.contrast(color, "black") >= 4.5,
+            aaaPass: chroma.contrast(color, "black") >= 7,
+        },
+        white: {
+            value: chroma.contrast(color, "white"),
+            aaPass: chroma.contrast(color, "white") >= 4.5,
+            aaaPass: chroma.contrast(color, "white") >= 7,
+        },
+    }
+
+    const lch = color.lch()
+    const rgba = color.rgba()
+    const hex = color.hex()
+
+    // 55 is a magic number. It's the lightness value at which we consider a color to be "light".
+    // It was picked by eye with some testing. We might want to use a more scientific approach in the future.
+    const isLight = lch[0] > 55
+
+    const result: Color = {
+        step,
+        lch,
+        hex,
+        rgba,
+        contrast,
+        isLight,
+    }
+
+    return result
+}
+
+/**
+ * Generates a color scale based on a color family configuration.
+ *
+ * @param {ColorFamilyConfig} config - The configuration for the color family.
+ * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
+ *
+ * @returns {ColorScale} The generated color scale.
+ *
+ * @example
+ * ```ts
+ * const colorScale = generateColorScale({
+ *   name: "blue",
+ *   color: {
+ *     hue: {
+ *       start: 210,
+ *       end: 240,
+ *       curve: "easeInOut"
+ *     },
+ *     saturation: {
+ *       start: 100,
+ *       end: 100,
+ *       curve: "easeInOut"
+ *     },
+ *     lightness: {
+ *       start: 50,
+ *       end: 50,
+ *       curve: "easeInOut"
+ *     }
+ *   }
+ * });
+ * ```
+ */
+
+export function generateColorScale(
+    config: ColorFamilyConfig,
+    inverted: Boolean = false
+) {
+    const { hue, saturation, lightness } = config.color
+
+    // 101 steps means we get values from 0-100
+    const NUM_STEPS = 101
+
+    const hueEasing = curve(hue.curve, inverted)
+    const saturationEasing = curve(saturation.curve, inverted)
+    const lightnessEasing = curve(lightness.curve, inverted)
+
+    let scale: ColorScale = {
+        colors: [],
+        values: [],
+    }
+
+    for (let i = 0; i < NUM_STEPS; i++) {
+        const color = generateColor(
+            hueEasing,
+            saturationEasing,
+            lightnessEasing,
+            config,
+            i,
+            NUM_STEPS
+        )
+
+        scale.colors.push(color)
+        scale.values.push(color.hex)
+    }
+
+    return scale
+}
+
+/** Generates a color family with a scale and an inverted scale. */
+export function generateColorFamily(config: ColorFamilyConfig) {
+    const scale = generateColorScale(config, false)
+    const invertedScale = generateColorScale(config, true)
+
+    const family: ColorFamily = {
+        name: config.name,
+        scale,
+        invertedScale,
+    }
+
+    return family
+}

styles/src/system/ref/color.ts πŸ”—

@@ -0,0 +1,445 @@
+import { generateColorFamily } from "../lib/generate"
+import { curve } from "./curves"
+
+// These are the source colors for the color scales in the system.
+// These should never directly be used directly in components or themes as they generate thousands of lines of code.
+// Instead, use the outputs from the reference palette which exports a smaller subset of colors.
+
+// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale.
+
+// Light Gray ======================================== //
+
+export const lightgray = generateColorFamily({
+    name: "lightgray",
+    color: {
+        hue: {
+            start: 210,
+            end: 210,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 10,
+            end: 15,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 50,
+            curve: curve.linear,
+        },
+    },
+})
+
+// Light Dark ======================================== //
+
+export const darkgray = generateColorFamily({
+    name: "darkgray",
+    color: {
+        hue: {
+            start: 210,
+            end: 210,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 15,
+            end: 20,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 55,
+            end: 8,
+            curve: curve.linear,
+        },
+    },
+})
+
+// Red ======================================== //
+
+export const red = generateColorFamily({
+    name: "red",
+    color: {
+        hue: {
+            start: 0,
+            end: 0,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 95,
+            end: 75,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 25,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Sunset ======================================== //
+
+export const sunset = generateColorFamily({
+    name: "sunset",
+    color: {
+        hue: {
+            start: 15,
+            end: 15,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 25,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Orange ======================================== //
+
+export const orange = generateColorFamily({
+    name: "orange",
+    color: {
+        hue: {
+            start: 25,
+            end: 25,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 95,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Amber ======================================== //
+
+export const amber = generateColorFamily({
+    name: "amber",
+    color: {
+        hue: {
+            start: 38,
+            end: 38,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 100,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Yellow ======================================== //
+
+export const yellow = generateColorFamily({
+    name: "yellow",
+    color: {
+        hue: {
+            start: 48,
+            end: 48,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 90,
+            end: 100,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Lemon ======================================== //
+
+export const lemon = generateColorFamily({
+    name: "lemon",
+    color: {
+        hue: {
+            start: 55,
+            end: 55,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 95,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Citron ======================================== //
+
+export const citron = generateColorFamily({
+    name: "citron",
+    color: {
+        hue: {
+            start: 70,
+            end: 70,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Lime ======================================== //
+
+export const lime = generateColorFamily({
+    name: "lime",
+    color: {
+        hue: {
+            start: 85,
+            end: 85,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 80,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Green ======================================== //
+
+export const green = generateColorFamily({
+    name: "green",
+    color: {
+        hue: {
+            start: 108,
+            end: 108,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Mint ======================================== //
+
+export const mint = generateColorFamily({
+    name: "mint",
+    color: {
+        hue: {
+            start: 142,
+            end: 142,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 75,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Cyan ======================================== //
+
+export const cyan = generateColorFamily({
+    name: "cyan",
+    color: {
+        hue: {
+            start: 179,
+            end: 179,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 70,
+            end: 80,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Sky ======================================== //
+
+export const sky = generateColorFamily({
+    name: "sky",
+    color: {
+        hue: {
+            start: 195,
+            end: 205,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Blue ======================================== //
+
+export const blue = generateColorFamily({
+    name: "blue",
+    color: {
+        hue: {
+            start: 218,
+            end: 218,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Indigo ======================================== //
+
+export const indigo = generateColorFamily({
+    name: "indigo",
+    color: {
+        hue: {
+            start: 245,
+            end: 245,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 50,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 22,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Purple ======================================== //
+
+export const purple = generateColorFamily({
+    name: "purple",
+    color: {
+        hue: {
+            start: 260,
+            end: 270,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 65,
+            end: 55,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Pink ======================================== //
+
+export const pink = generateColorFamily({
+    name: "pink",
+    color: {
+        hue: {
+            start: 320,
+            end: 330,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 70,
+            end: 65,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 32,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Rose ======================================== //
+
+export const rose = generateColorFamily({
+    name: "rose",
+    color: {
+        hue: {
+            start: 345,
+            end: 345,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 90,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 32,
+            curve: curve.lightness,
+        },
+    },
+})

styles/src/system/ref/curves.ts πŸ”—

@@ -0,0 +1,25 @@
+export interface Curve {
+    name: string
+    value: number[]
+}
+
+export interface Curves {
+    lightness: Curve
+    saturation: Curve
+    linear: Curve
+}
+
+export const curve: Curves = {
+    lightness: {
+        name: "lightnessCurve",
+        value: [0.2, 0, 0.75, 1.0],
+    },
+    saturation: {
+        name: "saturationCurve",
+        value: [0.67, 0.6, 0.55, 1.0],
+    },
+    linear: {
+        name: "linear",
+        value: [0.5, 0.5, 0.5, 0.5],
+    },
+}

styles/src/system/system.ts πŸ”—

@@ -0,0 +1,32 @@
+import chroma from "chroma-js"
+import * as colorFamily from "./ref/color"
+
+const color = {
+    lightgray: chroma
+        .scale(colorFamily.lightgray.scale.values)
+        .mode("lch")
+        .colors(9),
+    darkgray: chroma
+        .scale(colorFamily.darkgray.scale.values)
+        .mode("lch")
+        .colors(9),
+    red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9),
+    sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9),
+    orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9),
+    amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9),
+    yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9),
+    lemon: chroma.scale(colorFamily.lemon.scale.values).mode("lch").colors(9),
+    citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9),
+    lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9),
+    green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9),
+    mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9),
+    cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9),
+    sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9),
+    blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9),
+    indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9),
+    purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9),
+    pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9),
+    rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9),
+}
+
+export { color }

styles/src/system/types.ts πŸ”—

@@ -0,0 +1,66 @@
+import { Curve } from "./ref/curves"
+
+export interface ColorAccessiblityValue {
+    value: number
+    aaPass: boolean
+    aaaPass: boolean
+}
+
+/**
+ * Calculates the color contrast between a specified color and its corresponding background and foreground colors.
+ *
+ * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette.
+ * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information.
+ */
+export interface ColorAccessiblity {
+    black: ColorAccessiblityValue
+    white: ColorAccessiblityValue
+}
+
+export type Color = {
+    step: number
+    contrast: ColorAccessiblity
+    hex: string
+    lch: number[]
+    rgba: number[]
+    isLight: boolean
+}
+
+export interface ColorScale {
+    colors: Color[]
+    // An array of hex values for each color in the scale
+    values: string[]
+}
+
+export type ColorFamily = {
+    name: string
+    scale: ColorScale
+    invertedScale: ColorScale
+}
+
+export interface ColorFamilyHue {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilySaturation {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilyLightness {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilyConfig {
+    name: string
+    color: {
+        hue: ColorFamilyHue
+        saturation: ColorFamilySaturation
+        lightness: ColorFamilyLightness
+    }
+}

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

@@ -1,41 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Andromeda";
+const name = "Andromeda"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#96E072")),
-  cyan: colorRamp(chroma("#00E8C6")),
-  blue: colorRamp(chroma("#0CA793")),
-  violet: colorRamp(chroma("#8A3FA6")),
-  magenta: colorRamp(chroma("#C74DED")),
-};
+    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")),
+    green: colorRamp(chroma("#96E072")),
+    cyan: colorRamp(chroma("#00E8C6")),
+    blue: colorRamp(chroma("#0CA793")),
+    violet: colorRamp(chroma("#8A3FA6")),
+    magenta: colorRamp(chroma("#C74DED")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "EliverLara",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
-    license_checksum: "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89"
-  },
-  url: "https://github.com/EliverLara/Andromeda"
-}
+    name,
+    author: "EliverLara",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
+        license_checksum:
+            "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89",
+    },
+    url: "https://github.com/EliverLara/Andromeda",
+}

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

@@ -1,63 +1,63 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Atelier Cave";
+const name = "Atelier Cave"
 
 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")),
-  green: colorRamp(chroma("#2a9292")),
-  cyan: colorRamp(chroma("#398bc6")),
-  blue: colorRamp(chroma("#576ddb")),
-  violet: colorRamp(chroma("#955ae7")),
-  magenta: colorRamp(chroma("#bf40bf")),
-});
+    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")),
+    green: colorRamp(chroma("#2a9292")),
+    cyan: colorRamp(chroma("#398bc6")),
+    blue: colorRamp(chroma("#576ddb")),
+    violet: colorRamp(chroma("#955ae7")),
+    magenta: colorRamp(chroma("#bf40bf")),
+})
 
 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")),
-});
-
+    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")),
+})
 
 export const meta: Meta = {
-  name,
-  author: "atelierbram",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://atelierbram.mit-license.org/license.txt",
-    license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5"
-  },
-  url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/"
-}
+    name,
+    author: "atelierbram",
+    license: {
+        SPDX: "MIT",
+        https_url: "https://atelierbram.mit-license.org/license.txt",
+        license_checksum:
+            "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5",
+    },
+    url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
+}

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

@@ -1,42 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Atelier Sulphurpool";
+const name = "Atelier Sulphurpool"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#ac9739")),
-  cyan: colorRamp(chroma("#22a2c9")),
-  blue: colorRamp(chroma("#3d8fd1")),
-  violet: colorRamp(chroma("#6679cc")),
-  magenta: colorRamp(chroma("#9c637a")),
-};
+    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")),
+    green: colorRamp(chroma("#ac9739")),
+    cyan: colorRamp(chroma("#22a2c9")),
+    blue: colorRamp(chroma("#3d8fd1")),
+    violet: colorRamp(chroma("#6679cc")),
+    magenta: colorRamp(chroma("#9c637a")),
+}
 
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "atelierbram",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://atelierbram.mit-license.org/license.txt",
-    license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5"
-  },
-  url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/"
-}
+    name,
+    author: "atelierbram",
+    license: {
+        SPDX: "MIT",
+        https_url: "https://atelierbram.mit-license.org/license.txt",
+        license_checksum:
+            "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5",
+    },
+    url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/",
+}

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

@@ -1,296 +0,0 @@
-// NOTE – This should be removed
-// I (Nate) need to come back and check if we are still using this anywhere
-
-import chroma, { Color, Scale } from "chroma-js";
-import { fontWeights } from "../../common";
-import { withOpacity } from "../../utils/color";
-import Theme, { buildPlayer, Syntax } from "./theme";
-
-export function colorRamp(color: Color): Scale {
-  let hue = color.hsl()[0];
-  let endColor = chroma.hsl(hue, 0.88, 0.96);
-  let startColor = chroma.hsl(hue, 0.68, 0.12);
-  return chroma.scale([startColor, color, endColor]).mode("hsl");
-}
-
-export function createTheme(
-  name: string,
-  isLight: boolean,
-  color_ramps: { [rampName: string]: Scale }
-): Theme {
-  let ramps: typeof color_ramps = {};
-  // 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 color_ramps) {
-      ramps[rampName] = chroma
-        .scale((color_ramps[rampName].colors as any)())
-        .domain([1, 0]);
-    }
-    ramps.neutral = chroma
-      .scale((color_ramps.neutral.colors as any)())
-      .domain([7, 0]);
-  } else {
-    for (var rampName in color_ramps) {
-      ramps[rampName] = chroma
-        .scale((color_ramps[rampName].colors as any)())
-        .domain([0, 1]);
-    }
-    ramps.neutral = chroma
-      .scale((color_ramps.neutral.colors as any)())
-      .domain([0, 7]);
-  }
-
-  let blend = isLight ? 0.12 : 0.24;
-
-  function sample(ramp: Scale, index: number): string {
-    return ramp(index).hex();
-  }
-  const darkest = ramps.neutral(isLight ? 7 : 0).hex();
-
-  const backgroundColor = {
-    // Title bar
-    100: {
-      base: sample(ramps.neutral, 1.25),
-      hovered: sample(ramps.neutral, 1.5),
-      active: sample(ramps.neutral, 1.75),
-    },
-    // Midground (panels, etc)
-    300: {
-      base: sample(ramps.neutral, 1),
-      hovered: sample(ramps.neutral, 1.25),
-      active: sample(ramps.neutral, 1.5),
-    },
-    // Editor
-    500: {
-      base: sample(ramps.neutral, 0),
-      hovered: sample(ramps.neutral, 0.25),
-      active: sample(ramps.neutral, 0.5),
-    },
-    on300: {
-      base: sample(ramps.neutral, 0),
-      hovered: sample(ramps.neutral, 0.5),
-      active: sample(ramps.neutral, 1),
-    },
-    on500: {
-      base: sample(ramps.neutral, 1),
-      hovered: sample(ramps.neutral, 1.5),
-      active: sample(ramps.neutral, 2),
-    },
-    ok: {
-      base: withOpacity(sample(ramps.green, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
-      active: withOpacity(sample(ramps.green, 0.5), 0.25),
-    },
-    error: {
-      base: withOpacity(sample(ramps.red, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
-      active: withOpacity(sample(ramps.red, 0.5), 0.25),
-    },
-    on500Error: {
-      base: sample(ramps.red, 0.05),
-      hovered: sample(ramps.red, 0.1),
-      active: sample(ramps.red, 0.15),
-    },
-    warning: {
-      base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
-      active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
-    },
-    on500Warning: {
-      base: sample(ramps.yellow, 0.05),
-      hovered: sample(ramps.yellow, 0.1),
-      active: sample(ramps.yellow, 0.15),
-    },
-    info: {
-      base: withOpacity(sample(ramps.blue, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
-      active: withOpacity(sample(ramps.blue, 0.5), 0.25),
-    },
-    on500Info: {
-      base: sample(ramps.blue, 0.05),
-      hovered: sample(ramps.blue, 0.1),
-      active: sample(ramps.blue, 0.15),
-    },
-    on500Ok: {
-      base: sample(ramps.green, 0.05),
-      hovered: sample(ramps.green, 0.1),
-      active: sample(ramps.green, 0.15),
-    },
-  };
-
-  const borderColor = {
-    primary: sample(ramps.neutral, isLight ? 1.5 : 0),
-    secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
-    muted: sample(ramps.neutral, isLight ? 1.25 : 3),
-    active: sample(ramps.neutral, isLight ? 4 : 3),
-    onMedia: withOpacity(darkest, 0.1),
-    ok: sample(ramps.green, 0.3),
-    error: sample(ramps.red, 0.3),
-    warning: sample(ramps.yellow, 0.3),
-    info: sample(ramps.blue, 0.3),
-  };
-
-  const textColor = {
-    primary: sample(ramps.neutral, 6),
-    secondary: sample(ramps.neutral, 5),
-    muted: sample(ramps.neutral, 4),
-    placeholder: sample(ramps.neutral, 3),
-    active: sample(ramps.neutral, 7),
-    feature: sample(ramps.blue, 0.5),
-    ok: sample(ramps.green, 0.5),
-    error: sample(ramps.red, 0.5),
-    warning: sample(ramps.yellow, 0.5),
-    info: sample(ramps.blue, 0.5),
-    onMedia: darkest,
-  };
-
-  const player = {
-    1: buildPlayer(sample(ramps.blue, 0.5)),
-    2: buildPlayer(sample(ramps.green, 0.5)),
-    3: buildPlayer(sample(ramps.magenta, 0.5)),
-    4: buildPlayer(sample(ramps.orange, 0.5)),
-    5: buildPlayer(sample(ramps.violet, 0.5)),
-    6: buildPlayer(sample(ramps.cyan, 0.5)),
-    7: buildPlayer(sample(ramps.red, 0.5)),
-    8: buildPlayer(sample(ramps.yellow, 0.5)),
-  };
-
-  const editor = {
-    background: backgroundColor[500].base,
-    indent_guide: borderColor.muted,
-    indent_guide_active: borderColor.secondary,
-    line: {
-      active: sample(ramps.neutral, 1),
-      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
-    },
-    highlight: {
-      selection: player[1].selectionColor,
-      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
-      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
-      matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
-      match: sample(ramps.violet, 0.15),
-      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
-      related: backgroundColor[500].hovered,
-    },
-    gutter: {
-      primary: textColor.placeholder,
-      active: textColor.active,
-    },
-  };
-
-  const syntax: Syntax = {
-    primary: {
-      color: sample(ramps.neutral, 7),
-      weight: fontWeights.normal,
-    },
-    "variable.special": {
-      color: sample(ramps.blue, 0.8),
-      weight: fontWeights.normal,
-    },
-    comment: {
-      color: sample(ramps.neutral, 5),
-      weight: fontWeights.normal,
-    },
-    punctuation: {
-      color: sample(ramps.neutral, 6),
-      weight: fontWeights.normal,
-    },
-    constant: {
-      color: sample(ramps.neutral, 4),
-      weight: fontWeights.normal,
-    },
-    keyword: {
-      color: sample(ramps.blue, 0.5),
-      weight: fontWeights.normal,
-    },
-    function: {
-      color: sample(ramps.yellow, 0.5),
-      weight: fontWeights.normal,
-    },
-    type: {
-      color: sample(ramps.cyan, 0.5),
-      weight: fontWeights.normal,
-    },
-    constructor: {
-      color: sample(ramps.cyan, 0.5),
-      weight: fontWeights.normal,
-    },
-    property: {
-      color: sample(ramps.blue, 0.6),
-      weight: fontWeights.normal,
-    },
-    enum: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    operator: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    string: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    number: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-    },
-    boolean: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-    },
-    predictive: {
-      color: textColor.muted,
-      weight: fontWeights.normal,
-    },
-    title: {
-      color: sample(ramps.yellow, 0.5),
-      weight: fontWeights.bold,
-    },
-    emphasis: {
-      color: textColor.feature,
-      weight: fontWeights.normal,
-    },
-    "emphasis.strong": {
-      color: textColor.feature,
-      weight: fontWeights.bold,
-    },
-    linkUri: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-      underline: true,
-    },
-    linkText: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-      italic: true,
-    },
-  };
-
-  const shadow = withOpacity(
-    ramps
-      .neutral(isLight ? 7 : 0)
-      .darken()
-      .hex(),
-    blend
-  );
-
-  return {
-    name,
-    isLight,
-    backgroundColor,
-    borderColor,
-    textColor,
-    iconColor: textColor,
-    editor,
-    syntax,
-    player,
-    shadow,
-    ramps,
-  };
-}

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

@@ -1,100 +1,138 @@
-import { Scale } from "chroma-js";
+import { Scale } from "chroma-js"
+import { FontWeight } from "../../common"
 
 export interface ColorScheme {
-  name: string;
-  isLight: boolean;
+    name: string
+    isLight: boolean
 
-  lowest: Layer;
-  middle: Layer;
-  highest: Layer;
+    lowest: Layer
+    middle: Layer
+    highest: Layer
 
-  ramps: RampSet;
+    ramps: RampSet
 
-  popoverShadow: Shadow;
-  modalShadow: Shadow;
+    popoverShadow: Shadow
+    modalShadow: Shadow
 
-  players: Players;
+    players: Players
+    syntax?: Partial<ThemeSyntax>
 }
 
 export interface Meta {
-  name: string,
-  author: string,
-  url: string,
-  license: License
+    name: string
+    author: string
+    url: string
+    license: License
 }
 
 export interface License {
-  SPDX: SPDXExpression,
-  /// A url where we can download the license's text
-  https_url: string,
-  license_checksum: string
+    SPDX: SPDXExpression
+    /// A url where we can download the license's text
+    https_url: string
+    license_checksum: string
 }
 
 // License name -> License text
 export interface Licenses {
-  [key: string]: string
+    [key: string]: string
 }
 
 // FIXME: Add support for the SPDX expression syntax
-export type SPDXExpression = "MIT";
+export type SPDXExpression = "MIT"
 
 export interface Player {
-  cursor: string;
-  selection: string;
+    cursor: string
+    selection: string
 }
 
 export interface Players {
-  "0": Player;
-  "1": Player;
-  "2": Player;
-  "3": Player;
-  "4": Player;
-  "5": Player;
-  "6": Player;
-  "7": Player;
+    "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[];
+    blur: number
+    color: string
+    offset: number[]
 }
 
-export type StyleSets = keyof Layer;
+export type StyleSets = keyof Layer
 export interface Layer {
-  base: StyleSet;
-  variant: StyleSet;
-  on: StyleSet;
-  accent: StyleSet;
-  positive: StyleSet;
-  warning: StyleSet;
-  negative: StyleSet;
+    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;
+    neutral: Scale
+    red: Scale
+    orange: Scale
+    yellow: Scale
+    green: Scale
+    cyan: Scale
+    blue: Scale
+    violet: Scale
+    magenta: Scale
 }
 
-export type Styles = keyof StyleSet;
+export type Styles = keyof StyleSet
 export interface StyleSet {
-  default: Style;
-  active: Style;
-  disabled: Style;
-  hovered: Style;
-  pressed: Style;
-  inverted: Style;
+    default: Style
+    active: Style
+    disabled: Style
+    hovered: Style
+    pressed: Style
+    inverted: Style
 }
 
 export interface Style {
-  background: string;
-  border: string;
-  foreground: string;
+    background: string
+    border: string
+    foreground: string
 }
+
+export interface SyntaxHighlightStyle {
+    color: string
+    weight?: FontWeight
+    underline?: boolean
+    italic?: boolean
+}
+
+export interface Syntax {
+    primary: SyntaxHighlightStyle
+    "variable.special": SyntaxHighlightStyle
+    comment: SyntaxHighlightStyle
+    punctuation: SyntaxHighlightStyle
+    constant: SyntaxHighlightStyle
+    keyword: SyntaxHighlightStyle
+    function: SyntaxHighlightStyle
+    type: SyntaxHighlightStyle
+    constructor: SyntaxHighlightStyle
+    variant: SyntaxHighlightStyle
+    property: SyntaxHighlightStyle
+    enum: SyntaxHighlightStyle
+    operator: SyntaxHighlightStyle
+    string: SyntaxHighlightStyle
+    number: SyntaxHighlightStyle
+    boolean: SyntaxHighlightStyle
+    predictive: SyntaxHighlightStyle
+    title: SyntaxHighlightStyle
+    emphasis: SyntaxHighlightStyle
+    "emphasis.strong": SyntaxHighlightStyle
+    linkUri: SyntaxHighlightStyle
+    linkText: SyntaxHighlightStyle
+}
+
+// HACK: "constructor" as a key in the syntax interface returns an error when a theme tries to use it.
+// For now hack around it by omiting constructor as a valid key for overrides.
+export type ThemeSyntax = Partial<Omit<Syntax, "constructor">>

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

@@ -1,210 +1,215 @@
-import chroma, { Color, Scale } from "chroma-js";
+import chroma, { Color, Scale } from "chroma-js"
 import {
-  ColorScheme,
-  Layer,
-  Player,
-  RampSet,
-  Style,
-  Styles,
-  StyleSet,
-} from "./colorScheme";
+    ColorScheme,
+    Layer,
+    Player,
+    RampSet,
+    Style,
+    Styles,
+    StyleSet,
+    ThemeSyntax,
+} 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");
+    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 }
+    name: string,
+    isLight: boolean,
+    colorRamps: { [rampName: string]: Scale },
+    syntax?: ThemeSyntax
 ): 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()
-      );
+    // 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),
     }
-    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));
+
+    return {
+        name,
+        isLight,
+
+        ramps,
+
+        lowest,
+        middle,
+        highest,
+
+        popoverShadow,
+        modalShadow,
+
+        players,
+        syntax,
     }
-    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(),
-  };
+    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),
-  };
+    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),
-  };
+    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),
-  };
+    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
+    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();
+    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]),
+        }
     }
-  }
 
-  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"),
-  };
+        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
+    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,
-    },
-  };
+    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/common/theme.ts πŸ”—

@@ -1,165 +0,0 @@
-import { Scale } from "chroma-js";
-import { FontWeight } from "../../common";
-import { withOpacity } from "../../utils/color";
-
-export interface SyntaxHighlightStyle {
-  color: string;
-  weight?: FontWeight;
-  underline?: boolean;
-  italic?: boolean;
-}
-
-export interface Player {
-  baseColor: string;
-  cursorColor: string;
-  selectionColor: string;
-  borderColor: string;
-}
-export function buildPlayer(
-  color: string,
-  cursorOpacity?: number,
-  selectionOpacity?: number,
-  borderOpacity?: number
-) {
-  return {
-    baseColor: color,
-    cursorColor: withOpacity(color, cursorOpacity || 1.0),
-    selectionColor: withOpacity(color, selectionOpacity || 0.24),
-    borderColor: withOpacity(color, borderOpacity || 0.8),
-  };
-}
-
-export interface BackgroundColorSet {
-  base: string;
-  hovered: string;
-  active: string;
-}
-
-export interface Syntax {
-  primary: SyntaxHighlightStyle;
-  comment: SyntaxHighlightStyle;
-  punctuation: SyntaxHighlightStyle;
-  constant: SyntaxHighlightStyle;
-  keyword: SyntaxHighlightStyle;
-  function: SyntaxHighlightStyle;
-  type: SyntaxHighlightStyle;
-  constructor: SyntaxHighlightStyle;
-  property: SyntaxHighlightStyle;
-  enum: SyntaxHighlightStyle;
-  operator: SyntaxHighlightStyle;
-  string: SyntaxHighlightStyle;
-  number: SyntaxHighlightStyle;
-  boolean: SyntaxHighlightStyle;
-  predictive: SyntaxHighlightStyle;
-  title: SyntaxHighlightStyle;
-  emphasis: SyntaxHighlightStyle;
-  linkUri: SyntaxHighlightStyle;
-  linkText: SyntaxHighlightStyle;
-
-  [key: string]: SyntaxHighlightStyle;
-}
-
-export default interface Theme {
-  name: string;
-  isLight: boolean;
-  backgroundColor: {
-    // Basically just Title Bar
-    // Lowest background level
-    100: BackgroundColorSet;
-    // Tab bars, panels, popovers
-    // Mid-ground
-    300: BackgroundColorSet;
-    // The editor
-    // Foreground
-    500: BackgroundColorSet;
-    // Hacks for elements on top of the midground
-    // Buttons in a panel, tab bar, or panel
-    on300: BackgroundColorSet;
-    // Hacks for elements on top of the editor
-    on500: BackgroundColorSet;
-    ok: BackgroundColorSet;
-    on500Ok: BackgroundColorSet;
-    error: BackgroundColorSet;
-    on500Error: BackgroundColorSet;
-    warning: BackgroundColorSet;
-    on500Warning: BackgroundColorSet;
-    info: BackgroundColorSet;
-    on500Info: BackgroundColorSet;
-  };
-  borderColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    active: string;
-    /**
-     * Used for rendering borders on top of media like avatars, images, video, etc.
-     */
-    onMedia: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-  };
-  textColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    placeholder: string;
-    active: string;
-    feature: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-    onMedia: string;
-  };
-  iconColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    placeholder: string;
-    active: string;
-    feature: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-  };
-  editor: {
-    background: string;
-    indent_guide: string;
-    indent_guide_active: string;
-    line: {
-      active: string;
-      highlighted: string;
-    };
-    highlight: {
-      selection: string;
-      occurrence: string;
-      activeOccurrence: string;
-      matchingBracket: string;
-      match: string;
-      activeMatch: string;
-      related: string;
-    };
-    gutter: {
-      primary: string;
-      active: string;
-    };
-  };
-
-  syntax: Syntax;
-
-  player: {
-    1: Player;
-    2: Player;
-    3: Player;
-    4: Player;
-    5: Player;
-    6: Player;
-    7: Player;
-    8: Player;
-  };
-  shadow: string;
-  ramps: { [rampName: string]: Scale };
-}

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

@@ -1,40 +1,69 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta, ThemeSyntax } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "One Dark";
+const name = "One Dark"
 
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma
-    .scale([
-      "#282c34",
-      "#353b45",
-      "#3e4451",
-      "#545862",
-      "#565c64",
-      "#abb2bf",
-      "#b6bdca",
-      "#c8ccd4",
-    ])
-    .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+const color = {
+    white: "#ACB2BE",
+    grey: "#5D636F",
+    red: "#D07277",
+    orange: "#C0966B",
+    yellow: "#DFC184",
+    green: "#A1C181",
+    teal: "#6FB4C0",
+    blue: "#74ADE9",
+    purple: "#B478CF",
+}
+
+const ramps = {
+    neutral: chroma
+        .scale([
+            "#282c34",
+            "#353b45",
+            "#3e4451",
+            "#545862",
+            "#565c64",
+            "#abb2bf",
+            "#b6bdca",
+            "#c8ccd4",
+        ])
+        .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+    red: colorRamp(chroma(color.red)),
+    orange: colorRamp(chroma(color.orange)),
+    yellow: colorRamp(chroma(color.yellow)),
+    green: colorRamp(chroma(color.green)),
+    cyan: colorRamp(chroma(color.teal)),
+    blue: colorRamp(chroma(color.blue)),
+    violet: colorRamp(chroma(color.purple)),
+    magenta: colorRamp(chroma("#be5046")),
+}
+
+const syntax: ThemeSyntax = {
+    primary: { color: color.white },
+    comment: { color: color.grey },
+    function: { color: color.blue },
+    type: { color: color.teal },
+    property: { color: color.red },
+    number: { color: color.orange },
+    string: { color: color.green },
+    keyword: { color: color.purple },
+    boolean: { color: color.orange },
+    punctuation: { color: color.white },
+    operator: { color: color.teal },
+}
 
-  red: colorRamp(chroma("#e06c75")),
-  orange: colorRamp(chroma("#d19a66")),
-  yellow: colorRamp(chroma("#e5c07b")),
-  green: colorRamp(chroma("#98c379")),
-  cyan: colorRamp(chroma("#56b6c2")),
-  blue: colorRamp(chroma("#61afef")),
-  violet: colorRamp(chroma("#c678dd")),
-  magenta: colorRamp(chroma("#be5046")),
-});
+export const dark = createColorScheme(name, false, ramps, syntax)
 
 export const meta: Meta = {
-  name,
-  author: "simurai",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-    license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8"
-  },
-  url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui"
+    name,
+    author: "simurai",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
+        license_checksum:
+            "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
+    },
+    url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
 }

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

@@ -1,39 +1,80 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { fontWeights } from "../common"
+import { Meta, ThemeSyntax } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "One Light";
+const name = "One Light"
 
-export const light = createColorScheme(`${name}`, true, {
-  neutral: chroma.scale([
-    "#090a0b",
-    "#202227",
-    "#383a42",
-    "#696c77",
-    "#a0a1a7",
-    "#e5e5e6",
-    "#f0f0f1",
-    "#fafafa",
-  ])
-    .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+const color = {
+    black: "#383A41",
+    grey: "#A2A3A7",
+    red: "#D36050",
+    orange: "#AD6F26",
+    yellow: "#DFC184",
+    green: "#659F58",
+    teal: "#3982B7",
+    blue: "#5B79E3",
+    purple: "#A449AB",
+    magenta: "#994EA6",
+}
+
+const ramps = {
+    neutral: chroma
+        .scale([
+            "#383A41",
+            "#535456",
+            "#696c77",
+            "#9D9D9F",
+            "#A9A9A9",
+            "#DBDBDC",
+            "#EAEAEB",
+            "#FAFAFA",
+        ])
+        .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+    red: colorRamp(chroma(color.red)),
+    orange: colorRamp(chroma(color.orange)),
+    yellow: colorRamp(chroma(color.yellow)),
+    green: colorRamp(chroma(color.green)),
+    cyan: colorRamp(chroma(color.teal)),
+    blue: colorRamp(chroma(color.blue)),
+    violet: colorRamp(chroma(color.purple)),
+    magenta: colorRamp(chroma(color.magenta)),
+}
+
+const syntax: ThemeSyntax = {
+    primary: { color: color.black },
+    "variable.special": { color: color.orange },
+    comment: { color: color.grey },
+    punctuation: { color: color.black },
+    keyword: { color: color.purple },
+    function: { color: color.blue },
+    type: { color: color.teal },
+    variant: { color: color.blue },
+    property: { color: color.red },
+    enum: { color: color.red },
+    operator: { color: color.teal },
+    string: { color: color.green },
+    number: { color: color.orange },
+    boolean: { color: color.orange },
+    title: { color: color.red, weight: fontWeights.normal },
+    "emphasis.strong": {
+        color: color.orange,
+    },
+    linkText: { color: color.blue },
+    linkUri: { color: color.teal },
+}
 
-  red: colorRamp(chroma("#ca1243")),
-  orange: colorRamp(chroma("#d75f00")),
-  yellow: colorRamp(chroma("#c18401")),
-  green: colorRamp(chroma("#50a14f")),
-  cyan: colorRamp(chroma("#0184bc")),
-  blue: colorRamp(chroma("#4078f2")),
-  violet: colorRamp(chroma("#a626a4")),
-  magenta: colorRamp(chroma("#986801")),
-});
+export const light = createColorScheme(name, true, ramps, syntax)
 
 export const meta: Meta = {
-  name,
-  author: "simurai",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-    license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8"
-  },
-  url: "https://github.com/atom/atom/tree/master/packages/one-light-ui"
+    name,
+    author: "simurai",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
+        license_checksum:
+            "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
+    },
+    url: "https://github.com/atom/atom/tree/master/packages/one-light-ui",
 }

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

@@ -1,41 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "RosΓ© Pine Dawn";
+const name = "RosΓ© Pine Dawn"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#679967")),
-  cyan: colorRamp(chroma("#286983")),
-  blue: colorRamp(chroma("#56949F")),
-  violet: colorRamp(chroma("#907AA9")),
-  magenta: colorRamp(chroma("#79549F")),
-};
+    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")),
+    green: colorRamp(chroma("#679967")),
+    cyan: colorRamp(chroma("#286983")),
+    blue: colorRamp(chroma("#56949F")),
+    violet: colorRamp(chroma("#907AA9")),
+    magenta: colorRamp(chroma("#79549F")),
+}
 
-export const light = createColorScheme(`${name}`, true, ramps);
+export const light = createColorScheme(`${name}`, true, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+        license_checksum:
+            "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

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

@@ -1,41 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "RosΓ© Pine Moon";
+const name = "RosΓ© Pine Moon"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#8DBD8D")),
-  cyan: colorRamp(chroma("#409BBE")),
-  blue: colorRamp(chroma("#9CCFD8")),
-  violet: colorRamp(chroma("#C4A7E7")),
-  magenta: colorRamp(chroma("#AB6FE9")),
-};
+    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")),
+    green: colorRamp(chroma("#8DBD8D")),
+    cyan: colorRamp(chroma("#409BBE")),
+    blue: colorRamp(chroma("#9CCFD8")),
+    violet: colorRamp(chroma("#C4A7E7")),
+    magenta: colorRamp(chroma("#AB6FE9")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+        license_checksum:
+            "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

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

@@ -1,39 +1,41 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "RosΓ© Pine";
+const name = "RosΓ© Pine"
 
 const ramps = {
-  neutral: chroma.scale([
-    "#191724",
-    "#1f1d2e",
-    "#26233A",
-    "#3E3A53",
-    "#56526C",
-    "#6E6A86",
-    "#908CAA",
-    "#E0DEF4",
-  ]),
-  red: colorRamp(chroma("#EB6F92")),
-  orange: colorRamp(chroma("#EBBCBA")),
-  yellow: colorRamp(chroma("#F6C177")),
-  green: colorRamp(chroma("#8DBD8D")),
-  cyan: colorRamp(chroma("#409BBE")),
-  blue: colorRamp(chroma("#9CCFD8")),
-  violet: colorRamp(chroma("#C4A7E7")),
-  magenta: colorRamp(chroma("#AB6FE9")),
-};
+    neutral: chroma.scale([
+        "#191724",
+        "#1f1d2e",
+        "#26233A",
+        "#3E3A53",
+        "#56526C",
+        "#6E6A86",
+        "#908CAA",
+        "#E0DEF4",
+    ]),
+    red: colorRamp(chroma("#EB6F92")),
+    orange: colorRamp(chroma("#EBBCBA")),
+    yellow: colorRamp(chroma("#F6C177")),
+    green: colorRamp(chroma("#8DBD8D")),
+    cyan: colorRamp(chroma("#409BBE")),
+    blue: colorRamp(chroma("#9CCFD8")),
+    violet: colorRamp(chroma("#C4A7E7")),
+    magenta: colorRamp(chroma("#AB6FE9")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+        license_checksum:
+            "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

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

@@ -1,40 +1,41 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Sandcastle";
+const name = "Sandcastle"
 
 const ramps = {
-  neutral: chroma.scale([
-    "#282c34",
-    "#2c323b",
-    "#3e4451",
-    "#665c54",
-    "#928374",
-    "#a89984",
-    "#d5c4a1",
-    "#fdf4c1",
-  ]),
-  red: colorRamp(chroma("#B4637A")),
-  orange: colorRamp(chroma("#a07e3b")),
-  yellow: colorRamp(chroma("#a07e3b")),
-  green: colorRamp(chroma("#83a598")),
-  cyan: colorRamp(chroma("#83a598")),
-  blue: colorRamp(chroma("#528b8b")),
-  violet: colorRamp(chroma("#d75f5f")),
-  magenta: colorRamp(chroma("#a87322")),
-};
+    neutral: chroma.scale([
+        "#282c34",
+        "#2c323b",
+        "#3e4451",
+        "#665c54",
+        "#928374",
+        "#a89984",
+        "#d5c4a1",
+        "#fdf4c1",
+    ]),
+    red: colorRamp(chroma("#B4637A")),
+    orange: colorRamp(chroma("#a07e3b")),
+    yellow: colorRamp(chroma("#a07e3b")),
+    green: colorRamp(chroma("#83a598")),
+    cyan: colorRamp(chroma("#83a598")),
+    blue: colorRamp(chroma("#528b8b")),
+    violet: colorRamp(chroma("#d75f5f")),
+    magenta: colorRamp(chroma("#a87322")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "gessig",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE",
-    license_checksum: "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc"
-  },
-  url: "https://github.com/gessig/base16-sandcastle-scheme"
+    name,
+    author: "gessig",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE",
+        license_checksum:
+            "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc",
+    },
+    url: "https://github.com/gessig/base16-sandcastle-scheme",
 }
-

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

@@ -1,43 +1,44 @@
-import chroma from "chroma-js";
-import { Meta as Metadata } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta as Metadata } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Solarized";
+const name = "Solarized"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#859900")),
-  cyan: colorRamp(chroma("#2aa198")),
-  blue: colorRamp(chroma("#268bd2")),
-  violet: colorRamp(chroma("#6c71c4")),
-  magenta: colorRamp(chroma("#d33682")),
-};
+    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")),
+    green: colorRamp(chroma("#859900")),
+    cyan: colorRamp(chroma("#2aa198")),
+    blue: colorRamp(chroma("#268bd2")),
+    violet: colorRamp(chroma("#6c71c4")),
+    magenta: colorRamp(chroma("#d33682")),
+}
 
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)
 
 export const meta: Metadata = {
-  name,
-  author: "Ethan Schoonover",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE",
-    license_checksum: "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6"
-  },
-  url: "https://github.com/altercation/solarized"
+    name,
+    author: "Ethan Schoonover",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE",
+        license_checksum:
+            "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6",
+    },
+    url: "https://github.com/altercation/solarized",
 }
-

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

@@ -1,31 +1,31 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Abruzzo";
-const author = "slightknack <hey@isaac.sh>";
-const url = "https://github.com/slightknack";
+const name = "Abruzzo"
+const author = "slightknack <hey@isaac.sh>"
+const url = "https://github.com/slightknack"
 const license = {
-  type: "",
-  url: ""
+    type: "",
+    url: "",
 }
 
 export const dark = createColorScheme(`${name}`, false, {
-  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")),
-});
+    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")),
+})

styles/src/themes/staff/atelier-dune.ts πŸ”—

@@ -1,34 +1,35 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Atelier Dune";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/";
+const name = "Atelier Dune"
+const author = "atelierbram"
+const url =
+    "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/"
 const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
+    type: "MIT",
+    url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
+}
 
 const ramps = {
-  neutral: chroma.scale([
-    "#20201d",
-    "#292824",
-    "#6e6b5e",
-    "#7d7a68",
-    "#999580",
-    "#a6a28c",
-    "#e8e4cf",
-    "#fefbec",
-  ]),
-  red: colorRamp(chroma("#d73737")),
-  orange: colorRamp(chroma("#b65611")),
-  yellow: colorRamp(chroma("#ae9513")),
-  green: colorRamp(chroma("#60ac39")),
-  cyan: colorRamp(chroma("#1fad83")),
-  blue: colorRamp(chroma("#6684e1")),
-  violet: colorRamp(chroma("#b854d4")),
-  magenta: colorRamp(chroma("#d43552")),
-};
+    neutral: chroma.scale([
+        "#20201d",
+        "#292824",
+        "#6e6b5e",
+        "#7d7a68",
+        "#999580",
+        "#a6a28c",
+        "#e8e4cf",
+        "#fefbec",
+    ]),
+    red: colorRamp(chroma("#d73737")),
+    orange: colorRamp(chroma("#b65611")),
+    yellow: colorRamp(chroma("#ae9513")),
+    green: colorRamp(chroma("#60ac39")),
+    cyan: colorRamp(chroma("#1fad83")),
+    blue: colorRamp(chroma("#6684e1")),
+    violet: colorRamp(chroma("#b854d4")),
+    magenta: colorRamp(chroma("#d43552")),
+}
 
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)

styles/src/themes/staff/atelier-heath.ts πŸ”—

@@ -1,53 +1,54 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Atelier Heath";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/";
+const name = "Atelier Heath"
+const author = "atelierbram"
+const url =
+    "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/"
 const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
+    type: "MIT",
+    url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma.scale([
-    "#1b181b",
-    "#292329",
-    "#695d69",
-    "#776977",
-    "#9e8f9e",
-    "#ab9bab",
-    "#d8cad8",
-    "#f7f3f7",
-  ]),
-  red: colorRamp(chroma("#ca402b")),
-  orange: colorRamp(chroma("#a65926")),
-  yellow: colorRamp(chroma("#bb8a35")),
-  green: colorRamp(chroma("#918b3b")),
-  cyan: colorRamp(chroma("#159393")),
-  blue: colorRamp(chroma("#516aec")),
-  violet: colorRamp(chroma("#7b59c0")),
-  magenta: colorRamp(chroma("#cc33cc")),
-});
+    neutral: chroma.scale([
+        "#1b181b",
+        "#292329",
+        "#695d69",
+        "#776977",
+        "#9e8f9e",
+        "#ab9bab",
+        "#d8cad8",
+        "#f7f3f7",
+    ]),
+    red: colorRamp(chroma("#ca402b")),
+    orange: colorRamp(chroma("#a65926")),
+    yellow: colorRamp(chroma("#bb8a35")),
+    green: colorRamp(chroma("#918b3b")),
+    cyan: colorRamp(chroma("#159393")),
+    blue: colorRamp(chroma("#516aec")),
+    violet: colorRamp(chroma("#7b59c0")),
+    magenta: colorRamp(chroma("#cc33cc")),
+})
 
 export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#161b1d",
-    "#1f292e",
-    "#516d7b",
-    "#5a7b8c",
-    "#7195a8",
-    "#7ea2b4",
-    "#c1e4f6",
-    "#ebf8ff",
-  ]),
-  red: colorRamp(chroma("#d22d72")),
-  orange: colorRamp(chroma("#935c25")),
-  yellow: colorRamp(chroma("#8a8a0f")),
-  green: colorRamp(chroma("#568c3b")),
-  cyan: colorRamp(chroma("#2d8f6f")),
-  blue: colorRamp(chroma("#257fad")),
-  violet: colorRamp(chroma("#6b6bb8")),
-  magenta: colorRamp(chroma("#b72dd2")),
-});
+    neutral: chroma.scale([
+        "#161b1d",
+        "#1f292e",
+        "#516d7b",
+        "#5a7b8c",
+        "#7195a8",
+        "#7ea2b4",
+        "#c1e4f6",
+        "#ebf8ff",
+    ]),
+    red: colorRamp(chroma("#d22d72")),
+    orange: colorRamp(chroma("#935c25")),
+    yellow: colorRamp(chroma("#8a8a0f")),
+    green: colorRamp(chroma("#568c3b")),
+    cyan: colorRamp(chroma("#2d8f6f")),
+    blue: colorRamp(chroma("#257fad")),
+    violet: colorRamp(chroma("#6b6bb8")),
+    magenta: colorRamp(chroma("#b72dd2")),
+})

styles/src/themes/staff/atelier-seaside.ts πŸ”—

@@ -1,34 +1,35 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Atelier Seaside";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/";
+const name = "Atelier Seaside"
+const author = "atelierbram"
+const url =
+    "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/"
 const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
+    type: "MIT",
+    url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
+}
 
 const ramps = {
-  neutral: chroma.scale([
-    "#131513",
-    "#242924",
-    "#5e6e5e",
-    "#687d68",
-    "#809980",
-    "#8ca68c",
-    "#cfe8cf",
-    "#f4fbf4",
-  ]),
-  red: colorRamp(chroma("#e6193c")),
-  orange: colorRamp(chroma("#87711d")),
-  yellow: colorRamp(chroma("#98981b")),
-  green: colorRamp(chroma("#29a329")),
-  cyan: colorRamp(chroma("#1999b3")),
-  blue: colorRamp(chroma("#3d62f5")),
-  violet: colorRamp(chroma("#ad2bee")),
-  magenta: colorRamp(chroma("#e619c3")),
-};
+    neutral: chroma.scale([
+        "#131513",
+        "#242924",
+        "#5e6e5e",
+        "#687d68",
+        "#809980",
+        "#8ca68c",
+        "#cfe8cf",
+        "#f4fbf4",
+    ]),
+    red: colorRamp(chroma("#e6193c")),
+    orange: colorRamp(chroma("#87711d")),
+    yellow: colorRamp(chroma("#98981b")),
+    green: colorRamp(chroma("#29a329")),
+    cyan: colorRamp(chroma("#1999b3")),
+    blue: colorRamp(chroma("#3d62f5")),
+    violet: colorRamp(chroma("#ad2bee")),
+    magenta: colorRamp(chroma("#e619c3")),
+}
 
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)

styles/src/themes/staff/ayu-mirage.ts πŸ”—

@@ -1,31 +1,31 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Ayu";
-const author = "Konstantin Pschera <me@kons.ch>";
-const url = "https://github.com/ayu-theme/ayu-colors";
+const name = "Ayu"
+const author = "Konstantin Pschera <me@kons.ch>"
+const url = "https://github.com/ayu-theme/ayu-colors"
 const license = {
-  type: "MIT",
-  url: "https://github.com/ayu-theme/ayu-colors/blob/master/license"
+    type: "MIT",
+    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
 }
 
 export const dark = createColorScheme(`${name} Mirage`, false, {
-  neutral: chroma.scale([
-    "#171B24",
-    "#1F2430",
-    "#242936",
-    "#707A8C",
-    "#8A9199",
-    "#CCCAC2",
-    "#D9D7CE",
-    "#F3F4F5",
-  ]),
-  red: colorRamp(chroma("#F28779")),
-  orange: colorRamp(chroma("#FFAD66")),
-  yellow: colorRamp(chroma("#FFD173")),
-  green: colorRamp(chroma("#D5FF80")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#5CCFE6")),
-  violet: colorRamp(chroma("#D4BFFF")),
-  magenta: colorRamp(chroma("#F29E74")),
-});
+    neutral: chroma.scale([
+        "#171B24",
+        "#1F2430",
+        "#242936",
+        "#707A8C",
+        "#8A9199",
+        "#CCCAC2",
+        "#D9D7CE",
+        "#F3F4F5",
+    ]),
+    red: colorRamp(chroma("#F28779")),
+    orange: colorRamp(chroma("#FFAD66")),
+    yellow: colorRamp(chroma("#FFD173")),
+    green: colorRamp(chroma("#D5FF80")),
+    cyan: colorRamp(chroma("#95E6CB")),
+    blue: colorRamp(chroma("#5CCFE6")),
+    violet: colorRamp(chroma("#D4BFFF")),
+    magenta: colorRamp(chroma("#F29E74")),
+})

styles/src/themes/staff/ayu.ts πŸ”—

@@ -1,52 +1,52 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Ayu";
-const author = "Konstantin Pschera <me@kons.ch>";
-const url = "https://github.com/ayu-theme/ayu-colors";
+const name = "Ayu"
+const author = "Konstantin Pschera <me@kons.ch>"
+const url = "https://github.com/ayu-theme/ayu-colors"
 const license = {
-  type: "MIT",
-  url: "https://github.com/ayu-theme/ayu-colors/blob/master/license"
+    type: "MIT",
+    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
 }
 
 export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma.scale([
-    "#0F1419",
-    "#131721",
-    "#272D38",
-    "#3E4B59",
-    "#BFBDB6",
-    "#E6E1CF",
-    "#E6E1CF",
-    "#F3F4F5",
-  ]),
-  red: colorRamp(chroma("#F07178")),
-  orange: colorRamp(chroma("#FF8F40")),
-  yellow: colorRamp(chroma("#FFB454")),
-  green: colorRamp(chroma("#B8CC52")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#59C2FF")),
-  violet: colorRamp(chroma("#D2A6FF")),
-  magenta: colorRamp(chroma("#E6B673")),
-});
+    neutral: chroma.scale([
+        "#0F1419",
+        "#131721",
+        "#272D38",
+        "#3E4B59",
+        "#BFBDB6",
+        "#E6E1CF",
+        "#E6E1CF",
+        "#F3F4F5",
+    ]),
+    red: colorRamp(chroma("#F07178")),
+    orange: colorRamp(chroma("#FF8F40")),
+    yellow: colorRamp(chroma("#FFB454")),
+    green: colorRamp(chroma("#B8CC52")),
+    cyan: colorRamp(chroma("#95E6CB")),
+    blue: colorRamp(chroma("#59C2FF")),
+    violet: colorRamp(chroma("#D2A6FF")),
+    magenta: colorRamp(chroma("#E6B673")),
+})
 
 export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#1A1F29",
-    "#242936",
-    "#5C6773",
-    "#828C99",
-    "#ABB0B6",
-    "#F8F9FA",
-    "#F3F4F5",
-    "#FAFAFA",
-  ]),
-  red: colorRamp(chroma("#F07178")),
-  orange: colorRamp(chroma("#FA8D3E")),
-  yellow: colorRamp(chroma("#F2AE49")),
-  green: colorRamp(chroma("#86B300")),
-  cyan: colorRamp(chroma("#4CBF99")),
-  blue: colorRamp(chroma("#36A3D9")),
-  violet: colorRamp(chroma("#A37ACC")),
-  magenta: colorRamp(chroma("#E6BA7E")),
-});
+    neutral: chroma.scale([
+        "#1A1F29",
+        "#242936",
+        "#5C6773",
+        "#828C99",
+        "#ABB0B6",
+        "#F8F9FA",
+        "#F3F4F5",
+        "#FAFAFA",
+    ]),
+    red: colorRamp(chroma("#F07178")),
+    orange: colorRamp(chroma("#FA8D3E")),
+    yellow: colorRamp(chroma("#F2AE49")),
+    green: colorRamp(chroma("#86B300")),
+    cyan: colorRamp(chroma("#4CBF99")),
+    blue: colorRamp(chroma("#36A3D9")),
+    violet: colorRamp(chroma("#A37ACC")),
+    magenta: colorRamp(chroma("#E6BA7E")),
+})

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

@@ -1,73 +1,73 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Brush Trees";
-const author = "Abraham White <abelincoln.white@gmail.com>";
-const url = "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme";
+const name = "Brush Trees"
+const author = "Abraham White <abelincoln.white@gmail.com>"
+const url = "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme"
 const license = {
-  type: "MIT",
-  url: "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme/blob/master/LICENSE"
+    type: "MIT",
+    url: "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme/blob/master/LICENSE",
 }
 
 export const dark = createColorScheme(`${name} Dark`, false, {
-  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")),
-});
+    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 mirage = createColorScheme(`${name} Mirage`, false, {
-  neutral: chroma.scale([
-    "#485867",
-    "#5A6D7A",
-    "#6D828E",
-    "#8299A1",
-    "#98AFB5",
-    "#B0C5C8",
-    "#C9DBDC",
-    "#E3EFEF",
-  ]),
-  red: colorRamp(chroma("#F28779")),
-  orange: colorRamp(chroma("#FFAD66")),
-  yellow: colorRamp(chroma("#FFD173")),
-  green: colorRamp(chroma("#D5FF80")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#5CCFE6")),
-  violet: colorRamp(chroma("#D4BFFF")),
-  magenta: colorRamp(chroma("#F29E74")),
-});
+    neutral: chroma.scale([
+        "#485867",
+        "#5A6D7A",
+        "#6D828E",
+        "#8299A1",
+        "#98AFB5",
+        "#B0C5C8",
+        "#C9DBDC",
+        "#E3EFEF",
+    ]),
+    red: colorRamp(chroma("#F28779")),
+    orange: colorRamp(chroma("#FFAD66")),
+    yellow: colorRamp(chroma("#FFD173")),
+    green: colorRamp(chroma("#D5FF80")),
+    cyan: colorRamp(chroma("#95E6CB")),
+    blue: colorRamp(chroma("#5CCFE6")),
+    violet: colorRamp(chroma("#D4BFFF")),
+    magenta: colorRamp(chroma("#F29E74")),
+})
 
 export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#1A1F29",
-    "#242936",
-    "#5C6773",
-    "#828C99",
-    "#ABB0B6",
-    "#F8F9FA",
-    "#F3F4F5",
-    "#FAFAFA",
-  ]),
-  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")),
-});
+    neutral: chroma.scale([
+        "#1A1F29",
+        "#242936",
+        "#5C6773",
+        "#828C99",
+        "#ABB0B6",
+        "#F8F9FA",
+        "#F3F4F5",
+        "#FAFAFA",
+    ]),
+    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")),
+})

styles/src/themes/staff/dracula.ts πŸ”—

@@ -1,31 +1,31 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Dracula";
-const author = "zenorocha";
-const url = "https://github.com/dracula/dracula-theme";
+const name = "Dracula"
+const author = "zenorocha"
+const url = "https://github.com/dracula/dracula-theme"
 const license = {
-  type: "MIT",
-  url: "https://github.com/dracula/dracula-theme/blob/master/LICENSE",
-};
+    type: "MIT",
+    url: "https://github.com/dracula/dracula-theme/blob/master/LICENSE",
+}
 
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#282A36",
-    "#3a3c4e",
-    "#4d4f68",
-    "#626483",
-    "#62d6e8",
-    "#e9e9f4",
-    "#f1f2f8",
-    "#f8f8f2",
-  ]),
-  red: colorRamp(chroma("#ff5555")),
-  orange: colorRamp(chroma("#ffb86c")),
-  yellow: colorRamp(chroma("#f1fa8c")),
-  green: colorRamp(chroma("#50fa7b")),
-  cyan: colorRamp(chroma("#8be9fd")),
-  blue: colorRamp(chroma("#6272a4")),
-  violet: colorRamp(chroma("#bd93f9")),
-  magenta: colorRamp(chroma("#00f769")),
-});
+    neutral: chroma.scale([
+        "#282A36",
+        "#3a3c4e",
+        "#4d4f68",
+        "#626483",
+        "#62d6e8",
+        "#e9e9f4",
+        "#f1f2f8",
+        "#f8f8f2",
+    ]),
+    red: colorRamp(chroma("#ff5555")),
+    orange: colorRamp(chroma("#ffb86c")),
+    yellow: colorRamp(chroma("#f1fa8c")),
+    green: colorRamp(chroma("#50fa7b")),
+    cyan: colorRamp(chroma("#8be9fd")),
+    blue: colorRamp(chroma("#6272a4")),
+    violet: colorRamp(chroma("#bd93f9")),
+    magenta: colorRamp(chroma("#00f769")),
+})

styles/src/themes/staff/gruvbox-medium.ts πŸ”—

@@ -1,138 +1,138 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Gruvbox";
-const author = "Dawid Kurek (dawikur@gmail.com)";
-const url = "https://github.com/morhetz/gruvbox";
+const name = "Gruvbox"
+const author = "Dawid Kurek (dawikur@gmail.com)"
+const url = "https://github.com/morhetz/gruvbox"
 const license = {
-  type: "MIT/X11",
-  url: "https://en.wikipedia.org/wiki/MIT_License",
-};
+    type: "MIT/X11",
+    url: "https://en.wikipedia.org/wiki/MIT_License",
+}
 
 export const dark = createColorScheme(`${name} Dark Medium`, false, {
-  neutral: chroma.scale([
-    "#282828",
-    "#3c3836",
-    "#504945",
-    "#665c54",
-    "#7C6F64",
-    "#928374",
-    "#A89984",
-    "#BDAE93",
-    "#D5C4A1",
-    "#EBDBB2",
-    "#FBF1C7",
-  ]),
-  red: chroma.scale([
-    "#4D150F",
-    "#7D241A",
-    "#A31C17",
-    "#CC241D",
-    "#C83A29",
-    "#FB4934",
-    "#F06D61",
-    "#E6928E",
-    "#FFFFFF",
-  ]),
-  orange: chroma.scale([
-    "#462307",
-    "#7F400C",
-    "#AB4A0B",
-    "#D65D0E",
-    "#CB6614",
-    "#FE8019",
-    "#F49750",
-    "#EBAE87",
-    "#FFFFFF",
-  ]),
-  yellow: chroma.scale([
-    "#3D2C05",
-    "#7D5E17",
-    "#AC7A1A",
-    "#D79921",
-    "#E8AB28",
-    "#FABD2F",
-    "#F2C45F",
-    "#EBCC90",
-    "#FFFFFF",
-  ]),
-  green: chroma.scale([
-    "#32330A",
-    "#5C5D13",
-    "#797814",
-    "#98971A",
-    "#93951E",
-    "#B8BB26",
-    "#C2C359",
-    "#CCCB8D",
-    "#FFFFFF",
-  ]),
-  cyan: chroma.scale([
-    "#283D20",
-    "#47603E",
-    "#537D54",
-    "#689D6A",
-    "#719963",
-    "#8EC07C",
-    "#A1C798",
-    "#B4CEB5",
-    "#FFFFFF",
-  ]),
-  blue: chroma.scale([
-    "#103738",
-    "#214C4D",
-    "#376A6C",
-    "#458588",
-    "#688479",
-    "#83A598",
-    "#92B3AE",
-    "#A2C2C4",
-    "#FFFFFF",
-  ]),
-  violet: chroma.scale([
-    "#392228",
-    "#69434D",
-    "#8D4E6B",
-    "#B16286",
-    "#A86B7C",
-    "#D3869B",
-    "#D59BAF",
-    "#D8B1C3",
-    "#FFFFFF",
-  ]),
-  magenta: chroma.scale([
-    "#48402C",
-    "#756D59",
-    "#867A69",
-    "#A89984",
-    "#BCAF8E",
-    "#EBDBB2",
-    "#DFD3BA",
-    "#D4CCC2",
-    "#FFFFFF",
-  ]),
-});
+    neutral: chroma.scale([
+        "#282828",
+        "#3c3836",
+        "#504945",
+        "#665c54",
+        "#7C6F64",
+        "#928374",
+        "#A89984",
+        "#BDAE93",
+        "#D5C4A1",
+        "#EBDBB2",
+        "#FBF1C7",
+    ]),
+    red: chroma.scale([
+        "#4D150F",
+        "#7D241A",
+        "#A31C17",
+        "#CC241D",
+        "#C83A29",
+        "#FB4934",
+        "#F06D61",
+        "#E6928E",
+        "#FFFFFF",
+    ]),
+    orange: chroma.scale([
+        "#462307",
+        "#7F400C",
+        "#AB4A0B",
+        "#D65D0E",
+        "#CB6614",
+        "#FE8019",
+        "#F49750",
+        "#EBAE87",
+        "#FFFFFF",
+    ]),
+    yellow: chroma.scale([
+        "#3D2C05",
+        "#7D5E17",
+        "#AC7A1A",
+        "#D79921",
+        "#E8AB28",
+        "#FABD2F",
+        "#F2C45F",
+        "#EBCC90",
+        "#FFFFFF",
+    ]),
+    green: chroma.scale([
+        "#32330A",
+        "#5C5D13",
+        "#797814",
+        "#98971A",
+        "#93951E",
+        "#B8BB26",
+        "#C2C359",
+        "#CCCB8D",
+        "#FFFFFF",
+    ]),
+    cyan: chroma.scale([
+        "#283D20",
+        "#47603E",
+        "#537D54",
+        "#689D6A",
+        "#719963",
+        "#8EC07C",
+        "#A1C798",
+        "#B4CEB5",
+        "#FFFFFF",
+    ]),
+    blue: chroma.scale([
+        "#103738",
+        "#214C4D",
+        "#376A6C",
+        "#458588",
+        "#688479",
+        "#83A598",
+        "#92B3AE",
+        "#A2C2C4",
+        "#FFFFFF",
+    ]),
+    violet: chroma.scale([
+        "#392228",
+        "#69434D",
+        "#8D4E6B",
+        "#B16286",
+        "#A86B7C",
+        "#D3869B",
+        "#D59BAF",
+        "#D8B1C3",
+        "#FFFFFF",
+    ]),
+    magenta: chroma.scale([
+        "#48402C",
+        "#756D59",
+        "#867A69",
+        "#A89984",
+        "#BCAF8E",
+        "#EBDBB2",
+        "#DFD3BA",
+        "#D4CCC2",
+        "#FFFFFF",
+    ]),
+})
 
 export const light = createColorScheme(`${name} Light Medium`, true, {
-  neutral: chroma.scale([
-    "#282828",
-    "#3c3836",
-    "#504945",
-    "#665c54",
-    "#7C6F64",
-    "#928374",
-    "#A89984",
-    "#BDAE93",
-    "#D5C4A1",
-    "#EBDBB2",
-    "#FBF1C7",
-  ]),
-  red: colorRamp(chroma("#9d0006")),
-  orange: colorRamp(chroma("#af3a03")),
-  yellow: colorRamp(chroma("#b57614")),
-  green: colorRamp(chroma("#79740e")),
-  cyan: colorRamp(chroma("#427b58")),
-  blue: colorRamp(chroma("#076678")),
-  violet: colorRamp(chroma("#8f3f71")),
-  magenta: colorRamp(chroma("#d65d0e")),
-});
+    neutral: chroma.scale([
+        "#282828",
+        "#3c3836",
+        "#504945",
+        "#665c54",
+        "#7C6F64",
+        "#928374",
+        "#A89984",
+        "#BDAE93",
+        "#D5C4A1",
+        "#EBDBB2",
+        "#FBF1C7",
+    ]),
+    red: colorRamp(chroma("#9d0006")),
+    orange: colorRamp(chroma("#af3a03")),
+    yellow: colorRamp(chroma("#b57614")),
+    green: colorRamp(chroma("#79740e")),
+    cyan: colorRamp(chroma("#427b58")),
+    blue: colorRamp(chroma("#076678")),
+    violet: colorRamp(chroma("#8f3f71")),
+    magenta: colorRamp(chroma("#d65d0e")),
+})

styles/src/themes/staff/monokai.ts πŸ”—

@@ -1,32 +1,32 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Monokai";
-const author = "Wimer Hazenberg (http://www.monokai.nl)";
-const url = "https://base16.netlify.app/previews/base16-monokai.html";
+const name = "Monokai"
+const author = "Wimer Hazenberg (http://www.monokai.nl)"
+const url = "https://base16.netlify.app/previews/base16-monokai.html"
 const license = {
-  type: "?",
-  url: "?",
-};
+    type: "?",
+    url: "?",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#272822",
-    "#383830",
-    "#49483e",
-    "#75715e",
-    "#a59f85",
-    "#f8f8f2",
-    "#f5f4f1",
-    "#f9f8f5",
-  ]),
-  red: colorRamp(chroma("#f92672")),
-  orange: colorRamp(chroma("#fd971f")),
-  yellow: colorRamp(chroma("#f4bf75")),
-  green: colorRamp(chroma("#a6e22e")),
-  cyan: colorRamp(chroma("#a1efe4")),
-  blue: colorRamp(chroma("#66d9ef")),
-  violet: colorRamp(chroma("#ae81ff")),
-  magenta: colorRamp(chroma("#cc6633")),
-});
+    neutral: chroma.scale([
+        "#272822",
+        "#383830",
+        "#49483e",
+        "#75715e",
+        "#a59f85",
+        "#f8f8f2",
+        "#f5f4f1",
+        "#f9f8f5",
+    ]),
+    red: colorRamp(chroma("#f92672")),
+    orange: colorRamp(chroma("#fd971f")),
+    yellow: colorRamp(chroma("#f4bf75")),
+    green: colorRamp(chroma("#a6e22e")),
+    cyan: colorRamp(chroma("#a1efe4")),
+    blue: colorRamp(chroma("#66d9ef")),
+    violet: colorRamp(chroma("#ae81ff")),
+    magenta: colorRamp(chroma("#cc6633")),
+})

styles/src/themes/staff/nord.ts πŸ”—

@@ -1,32 +1,32 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Nord";
-const author = "arcticicestudio";
-const url = "https://www.nordtheme.com/";
+const name = "Nord"
+const author = "arcticicestudio"
+const url = "https://www.nordtheme.com/"
 const license = {
-  type: "MIT",
-  url: "https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md",
-};
+    type: "MIT",
+    url: "https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#2E3440",
-    "#3B4252",
-    "#434C5E",
-    "#4C566A",
-    "#D8DEE9",
-    "#E5E9F0",
-    "#ECEFF4",
-    "#8FBCBB",
-  ]),
-  red: colorRamp(chroma("#88C0D0")),
-  orange: colorRamp(chroma("#81A1C1")),
-  yellow: colorRamp(chroma("#5E81AC")),
-  green: colorRamp(chroma("#BF616A")),
-  cyan: colorRamp(chroma("#D08770")),
-  blue: colorRamp(chroma("#EBCB8B")),
-  violet: colorRamp(chroma("#A3BE8C")),
-  magenta: colorRamp(chroma("#B48EAD")),
-});
+    neutral: chroma.scale([
+        "#2E3440",
+        "#3B4252",
+        "#434C5E",
+        "#4C566A",
+        "#D8DEE9",
+        "#E5E9F0",
+        "#ECEFF4",
+        "#8FBCBB",
+    ]),
+    red: colorRamp(chroma("#88C0D0")),
+    orange: colorRamp(chroma("#81A1C1")),
+    yellow: colorRamp(chroma("#5E81AC")),
+    green: colorRamp(chroma("#BF616A")),
+    cyan: colorRamp(chroma("#D08770")),
+    blue: colorRamp(chroma("#EBCB8B")),
+    violet: colorRamp(chroma("#A3BE8C")),
+    magenta: colorRamp(chroma("#B48EAD")),
+})

styles/src/themes/staff/seti-ui.ts πŸ”—

@@ -1,32 +1,32 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Seti UI";
-const author = "jesseweed";
-const url = "https://github.com/jesseweed/seti-ui";
+const name = "Seti UI"
+const author = "jesseweed"
+const url = "https://github.com/jesseweed/seti-ui"
 const license = {
-  type: "MIT",
-  url: "https://github.com/jesseweed/seti-ui/blob/master/LICENSE.md",
-};
+    type: "MIT",
+    url: "https://github.com/jesseweed/seti-ui/blob/master/LICENSE.md",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#151718",
-    "#262B30",
-    "#1E2326",
-    "#41535B",
-    "#43a5d5",
-    "#d6d6d6",
-    "#eeeeee",
-    "#ffffff",
-  ]),
-  red: colorRamp(chroma("#Cd3f45")),
-  orange: colorRamp(chroma("#db7b55")),
-  yellow: colorRamp(chroma("#e6cd69")),
-  green: colorRamp(chroma("#9fca56")),
-  cyan: colorRamp(chroma("#55dbbe")),
-  blue: colorRamp(chroma("#55b5db")),
-  violet: colorRamp(chroma("#a074c4")),
-  magenta: colorRamp(chroma("#8a553f")),
-});
+    neutral: chroma.scale([
+        "#151718",
+        "#262B30",
+        "#1E2326",
+        "#41535B",
+        "#43a5d5",
+        "#d6d6d6",
+        "#eeeeee",
+        "#ffffff",
+    ]),
+    red: colorRamp(chroma("#Cd3f45")),
+    orange: colorRamp(chroma("#db7b55")),
+    yellow: colorRamp(chroma("#e6cd69")),
+    green: colorRamp(chroma("#9fca56")),
+    cyan: colorRamp(chroma("#55dbbe")),
+    blue: colorRamp(chroma("#55b5db")),
+    violet: colorRamp(chroma("#a074c4")),
+    magenta: colorRamp(chroma("#8a553f")),
+})

styles/src/themes/staff/tokyo-night-storm.ts πŸ”—

@@ -1,32 +1,32 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Tokyo Night Storm";
-const author = "folke";
-const url = "https://github.com/folke/tokyonight.nvim";
+const name = "Tokyo Night Storm"
+const author = "folke"
+const url = "https://github.com/folke/tokyonight.nvim"
 const license = {
-  type: "MIT",
-  url: "https://github.com/ghifarit53/tokyonight-vim/blob/master/LICENSE",
-};
+    type: "MIT",
+    url: "https://github.com/ghifarit53/tokyonight-vim/blob/master/LICENSE",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#24283B",
-    "#16161E",
-    "#343A52",
-    "#444B6A",
-    "#787C99",
-    "#A9B1D6",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#C0CAF5")),
-  orange: colorRamp(chroma("#A9B1D6")),
-  yellow: colorRamp(chroma("#0DB9D7")),
-  green: colorRamp(chroma("#9ECE6A")),
-  cyan: colorRamp(chroma("#B4F9F8")),
-  blue: colorRamp(chroma("#2AC3DE")),
-  violet: colorRamp(chroma("#BB9AF7")),
-  magenta: colorRamp(chroma("#F7768E")),
-});
+    neutral: chroma.scale([
+        "#24283B",
+        "#16161E",
+        "#343A52",
+        "#444B6A",
+        "#787C99",
+        "#A9B1D6",
+        "#CBCCD1",
+        "#D5D6DB",
+    ]),
+    red: colorRamp(chroma("#C0CAF5")),
+    orange: colorRamp(chroma("#A9B1D6")),
+    yellow: colorRamp(chroma("#0DB9D7")),
+    green: colorRamp(chroma("#9ECE6A")),
+    cyan: colorRamp(chroma("#B4F9F8")),
+    blue: colorRamp(chroma("#2AC3DE")),
+    violet: colorRamp(chroma("#BB9AF7")),
+    magenta: colorRamp(chroma("#F7768E")),
+})

styles/src/themes/staff/tokyo-night.ts πŸ”—

@@ -1,53 +1,53 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Tokyo";
-const author = "folke";
-const url = "https://github.com/folke/tokyonight.nvim";
+const name = "Tokyo"
+const author = "folke"
+const url = "https://github.com/folke/tokyonight.nvim"
 const license = {
-  type: "Apache License 2.0",
-  url: "https://github.com/folke/tokyonight.nvim/blob/main/LICENSE",
-};
+    type: "Apache License 2.0",
+    url: "https://github.com/folke/tokyonight.nvim/blob/main/LICENSE",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name} Night`, false, {
-  neutral: chroma.scale([
-    "#1A1B26",
-    "#16161E",
-    "#2F3549",
-    "#444B6A",
-    "#787C99",
-    "#A9B1D6",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#C0CAF5")),
-  orange: colorRamp(chroma("#A9B1D6")),
-  yellow: colorRamp(chroma("#0DB9D7")),
-  green: colorRamp(chroma("#9ECE6A")),
-  cyan: colorRamp(chroma("#B4F9F8")),
-  blue: colorRamp(chroma("#2AC3DE")),
-  violet: colorRamp(chroma("#BB9AF7")),
-  magenta: colorRamp(chroma("#F7768E")),
-});
+    neutral: chroma.scale([
+        "#1A1B26",
+        "#16161E",
+        "#2F3549",
+        "#444B6A",
+        "#787C99",
+        "#A9B1D6",
+        "#CBCCD1",
+        "#D5D6DB",
+    ]),
+    red: colorRamp(chroma("#C0CAF5")),
+    orange: colorRamp(chroma("#A9B1D6")),
+    yellow: colorRamp(chroma("#0DB9D7")),
+    green: colorRamp(chroma("#9ECE6A")),
+    cyan: colorRamp(chroma("#B4F9F8")),
+    blue: colorRamp(chroma("#2AC3DE")),
+    violet: colorRamp(chroma("#BB9AF7")),
+    magenta: colorRamp(chroma("#F7768E")),
+})
 
 export const light = createColorScheme(`${name} Day`, true, {
-  neutral: chroma.scale([
-    "#1A1B26",
-    "#1A1B26",
-    "#343B59",
-    "#4C505E",
-    "#9699A3",
-    "#DFE0E5",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#343B58")),
-  orange: colorRamp(chroma("#965027")),
-  yellow: colorRamp(chroma("#166775")),
-  green: colorRamp(chroma("#485E30")),
-  cyan: colorRamp(chroma("#3E6968")),
-  blue: colorRamp(chroma("#34548A")),
-  violet: colorRamp(chroma("#5A4A78")),
-  magenta: colorRamp(chroma("#8C4351")),
-});
+    neutral: chroma.scale([
+        "#1A1B26",
+        "#1A1B26",
+        "#343B59",
+        "#4C505E",
+        "#9699A3",
+        "#DFE0E5",
+        "#CBCCD1",
+        "#D5D6DB",
+    ]),
+    red: colorRamp(chroma("#343B58")),
+    orange: colorRamp(chroma("#965027")),
+    yellow: colorRamp(chroma("#166775")),
+    green: colorRamp(chroma("#485E30")),
+    cyan: colorRamp(chroma("#3E6968")),
+    blue: colorRamp(chroma("#34548A")),
+    violet: colorRamp(chroma("#5A4A78")),
+    magenta: colorRamp(chroma("#8C4351")),
+})

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

@@ -1,36 +1,36 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Zed Pro";
+const name = "Zed Pro"
 const author = "Nate Butler"
 const url = "https://github.com/iamnbutler"
 const license = {
-  type: "?",
-  url: "?",
-};
+    type: "?",
+    url: "?",
+}
 
 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")),
-};
+    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);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)

styles/src/themes/staff/zenburn.ts πŸ”—

@@ -1,32 +1,32 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Zenburn";
-const author = "elnawe";
-const url = "https://github.com/elnawe/base16-zenburn-scheme";
+const name = "Zenburn"
+const author = "elnawe"
+const url = "https://github.com/elnawe/base16-zenburn-scheme"
 const license = {
-  type: "None",
-  url: "",
-};
+    type: "None",
+    url: "",
+}
 
 // `name-[light|dark]`, isLight, color ramps
 export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#383838",
-    "#404040",
-    "#606060",
-    "#6f6f6f",
-    "#808080",
-    "#dcdccc",
-    "#c0c0c0",
-    "#ffffff",
-  ]),
-  red: colorRamp(chroma("#dca3a3")),
-  orange: colorRamp(chroma("#dfaf8f")),
-  yellow: colorRamp(chroma("#e0cf9f")),
-  green: colorRamp(chroma("#5f7f5f")),
-  cyan: colorRamp(chroma("#93e0e3")),
-  blue: colorRamp(chroma("#7cb8bb")),
-  violet: colorRamp(chroma("#dc8cc3")),
-  magenta: colorRamp(chroma("#000000")),
-});
+    neutral: chroma.scale([
+        "#383838",
+        "#404040",
+        "#606060",
+        "#6f6f6f",
+        "#808080",
+        "#dcdccc",
+        "#c0c0c0",
+        "#ffffff",
+    ]),
+    red: colorRamp(chroma("#dca3a3")),
+    orange: colorRamp(chroma("#dfaf8f")),
+    yellow: colorRamp(chroma("#e0cf9f")),
+    green: colorRamp(chroma("#5f7f5f")),
+    cyan: colorRamp(chroma("#93e0e3")),
+    blue: colorRamp(chroma("#7cb8bb")),
+    violet: colorRamp(chroma("#dc8cc3")),
+    magenta: colorRamp(chroma("#000000")),
+})

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

@@ -1,40 +1,42 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Summercamp";
+const name = "Summercamp"
 
 const ramps = {
-  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")),
-  green: colorRamp(chroma("#5ceb5a")),
-  cyan: colorRamp(chroma("#5aebbc")),
-  blue: colorRamp(chroma("#489bf0")),
-  violet: colorRamp(chroma("#FF8080")),
-  magenta: colorRamp(chroma("#F69BE7")),
-};
+    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")),
+    green: colorRamp(chroma("#5ceb5a")),
+    cyan: colorRamp(chroma("#5aebbc")),
+    blue: colorRamp(chroma("#489bf0")),
+    violet: colorRamp(chroma("#FF8080")),
+    magenta: colorRamp(chroma("#F69BE7")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(`${name}`, false, ramps)
 export const meta: Meta = {
-  name,
-  author: "zoefiri",
-  url: "https://github.com/zoefiri/base16-sc",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE",
-    license_checksum: "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf"
-  }
-}
+    name,
+    author: "zoefiri",
+    url: "https://github.com/zoefiri/base16-sc",
+    license: {
+        SPDX: "MIT",
+        https_url:
+            "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE",
+        license_checksum:
+            "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf",
+    },
+}

styles/src/utils/color.ts πŸ”—

@@ -1,5 +1,5 @@
-import chroma from "chroma-js";
+import chroma from "chroma-js"
 
 export function withOpacity(color: string, opacity: number): string {
-  return chroma(color).alpha(opacity).hex();
+    return chroma(color).alpha(opacity).hex()
 }

styles/src/utils/snakeCase.ts πŸ”—

@@ -1,35 +1,35 @@
-import { snakeCase } from "case-anything";
+import { snakeCase } from "case-anything"
 
 // https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case
 
 // Typescript magic to convert any string from camelCase to snake_case at compile time
 type SnakeCase<S> = S extends string
-  ? S extends `${infer T}${infer U}`
-    ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
+    ? S extends `${infer T}${infer U}`
+        ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
+        : S
     : S
-  : S;
 
 type SnakeCased<Type> = {
-  [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>;
-};
+    [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
+}
 
 export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
-  const snakeObject: any = {};
-  for (const key in object) {
-    snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
-      snakeCaseValue(object[key]);
-  }
-  return snakeObject;
+    const snakeObject: any = {}
+    for (const key in object) {
+        snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
+            snakeCaseValue(object[key])
+    }
+    return snakeObject
 }
 
 function snakeCaseValue(value: any): any {
-  if (typeof value === "object") {
-    if (Array.isArray(value)) {
-      return value.map(snakeCaseValue);
+    if (typeof value === "object") {
+        if (Array.isArray(value)) {
+            return value.map(snakeCaseValue)
+        } else {
+            return snakeCaseTree(value)
+        }
     } else {
-      return snakeCaseTree(value);
+        return value
     }
-  } else {
-    return value;
-  }
 }

styles/tsconfig.json πŸ”—

@@ -1,12 +1,12 @@
 {
-  "compilerOptions": {
-    "target": "es2015",
-    "module": "commonjs",
-    "esModuleInterop": true,
-    "noImplicitAny": true,
-    "removeComments": true,
-    "preserveConstEnums": true,
-    "sourceMap": true
-  },
-  "exclude": ["node_modules"]
+    "compilerOptions": {
+        "target": "es2015",
+        "module": "commonjs",
+        "esModuleInterop": true,
+        "noImplicitAny": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true
+    },
+    "exclude": ["node_modules"]
 }