Split Interactive into Interactive and Toggleable (#2628)

This is a part of the intensity driven theme rewrite. 

It introduces the `toggle` and `interactive` helper functions to build
Toggle<T> and Interactive<T> styles for interactive elements in the
theme.

This PR also removes the `theme_testbench` crate and related actions.

Huge thanks to @osiewicz and @mikayla-maki for pushing this forward 🙏🏽

Release Notes:

- Updated the style of many interactive elements.
This commit is contained in:
Nate Butler 2023-06-21 15:03:08 -04:00 committed by GitHub
commit 6ab0143469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 4074 additions and 1216 deletions

13
Cargo.lock generated
View File

@ -6916,18 +6916,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "theme_testbench"
version = "0.1.0"
dependencies = [
"gpui",
"project",
"settings",
"smallvec",
"theme",
"workspace",
]
[[package]]
name = "thiserror"
version = "1.0.40"
@ -8888,7 +8876,6 @@ dependencies = [
"text",
"theme",
"theme_selector",
"theme_testbench",
"thiserror",
"tiny_http",
"toml",

View File

@ -61,7 +61,6 @@ members = [
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/theme_testbench",
"crates/util",
"crates/vim",
"crates/workspace",

View File

@ -326,7 +326,7 @@ impl View for ActivityIndicator {
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
theme.hovered.as_ref().unwrap_or(&theme.default)
} else {
&theme.default
};

View File

@ -1233,19 +1233,19 @@ impl AssistantEditor {
cx,
|state, _| match message.role {
Role::User => {
let style = style.user_sender.style_for(state, false);
let style = style.user_sender.style_for(state);
Label::new("You", style.text.clone())
.contained()
.with_style(style.container)
}
Role::Assistant => {
let style = style.assistant_sender.style_for(state, false);
let style = style.assistant_sender.style_for(state);
Label::new("Assistant", style.text.clone())
.contained()
.with_style(style.container)
}
Role::System => {
let style = style.system_sender.style_for(state, false);
let style = style.system_sender.style_for(state);
Label::new("System", style.text.clone())
.contained()
.with_style(style.container)
@ -1484,7 +1484,7 @@ impl View for AssistantEditor {
Flex::row()
.with_child(
MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
let style = theme.model.style_for(state, false);
let style = theme.model.style_for(state);
Label::new(model, style.text.clone())
.contained()
.with_style(style.container)

View File

@ -49,7 +49,7 @@ impl View for UpdateNotification {
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -74,7 +74,7 @@ impl View for UpdateNotification {
),
)
.with_child({
let style = theme.action_message.style_for(state, false);
let style = theme.action_message.style_for(state);
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -83,7 +83,7 @@ impl View for Breadcrumbs {
}
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
let style = style.style_for(state);
crumbs.with_style(style.container)
})
.on_click(MouseButton::Left, |_, this, cx| {

View File

@ -299,7 +299,12 @@ impl CollabTitlebarItem {
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone();
let item_style = theme
.context_menu
.item
.inactive_state()
.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![
@ -361,8 +366,20 @@ impl CollabTitlebarItem {
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_left(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.with_margin_top(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.aligned(),
)
};
@ -372,7 +389,8 @@ impl CollabTitlebarItem {
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
.in_state(self.contacts_popover.is_some())
.style_for(state);
Svg::new("icons/user_plus_16.svg")
.with_color(style.color)
.constrained()
@ -419,7 +437,7 @@ impl CollabTitlebarItem {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
let style = titlebar.call_control.style_for(state);
Svg::new(icon)
.with_color(style.color)
.constrained()
@ -473,7 +491,7 @@ impl CollabTitlebarItem {
.with_child(
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistent width for both text variations
let style = titlebar.share_button.style_for(state, false);
let style = titlebar.share_button.inactive_state().style_for(state);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@ -511,7 +529,7 @@ impl CollabTitlebarItem {
Stack::new()
.with_child(
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
let style = titlebar.call_control.style_for(state);
Svg::new("icons/ellipsis_14.svg")
.with_color(style.color)
.constrained()
@ -549,7 +567,7 @@ impl CollabTitlebarItem {
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
let style = titlebar.sign_in_prompt.style_for(state, false);
let style = titlebar.sign_in_prompt.inactive_state().style_for(state);
Label::new("Sign In", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
.in_state(selected)
.style_for(mouse_state);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)

View File

@ -774,7 +774,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
.into_any()
}
@ -797,7 +798,7 @@ impl ContactList {
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.project_row.default;
let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
@ -810,8 +811,11 @@ impl ContactList {
};
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row()
.with_child(
@ -893,7 +897,7 @@ impl ContactList {
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.project_row.default;
let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
@ -904,8 +908,11 @@ impl ContactList {
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row()
.with_child(
@ -989,7 +996,8 @@ impl ContactList {
let header_style = theme
.header_row
.style_for(&mut Default::default(), is_selected);
.in_state(is_selected)
.style_for(&mut Default::default());
let text = match section {
Section::ActiveCall => "Collaborators",
Section::Requests => "Contact Requests",
@ -999,7 +1007,7 @@ impl ContactList {
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
let style = theme.leave_call.style_for(state);
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
@ -1110,8 +1118,7 @@ impl ContactList {
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style =
theme.contact_button.style_for(mouse_state, false);
let button_style = theme.contact_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
@ -1146,7 +1153,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
})
.on_click(MouseButton::Left, move |_, this, cx| {
@ -1204,7 +1212,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
})
@ -1227,7 +1235,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/check_8.svg")
.aligned()
@ -1250,7 +1258,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
@ -1277,7 +1285,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
.into_any()
}

View File

@ -53,7 +53,7 @@ where
)
.with_child(
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -93,7 +93,7 @@ where
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
let button = theme.button.style_for(state);
Label::new(message, button.text.clone())
.contained()
.with_style(button.container)

View File

@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate {
let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id];
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let key_style = &theme.command_palette.key.in_state(selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing;
Flex::row()

View File

@ -328,10 +328,8 @@ impl ContextMenu {
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(&mut Default::default());
match label {
ContextMenuItemLabel::String(label) => {
@ -363,10 +361,8 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { action, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(&mut Default::default());
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
@ -412,8 +408,8 @@ impl ContextMenu {
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(state);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new(

View File

@ -127,16 +127,16 @@ impl CopilotCodeVerification {
.with_child(
Label::new(
if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(),
device_code_style.cta.style_for(state).text.clone(),
)
.aligned()
.contained()
.with_style(*device_code_style.right_container.style_for(state, false))
.with_style(*device_code_style.right_container.style_for(state))
.constrained()
.with_width(device_code_style.right),
)
.contained()
.with_style(device_code_style.cta.style_for(state, false).container)
.with_style(device_code_style.cta.style_for(state).container)
})
.on_click(gpui::platform::MouseButton::Left, {
let user_code = data.user_code.clone();

View File

@ -71,7 +71,8 @@ impl View for CopilotButton {
.status_bar
.panel_buttons
.button
.style_for(state, active);
.in_state(active)
.style_for(state);
Flex::row()
.with_child(
@ -255,7 +256,7 @@ impl CopilotButton {
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.with_child(theme::ui::icon(icon_style.style_for(state)))
.align_children_center()
.into_any()
},

View File

@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
.workspace
.status_bar
.diagnostic_summary
.style_for(state, false);
.style_for(state);
let mut summary_row = Flex::row();
if self.summary.error_count > 0 {
@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(),
message_style.style_for(state).text.clone(),
)
.aligned()
.contained()

View File

@ -3320,15 +3320,21 @@ impl Editor {
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
active: bool,
is_active: bool,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
if self.available_code_actions.is_some() {
enum CodeActions {}
Some(
MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
Svg::new("icons/bolt_8.svg")
.with_color(style.code_actions.indicator.style_for(state, active).color)
Svg::new("icons/bolt_8.svg").with_color(
style
.code_actions
.indicator
.in_state(is_active)
.style_for(state)
.color,
)
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.))
@ -3378,10 +3384,8 @@ impl Editor {
.with_color(
style
.indicator
.style_for(
mouse_state,
fold_status == FoldStatus::Folded,
)
.in_state(fold_status == FoldStatus::Folded)
.style_for(mouse_state)
.color,
)
.constrained()

View File

@ -1529,7 +1529,7 @@ impl EditorElement {
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
let style = style.jump_icon.style_for(state);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
@ -2094,7 +2094,7 @@ impl Element<Editor> for EditorElement {
.folds
.ellipses
.background
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
.color;
(id, fold, color)

View File

@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
.status_bar
.panel_buttons
.button
.style_for(state, active);
.in_state(active)
.style_for(state);
Svg::new("icons/feedback_16.svg")
.with_color(style.icon_color)

View File

@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
let theme = theme::current(cx).clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false);
let style = theme.feedback.submit_button.style_for(state);
Label::new("Submit as Markdown", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match, cx, ix);
Flex::column()

View File

@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state, false);
let style = theme.active_language.style_for(state);
Label::new(active_language_text, style.text.clone())
.contained()
.with_style(style.container)

View File

@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let mat = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
let mut label = mat.string.clone();
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {

View File

@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
)
})
.unwrap_or_else(|| "No server selected".into());
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, logs_selected);
.in_state(logs_selected)
.style_for(state);
Label::new(SERVER_LOGS, style.text.clone())
.contained()
.with_style(style.container)
@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, rpc_trace_selected);
.in_state(rpc_trace_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(RPC_MESSAGES, style.text.clone())

View File

@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Flex::row()
.with_child(
Label::new(active_layer.language.name().to_string(), style.text.clone())
@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, is_selected);
.in_state(is_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(layer.language.name().to_string(), style.text.clone())

View File

@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];
let outline_item = &self.outline.items[string_match.candidate_id];

View File

@ -1253,7 +1253,10 @@ impl ProjectPanel {
let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
let mut style = entry_style.style_for(state, details.is_selected).clone();
let mut style = entry_style
.in_state(details.is_selected)
.style_for(state)
.clone();
if cx
.global::<DragAndDrop<Workspace>>()
@ -1264,7 +1267,7 @@ impl ProjectPanel {
.filter(|destination| details.path.starts_with(destination))
.is_some()
{
style = entry_style.active.clone().unwrap();
style = entry_style.active_state().default.clone();
}
let row_container_style = if show_editor {
@ -1405,9 +1408,11 @@ impl View for ProjectPanel {
let button_style = theme.open_project_button.clone();
let context_menu_item_style = theme::current(cx).context_menu.item.clone();
move |state, cx| {
let button_style = button_style.style_for(state, false).clone();
let context_menu_item =
context_menu_item_style.style_for(state, true).clone();
let button_style = button_style.style_for(state).clone();
let context_menu_item = context_menu_item_style
.active_state()
.style_for(state)
.clone();
theme::ui::keystroke_label(
"Open a project",

View File

@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = &theme.picker.item;
let current_style = style.style_for(mouse_state, selected);
let current_style = style.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];
let symbol = &self.symbols[string_match.candidate_id];
@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
.with_child(
// Avoid styling the path differently when it is selected, since
// the symbol's syntax highlighting doesn't change when selected.
Label::new(path.to_string(), style.default.label.clone()),
Label::new(
path.to_string(),
style.inactive_state().default.label.clone(),
),
)
.contained()
.with_style(current_style.container)

View File

@ -173,7 +173,7 @@ impl PickerDelegate for RecentProjectsDelegate {
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];

View File

@ -328,7 +328,11 @@ impl BufferSearchBar {
Some(
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -371,7 +375,7 @@ impl BufferSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false);
let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -403,7 +407,7 @@ impl BufferSearchBar {
enum CloseButton {}
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()

View File

@ -896,7 +896,7 @@ impl ProjectSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false);
let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -927,7 +927,11 @@ impl ProjectSearchBar {
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)

View File

@ -128,12 +128,12 @@ pub struct Titlebar {
pub leader_avatar: AvatarStyle,
pub follower_avatar: AvatarStyle,
pub inactive_avatar_grayscale: bool,
pub sign_in_prompt: Interactive<ContainedText>,
pub sign_in_prompt: Toggleable<Interactive<ContainedText>>,
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub share_button: Toggleable<Interactive<ContainedText>>,
pub call_control: Interactive<IconButton>,
pub toggle_contacts_button: Interactive<IconButton>,
pub user_menu_button: Interactive<IconButton>,
pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
pub user_menu_button: Toggleable<Interactive<IconButton>>,
pub toggle_contacts_badge: ContainerStyle,
}
@ -204,12 +204,12 @@ pub struct ContactList {
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub header_row: Toggleable<Interactive<ContainedText>>,
pub leave_call: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub contact_row: Toggleable<Interactive<ContainerStyle>>,
pub row_height: f32,
pub project_row: Interactive<ProjectRow>,
pub tree_branch: Interactive<TreeBranch>,
pub project_row: Toggleable<Interactive<ProjectRow>>,
pub tree_branch: Toggleable<Interactive<TreeBranch>>,
pub contact_avatar: ImageStyle,
pub contact_status_free: ContainerStyle,
pub contact_status_busy: ContainerStyle,
@ -251,7 +251,7 @@ pub struct DropdownMenu {
pub container: ContainerStyle,
pub header: Interactive<DropdownMenuItem>,
pub section_header: ContainedText,
pub item: Interactive<DropdownMenuItem>,
pub item: Toggleable<Interactive<DropdownMenuItem>>,
pub row_height: f32,
}
@ -270,7 +270,7 @@ pub struct DropdownMenuItem {
pub struct TabBar {
#[serde(flatten)]
pub container: ContainerStyle,
pub pane_button: Interactive<IconButton>,
pub pane_button: Toggleable<Interactive<IconButton>>,
pub pane_button_container: ContainerStyle,
pub active_pane: TabStyles,
pub inactive_pane: TabStyles,
@ -359,7 +359,7 @@ pub struct Search {
pub include_exclude_editor: FindEditor,
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Interactive<ContainedText>,
pub option_button: Toggleable<Interactive<ContainedText>>,
pub match_background: Color,
pub match_index: ContainedText,
pub results_status: TextStyle,
@ -395,7 +395,7 @@ pub struct StatusBarPanelButtons {
pub group_left: ContainerStyle,
pub group_bottom: ContainerStyle,
pub group_right: ContainerStyle,
pub button: Interactive<PanelButton>,
pub button: Toggleable<Interactive<PanelButton>>,
}
#[derive(Deserialize, Default)]
@ -444,10 +444,10 @@ pub struct PanelButton {
pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>,
pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>,
pub cut_entry: Interactive<ProjectPanelEntry>,
pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub filename_editor: FieldEditor,
pub indent_width: f32,
pub open_project_button: Interactive<ContainedText>,
@ -481,7 +481,7 @@ pub struct GitProjectStatus {
pub struct ContextMenu {
#[serde(flatten)]
pub container: ContainerStyle,
pub item: Interactive<ContextMenuItem>,
pub item: Toggleable<Interactive<ContextMenuItem>>,
pub keystroke_margin: f32,
pub separator: ContainerStyle,
}
@ -498,7 +498,7 @@ pub struct ContextMenuItem {
#[derive(Debug, Deserialize, Default)]
pub struct CommandPalette {
pub key: Interactive<ContainedLabel>,
pub key: Toggleable<ContainedLabel>,
pub keystroke_spacing: f32,
}
@ -565,7 +565,7 @@ pub struct Picker {
pub input_editor: FieldEditor,
pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel,
pub item: Interactive<ContainedLabel>,
pub item: Toggleable<Interactive<ContainedLabel>>,
}
#[derive(Clone, Debug, Deserialize, Default)]
@ -771,13 +771,13 @@ pub struct InteractiveColor {
#[derive(Clone, Deserialize, Default)]
pub struct CodeActions {
#[serde(default)]
pub indicator: Interactive<InteractiveColor>,
pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub vertical_scale: f32,
}
#[derive(Clone, Deserialize, Default)]
pub struct Folds {
pub indicator: Interactive<InteractiveColor>,
pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub ellipses: FoldEllipses,
pub fold_background: Color,
pub icon_margin_scale: f32,
@ -805,38 +805,46 @@ pub struct DiffStyle {
#[derive(Debug, Default, Clone, Copy)]
pub struct Interactive<T> {
pub default: T,
pub hover: Option<T>,
pub hover_and_active: Option<T>,
pub hovered: Option<T>,
pub clicked: Option<T>,
pub click_and_active: Option<T>,
pub active: Option<T>,
pub disabled: Option<T>,
}
impl<T> Interactive<T> {
pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct Toggleable<T> {
active: T,
inactive: T,
}
impl<T> Toggleable<T> {
pub fn new(active: T, inactive: T) -> Self {
Self { active, inactive }
}
pub fn in_state(&self, active: bool) -> &T {
if active {
if state.hovered() {
self.hover_and_active
.as_ref()
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
{
self.click_and_active
.as_ref()
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
} else {
self.active.as_ref().unwrap_or(&self.default)
}
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
&self.active
} else {
&self.inactive
}
}
pub fn active_state(&self) -> &T {
self.in_state(true)
}
pub fn inactive_state(&self) -> &T {
self.in_state(false)
}
}
impl<T> Interactive<T> {
pub fn style_for(&self, state: &mut MouseState) -> &T {
if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
self.clicked.as_ref().unwrap()
} else if state.hovered() {
self.hover.as_ref().unwrap_or(&self.default)
self.hovered.as_ref().unwrap_or(&self.default)
} else {
&self.default
}
}
pub fn disabled_style(&self) -> &T {
self.disabled.as_ref().unwrap_or(&self.default)
}
@ -849,13 +857,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
{
#[derive(Deserialize)]
struct Helper {
#[serde(flatten)]
default: Value,
hover: Option<Value>,
hover_and_active: Option<Value>,
hovered: Option<Value>,
clicked: Option<Value>,
click_and_active: Option<Value>,
active: Option<Value>,
disabled: Option<Value>,
}
@ -880,21 +884,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
}
};
let hover = deserialize_state(json.hover)?;
let hover_and_active = deserialize_state(json.hover_and_active)?;
let hovered = deserialize_state(json.hovered)?;
let clicked = deserialize_state(json.clicked)?;
let click_and_active = deserialize_state(json.click_and_active)?;
let active = deserialize_state(json.active)?;
let disabled = deserialize_state(json.disabled)?;
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
Ok(Interactive {
default,
hover,
hover_and_active,
hovered,
clicked,
click_and_active,
active,
disabled,
})
}

View File

@ -170,7 +170,7 @@ where
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
let style = style.style_for(state);
Label::new(label, style.text.to_owned())
.aligned()
.contained()
@ -220,13 +220,13 @@ where
title,
style
.title_text
.style_for(&mut MouseState::default(), false)
.style_for(&mut MouseState::default())
.clone(),
))
.with_child(
// FIXME: Get a better tag type
MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
let style = style.close_icon.style_for(state, false);
let style = style.close_icon.style_for(state);
icon(style)
})
.on_click(platform::MouseButton::Left, move |_, _, cx| {

View File

@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let theme_match = &self.matches[ix];
Label::new(theme_match.string.clone(), style.label.clone())

View File

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

View File

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

View File

@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
) -> gpui::AnyElement<Picker<Self>> {
let theme = &theme::current(cx);
let keymap_match = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Label::new(keymap_match.string.clone(), style.label.clone())
.with_highlights(keymap_match.positions.clone())

View File

@ -498,7 +498,9 @@ impl View for PanelButtons {
Stack::new()
.with_child(
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
let style = button_style.style_for(state, is_active);
let style = button_style.in_state(is_active);
let style = style.style_for(state);
Flex::row()
.with_child(
Svg::new(view.icon_path(cx))

View File

@ -291,7 +291,7 @@ pub mod simple_message_notification {
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -323,7 +323,7 @@ pub mod simple_message_notification {
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state, false);
let style = theme.action_message.style_for(state);
Flex::row()
.with_child(

View File

@ -1410,7 +1410,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize,
icon: &'static str,
active: bool,
is_active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>,
on_click: F,
@ -1420,7 +1420,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, active);
let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
Svg::new(icon)
.with_color(style.color)
.constrained()

View File

@ -231,7 +231,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
) -> AnyElement<Toolbar> {
MouseEventHandler::<A, _>::new(0, cx, |state, _| {
let style = if enabled {
style.style_for(state, false)
style.style_for(state)
} else {
style.disabled_style()
};

View File

@ -62,7 +62,6 @@ text = { path = "../text" }
terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
theme_testbench = { path = "../theme_testbench" }
util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }

View File

@ -154,7 +154,6 @@ fn main() {
search::init(cx);
vim::init(cx);
terminal_view::init(cx);
theme_testbench::init(cx);
copilot::init(http.clone(), node_runtime, cx);
ai::init(cx);

1
styles/.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/
coverage/

20
styles/.zed/settings.json Normal file
View File

@ -0,0 +1,20 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
{
"languages": {
"TypeScript": {
"tab_size": 4
},
"TSX": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 4
},
"JSON": {
"tab_size": 4
}
}
}

2334
styles/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,8 @@
"scripts": {
"build": "ts-node ./src/buildThemes.ts",
"build-licenses": "ts-node ./src/buildLicenses.ts",
"build-tokens": "ts-node ./src/buildTokens.ts"
"build-tokens": "ts-node ./src/buildTokens.ts",
"test": "vitest"
},
"author": "",
"license": "ISC",
@ -20,12 +21,18 @@
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
"toml": "^3.0.0",
"ts-node": "^10.9.1"
"ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
"utility-types": "^3.10.0",
"vitest": "^0.32.0"
},
"prettier": {
"semi": false,
"printWidth": 80,
"htmlWhitespaceSensitivity": "strict",
"tabWidth": 4
},
"devDependencies": {
"@vitest/coverage-v8": "^0.32.0"
}
}

View File

@ -1,13 +1,13 @@
import * as fs from "fs";
import * as path from "path";
import { ColorScheme, createColorScheme } from "./common";
import { themes } from "./themes";
import { slugify } from "./utils/slugify";
import { colorSchemeTokens } from "./theme/tokens/colorScheme";
import * as fs from "fs"
import * as path from "path"
import { ColorScheme, createColorScheme } from "./common"
import { themes } from "./themes"
import { slugify } from "./utils/slugify"
import { colorSchemeTokens } from "./theme/tokens/colorScheme"
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens");
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json");
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json");
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
function clearTokens(tokensDirectory: string) {
if (!fs.existsSync(tokensDirectory)) {
@ -22,64 +22,66 @@ function clearTokens(tokensDirectory: string) {
}
type TokenSet = {
id: string;
name: string;
selectedTokenSets: { [key: string]: "enabled" };
};
id: string
name: string
selectedTokenSets: { [key: string]: "enabled" }
}
function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } {
const tokenSetOrder: string[] = colorSchemes.map(
(scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_")
);
return { tokenSetOrder };
function buildTokenSetOrder(colorSchemes: ColorScheme[]): {
tokenSetOrder: string[]
} {
const tokenSetOrder: string[] = colorSchemes.map((scheme) =>
scheme.name.toLowerCase().replace(/\s+/g, "_")
)
return { tokenSetOrder }
}
function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
.toLowerCase()
.replace(/\s+/g, "_")}_${index}`;
const selectedTokenSets: { [key: string]: "enabled" } = {};
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_");
selectedTokenSets[tokenSet] = "enabled";
.replace(/\s+/g, "_")}_${index}`
const selectedTokenSets: { [key: string]: "enabled" } = {}
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_")
selectedTokenSets[tokenSet] = "enabled"
return {
id,
name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
selectedTokenSets,
};
});
}
})
return themesIndex;
return themesIndex
}
function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
clearTokens(tokensDirectory);
clearTokens(tokensDirectory)
for (const colorScheme of colorSchemes) {
const fileName = slugify(colorScheme.name) + ".json";
const tokens = colorSchemeTokens(colorScheme);
const tokensJSON = JSON.stringify(tokens, null, 2);
const outPath = path.join(tokensDirectory, fileName);
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 });
console.log(`- ${outPath} created`);
const fileName = slugify(colorScheme.name) + ".json"
const tokens = colorSchemeTokens(colorScheme)
const tokensJSON = JSON.stringify(tokens, null, 2)
const outPath = path.join(tokensDirectory, fileName)
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 })
console.log(`- ${outPath} created`)
}
const themeIndexData = buildThemesIndex(colorSchemes);
const themeIndexData = buildThemesIndex(colorSchemes)
const themesJSON = JSON.stringify(themeIndexData, null, 2);
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 });
console.log(`- ${TOKENS_FILE} created`);
const themesJSON = JSON.stringify(themeIndexData, null, 2)
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 })
console.log(`- ${TOKENS_FILE} created`)
const tokenSetOrderData = buildTokenSetOrder(colorSchemes);
const tokenSetOrderData = buildTokenSetOrder(colorSchemes)
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2);
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 });
console.log(`- ${METADATA_FILE} created`);
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2)
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 })
console.log(`- ${METADATA_FILE} created`)
}
const colorSchemes: ColorScheme[] = themes.map((theme) =>
createColorScheme(theme)
);
)
writeTokens(colorSchemes, TOKENS_DIRECTORY);
writeTokens(colorSchemes, TOKENS_DIRECTORY)

View File

@ -0,0 +1,4 @@
import { interactive } from "./interactive"
import { toggleable } from "./toggle"
export { interactive, toggleable }

View File

@ -0,0 +1,56 @@
import {
NOT_ENOUGH_STATES_ERROR,
NO_DEFAULT_OR_BASE_ERROR,
interactive,
} from "./interactive"
import { describe, it, expect } from "vitest"
describe("interactive", () => {
it("creates an Interactive<Element> with base properties and states", () => {
const result = interactive({
base: { fontSize: 10, color: "#FFFFFF" },
state: {
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("creates an Interactive<Element> with no base properties", () => {
const result = interactive({
state: {
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("throws error when both default and base are missing", () => {
const state = {
hovered: { color: "blue" },
}
expect(() => interactive({ state })).toThrow(NO_DEFAULT_OR_BASE_ERROR)
})
it("throws error when no other state besides default is present", () => {
const state = {
default: { fontSize: 10 },
}
expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
})
})

View File

@ -0,0 +1,97 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type InteractiveState =
| "default"
| "hovered"
| "clicked"
| "selected"
| "disabled"
type Interactive<T> = {
default: T
hovered?: T
clicked?: T
selected?: T
disabled?: T
}
export const NO_DEFAULT_OR_BASE_ERROR =
"An interactive object must have a default state, or a base property."
export const NOT_ENOUGH_STATES_ERROR =
"An interactive object must have a default and at least one other state."
interface InteractiveProps<T> {
base?: T
state: Partial<Record<InteractiveState, DeepPartial<T>>>
}
/**
* Helper function for creating Interactive<T> objects that works with Toggle<T>-like behavior.
* It takes a default object to be used as the value for `default` field and fills out other fields
* with fields from either `base` or from the `state` object which contains values for specific states.
* Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them.
*
* @param defaultObj Object to be used as the value for the `default` field.
* @param base Optional object containing base fields to be included in the resulting object.
* @param state Object containing optional modified fields to be included in the resulting object for each state.
* @returns Interactive<T> object with fields from `base` and `state`.
*/
export function interactive<T extends Object>({
base,
state,
}: InteractiveProps<T>): Interactive<T> {
if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
let defaultState: T
if (state.default && base) {
defaultState = merge(base, state.default) as T
} else {
defaultState = base ? base : (state.default as T)
}
let interactiveObj: Interactive<T> = {
default: defaultState,
}
let stateCount = 0
if (state.hovered !== undefined) {
interactiveObj.hovered = merge(
interactiveObj.default,
state.hovered
) as T
stateCount++
}
if (state.clicked !== undefined) {
interactiveObj.clicked = merge(
interactiveObj.default,
state.clicked
) as T
stateCount++
}
if (state.selected !== undefined) {
interactiveObj.selected = merge(
interactiveObj.default,
state.selected
) as T
stateCount++
}
if (state.disabled !== undefined) {
interactiveObj.disabled = merge(
interactiveObj.default,
state.disabled
) as T
stateCount++
}
if (stateCount < 1) {
throw new Error(NOT_ENOUGH_STATES_ERROR)
}
return interactiveObj
}

View File

@ -0,0 +1,52 @@
import {
NO_ACTIVE_ERROR,
NO_INACTIVE_OR_BASE_ERROR,
toggleable,
} from "./toggle"
import { describe, it, expect } from "vitest"
describe("toggleable", () => {
it("creates a Toggleable<Element> with base properties and states", () => {
const result = toggleable({
base: { background: "#000000", color: "#CCCCCC" },
state: {
active: { color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("creates a Toggleable<Element> with no base properties", () => {
const result = toggleable({
state: {
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("throws error when both inactive and base are missing", () => {
const state = {
active: { background: "#000000", color: "#FFFFFF" },
}
expect(() => toggleable({ state })).toThrow(NO_INACTIVE_OR_BASE_ERROR)
})
it("throws error when no active state is present", () => {
const state = {
inactive: { background: "#000000", color: "#CCCCCC" },
}
expect(() => toggleable({ state })).toThrow(NO_ACTIVE_ERROR)
})
})

View File

@ -0,0 +1,47 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type ToggleState = "inactive" | "active"
type Toggleable<T> = Record<ToggleState, T>
export const NO_INACTIVE_OR_BASE_ERROR =
"A toggleable object must have an inactive state, or a base property."
export const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
interface ToggleableProps<T> {
base?: T
state: Partial<Record<ToggleState, DeepPartial<T>>>
}
/**
* Helper function for creating Toggleable objects.
* @template T The type of the object being toggled.
* @param props Object containing the base (inactive) state and state modifications to create the active state.
* @returns A Toggleable object containing both the inactive and active states.
* @example
* ```
* toggleable({
* base: { background: "#000000", text: "#CCCCCC" },
* state: { active: { text: "#CCCCCC" } },
* })
* ```
*/
export function toggleable<T extends object>(
props: ToggleableProps<T>
): Toggleable<T> {
const { base, state } = props
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
const inactiveState = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
const toggleObj: Toggleable<T> = {
inactive: inactiveState,
active: merge(base ?? {}, state.active) as T,
}
return toggleObj
}

View File

@ -1,4 +1,3 @@
import { text } from "./components"
import contactFinder from "./contactFinder"
import contactsPopover from "./contactsPopover"
import commandPalette from "./commandPalette"

View File

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { text, border, background, foreground } from "./components"
import editor from "./editor"
import { interactive } from "../element"
export default function assistant(colorScheme: ColorScheme) {
const layer = colorScheme.highest
@ -15,13 +16,28 @@ export default function assistant(colorScheme: ColorScheme) {
background: editor(colorScheme).background,
},
userSender: {
...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
assistantSender: {
...text(layer, "sans", "accent", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "accent", {
size: "sm",
weight: "bold",
}),
},
},
systemSender: {
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "variant", {
size: "sm",
weight: "bold",
}),
},
},
sentAt: {
margin: { top: 2, left: 8 },
@ -30,16 +46,20 @@ export default function assistant(colorScheme: ColorScheme) {
modelInfoContainer: {
margin: { right: 16, top: 4 },
},
model: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
padding: 4,
cornerRadius: 4,
...text(layer, "sans", "default", { size: "xs" }),
hover: {
background: background(layer, "on", "hovered"),
model: interactive({
base: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
padding: 4,
cornerRadius: 4,
...text(layer, "sans", "default", { size: "xs" }),
},
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
},
},
}),
remainingTokens: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),

View File

@ -1,12 +1,13 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { text, background } from "./components"
import { toggleable } from "../element"
export default function commandPalette(colorScheme: ColorScheme) {
let layer = colorScheme.highest
return {
keystrokeSpacing: 8,
key: {
const key = toggleable({
base: {
text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2,
background: background(layer, "on"),
@ -21,10 +22,21 @@ export default function commandPalette(colorScheme: ColorScheme) {
bottom: 1,
left: 2,
},
},
state: {
active: {
text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2),
},
},
})
return {
keystrokeSpacing: 8,
// TODO: This should be a Toggle<ContainedText> on the rust side so we don't have to do this
key: {
inactive: { ...key.inactive },
active: key.active,
},
}
}

View File

@ -85,7 +85,7 @@ export function foreground(
return getStyle(layer, styleSetOrStyles, style).foreground
}
interface Text {
interface Text extends Object {
family: keyof typeof fontFamilies
color: string
size: number

View File

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, borderColor, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function contactsPanel(colorScheme: ColorScheme) {
const nameMargin = 8
const sidePadding = 12
@ -71,47 +71,85 @@ export default function contactsPanel(colorScheme: ColorScheme) {
},
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
left: sidePadding,
right: sidePadding,
headerRow: toggleable({
base: interactive({
base: {
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
left: sidePadding,
right: sidePadding,
},
background: background(layer, "default"), // posiewic: breaking change
},
state: {
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
}, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place.
}),
state: {
active: {
default: {
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
},
},
active: {
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
}),
leaveCall: interactive({
base: {
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" }),
},
},
leaveCall: {
background: background(layer),
border: border(layer),
cornerRadius: 6,
margin: {
top: 1,
state: {
hovered: {
...text(layer, "sans", "hovered", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "hovered"),
},
},
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,
inactive: {
default: {
padding: {
left: sidePadding,
right: sidePadding,
},
},
},
active: {
background: background(layer, "active"),
default: {
background: background(layer, "active"),
padding: {
left: sidePadding,
right: sidePadding,
},
},
},
},
contactAvatar: {
cornerRadius: 10,
width: 18,
@ -135,12 +173,14 @@ export default function contactsPanel(colorScheme: ColorScheme) {
},
},
contactButtonSpacing: nameMargin,
contactButton: {
...contactButton,
hover: {
background: background(layer, "hovered"),
contactButton: interactive({
base: { ...contactButton },
state: {
hovered: {
background: background(layer, "hovered"),
},
},
},
}),
disabledButton: {
...contactButton,
background: background(layer, "on"),
@ -149,34 +189,52 @@ export default function contactsPanel(colorScheme: ColorScheme) {
callingIndicator: {
...text(layer, "mono", "variant", { size: "xs" }),
},
treeBranch: {
color: borderColor(layer),
width: 1,
hover: {
color: borderColor(layer),
treeBranch: toggleable({
base: interactive({
base: {
color: borderColor(layer),
width: 1,
},
state: {
hovered: {
color: borderColor(layer),
},
},
}),
state: {
active: {
default: {
color: borderColor(layer),
},
},
},
active: {
color: borderColor(layer),
}),
projectRow: toggleable({
base: interactive({
base: {
...projectRow,
background: background(layer),
icon: {
margin: { left: nameMargin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
...projectRow.name,
...text(layer, "mono", { size: "sm" }),
},
},
state: {
hovered: {
background: background(layer, "hovered"),
},
},
}),
state: {
active: {
default: { background: background(layer, "active") },
},
},
},
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"),
},
},
}),
}
}

View File

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, foreground, text } from "./components"
import { interactive } from "../element"
const avatarSize = 12
const headerPadding = 8
@ -21,24 +21,32 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
...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"),
button: interactive({
base: {
...text(layer, "sans", "on", { size: "xs" }),
background: background(layer, "on"),
padding: 4,
cornerRadius: 6,
margin: { left: 6 },
},
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
},
},
}),
dismissButton: {
color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
default: {
color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
},
},
},
}

View File

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, borderColor, text } from "./components"
import { interactive, toggleable } from "../element"
export default function contextMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle
@ -10,37 +11,54 @@ export default function contextMenu(colorScheme: ColorScheme) {
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" }),
keystroke: {
...text(layer, "sans", "hovered", {
size: "sm",
weight: "bold",
}),
padding: { left: 3, right: 3 },
item: toggleable({
base: interactive({
base: {
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 },
},
},
state: {
hovered: {
background: background(layer, "hovered"),
label: text(layer, "sans", "hovered", { size: "sm" }),
keystroke: {
...text(layer, "sans", "hovered", {
size: "sm",
weight: "bold",
}),
padding: { left: 3, right: 3 },
},
},
clicked: {
background: background(layer, "pressed"),
},
},
}),
state: {
active: {
default: {
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
},
},
active: {
background: background(layer, "active"),
},
activeHover: {
background: background(layer, "active"),
},
},
}),
separator: {
background: borderColor(layer),
margin: { top: 2, bottom: 2 },

View File

@ -1,60 +1,69 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, svg, text } from "./components"
import { interactive } from "../element"
export default function copilot(colorScheme: ColorScheme) {
let layer = colorScheme.middle
let content_width = 264
let ctaButton = {
let ctaButton =
// Copied from welcome screen. FIXME: Move this into a ZDS component
background: background(layer),
border: border(layer, "default"),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
left: 8,
right: 8,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
hover: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
}
interactive({
base: {
background: background(layer),
border: border(layer, "default"),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
left: 8,
right: 8,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
},
state: {
hovered: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
},
})
return {
outLinkIcon: {
icon: svg(
foreground(layer, "variant"),
"icons/link_out_12.svg",
12,
12
),
container: {
cornerRadius: 6,
padding: { left: 6 },
},
hover: {
outLinkIcon: interactive({
base: {
icon: svg(
foreground(layer, "hovered"),
foreground(layer, "variant"),
"icons/link_out_12.svg",
12,
12
),
container: {
cornerRadius: 6,
padding: { left: 6 },
},
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered"),
},
},
},
}),
modal: {
titleText: {
...text(layer, "sans", { size: "xs", weight: "bold" }),
default: {
...text(layer, "sans", { size: "xs", weight: "bold" }),
},
},
titlebar: {
background: background(colorScheme.lowest),
@ -75,42 +84,46 @@ export default function copilot(colorScheme: ColorScheme) {
bottom: 8,
},
},
closeIcon: {
icon: svg(
foreground(layer, "variant"),
"icons/x_mark_8.svg",
8,
8
),
container: {
cornerRadius: 2,
padding: {
top: 4,
bottom: 4,
left: 4,
right: 4,
},
margin: {
right: 0,
},
},
hover: {
closeIcon: interactive({
base: {
icon: svg(
foreground(layer, "on"),
foreground(layer, "variant"),
"icons/x_mark_8.svg",
8,
8
),
container: {
cornerRadius: 2,
padding: {
top: 4,
bottom: 4,
left: 4,
right: 4,
},
margin: {
right: 0,
},
},
},
clicked: {
icon: svg(
foreground(layer, "base"),
"icons/x_mark_8.svg",
8,
8
),
state: {
hovered: {
icon: svg(
foreground(layer, "on"),
"icons/x_mark_8.svg",
8,
8
),
},
clicked: {
icon: svg(
foreground(layer, "base"),
"icons/x_mark_8.svg",
8,
8
),
},
},
},
}),
dimensions: {
width: 280,
height: 280,
@ -185,28 +198,32 @@ export default function copilot(colorScheme: ColorScheme) {
},
},
right: (content_width * 1) / 3,
rightContainer: {
border: border(colorScheme.lowest, "inverted", {
bottom: false,
right: false,
top: false,
left: true,
}),
padding: {
top: 3,
bottom: 5,
left: 8,
right: 0,
},
hover: {
border: border(layer, "active", {
rightContainer: interactive({
base: {
border: border(colorScheme.lowest, "inverted", {
bottom: false,
right: false,
top: false,
left: true,
}),
padding: {
top: 3,
bottom: 5,
left: 8,
right: 0,
},
},
},
state: {
hovered: {
border: border(layer, "active", {
bottom: false,
right: false,
top: false,
left: true,
}),
},
},
}),
},
},

View File

@ -4,6 +4,7 @@ import { background, border, borderColor, foreground, text } from "./components"
import hoverPopover from "./hoverPopover"
import { buildSyntax } from "../theme/syntax"
import { interactive, toggleable } from "../element"
export default function editor(colorScheme: ColorScheme) {
const { isLight } = colorScheme
@ -48,46 +49,76 @@ export default function editor(colorScheme: ColorScheme) {
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: syntax.predictive,
codeActions: {
indicator: {
color: foreground(layer, "variant"),
indicator: toggleable({
base: interactive({
base: {
color: foreground(layer, "variant"),
},
state: {
hovered: {
color: foreground(layer, "variant", "hovered"),
},
clicked: {
color: foreground(layer, "variant", "pressed"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "accent"),
},
hovered: {
color: foreground(layer, "accent", "hovered"),
},
clicked: {
color: foreground(layer, "accent", "pressed"),
},
},
},
}),
clicked: {
color: foreground(layer, "base"),
},
hover: {
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
verticalScale: 0.55,
},
folds: {
iconMarginScale: 2.5,
foldedIcon: "icons/chevron_right_8.svg",
foldableIcon: "icons/chevron_down_8.svg",
indicator: {
color: foreground(layer, "variant"),
clicked: {
color: foreground(layer, "base"),
indicator: toggleable({
base: interactive({
base: {
color: foreground(layer, "variant"),
},
state: {
hovered: {
color: foreground(layer, "on"),
},
clicked: {
color: foreground(layer, "base"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "default"),
},
hovered: {
color: foreground(layer, "variant"),
},
},
},
hover: {
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
}),
ellipses: {
textColor: colorScheme.ramps.neutral(0.71).hex(),
cornerRadiusFactor: 0.15,
background: {
// Copied from hover_popover highlight
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
default: {
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
},
hover: {
hovered: {
color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
},
@ -223,21 +254,26 @@ export default function editor(colorScheme: ColorScheme) {
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,
jumpIcon: interactive({
base: {
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"),
state: {
hovered: {
background: background(layer, "on", "hovered"),
},
},
},
}),
scrollbar: {
width: 12,
minHeightFactor: 1.0,

View File

@ -1,35 +1,40 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, text } from "./components"
import { interactive } from "../element"
export default function feedback(colorScheme: ColorScheme) {
let layer = colorScheme.highest
return {
submit_button: {
...text(layer, "mono", "on"),
background: background(layer, "on"),
cornerRadius: 6,
border: border(layer, "on"),
margin: {
right: 4,
submit_button: interactive({
base: {
...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,
},
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
state: {
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hovered: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
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", {

View File

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
export default function picker(colorScheme: ColorScheme): any {
let layer = colorScheme.lowest
@ -38,35 +39,65 @@ export default function picker(colorScheme: ColorScheme): any {
...container,
padding: {},
},
item: {
padding: {
bottom: 4,
left: 12,
right: 12,
top: 4,
item: toggleable({
base: interactive({
base: {
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",
}),
},
state: {
hovered: {
background: withOpacity(
background(layer, "hovered"),
0.5
),
},
clicked: {
background: withOpacity(
background(layer, "pressed"),
0.5
),
},
},
}),
state: {
active: {
default: {
background: withOpacity(
background(layer, "base", "active"),
0.5
),
},
hovered: {
background: withOpacity(
background(layer, "hovered"),
0.5
),
},
clicked: {
background: withOpacity(
background(layer, "pressed"),
0.5
),
},
},
},
margin: {
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
active: {
background: withOpacity(
background(layer, "base", "active"),
0.5
),
text: text(layer, "sans", "base", "active"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
hover: {
background: withOpacity(background(layer, "hovered"), 0.5),
},
},
}),
inputEditor,
emptyInputEditor,
noMatches: {

View File

@ -1,7 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function projectPanel(colorScheme: ColorScheme) {
const { isLight } = colorScheme
@ -28,48 +28,79 @@ export default function projectPanel(colorScheme: ColorScheme) {
},
}
let entry = {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
hover: {
background: background(layer, "variant", "hovered"),
const default_entry = interactive({
base: {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
status,
},
active: {
background: colorScheme.isLight
? withOpacity(background(layer, "active"), 0.5)
: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }),
state: {
default: {
background: background(layer),
},
hovered: {
background: background(layer, "variant", "hovered"),
},
clicked: {
background: background(layer, "variant", "pressed"),
},
},
activeHover: {
background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }),
})
let entry = toggleable({
base: default_entry,
state: {
active: interactive({
base: {
...default_entry,
},
state: {
default: {
background: background(colorScheme.lowest),
},
hovered: {
background: background(colorScheme.lowest, "hovered"),
},
clicked: {
background: background(colorScheme.lowest, "pressed"),
},
},
}),
},
status,
}
})
return {
openProjectButton: {
background: background(layer),
border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 16,
left: 16,
right: 16,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
hover: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
openProjectButton: interactive({
base: {
background: background(layer),
border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 16,
left: 16,
right: 16,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
},
},
state: {
hovered: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
clicked: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "pressed"),
border: border(layer, "active"),
},
},
}),
background: background(layer),
padding: { left: 6, right: 6, top: 0, bottom: 6 },
indentWidth: 12,
@ -94,8 +125,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
...entry,
text: text(layer, "mono", "disabled"),
active: {
background: background(layer, "active"),
text: text(layer, "mono", "disabled", { size: "sm" }),
...entry.active,
default: {
...entry.active.default,
background: background(layer, "active"),
text: text(layer, "mono", "disabled", { size: "sm" }),
},
},
},
filenameEditor: {

View File

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function search(colorScheme: ColorScheme) {
let layer = colorScheme.highest
@ -35,36 +36,50 @@ export default function search(colorScheme: ColorScheme) {
return {
// TODO: Add an activeMatchBackground on the rust side to differentiate 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,
optionButton: toggleable({
base: interactive({
base: {
...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,
},
},
state: {
hovered: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
},
}),
state: {
active: {
default: {
...text(layer, "mono", "accent"),
},
hovered: {
...text(layer, "mono", "accent", "hovered"),
},
clicked: {
...text(layer, "mono", "accent", "pressed"),
},
},
},
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,
@ -97,17 +112,24 @@ export default function search(colorScheme: ColorScheme) {
...text(layer, "mono", "on"),
size: 18,
},
dismissButton: {
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: 14,
padding: {
left: 10,
right: 10,
dismissButton: interactive({
base: {
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: 14,
padding: {
left: 10,
right: 10,
},
},
hover: {
color: foreground(layer, "hovered"),
state: {
hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
},
},
}),
}
}

View File

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, text } from "./components"
import { interactive } from "../element"
const headerPadding = 8
@ -12,33 +13,41 @@ export default function simpleMessageNotification(
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
actionMessage: {
...text(layer, "sans", { size: "xs" }),
border: border(layer, "active"),
cornerRadius: 4,
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
margin: { left: headerPadding, top: 6, bottom: 6 },
hover: {
...text(layer, "sans", "default", { size: "xs" }),
background: background(layer, "hovered"),
actionMessage: interactive({
base: {
...text(layer, "sans", { size: "xs" }),
border: border(layer, "active"),
cornerRadius: 4,
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
margin: { left: headerPadding, top: 6, bottom: 6 },
},
},
dismissButton: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
state: {
hovered: {
...text(layer, "sans", "default", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
},
},
}),
dismissButton: interactive({
base: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
},
}),
}
}

View File

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function statusBar(colorScheme: ColorScheme) {
let layer = colorScheme.lowest
@ -25,95 +25,123 @@ export default function statusBar(colorScheme: ColorScheme) {
},
border: border(layer, { top: true, overlay: true }),
cursorPosition: text(layer, "sans", "variant"),
activeLanguage: {
padding: { left: 6, right: 6 },
...text(layer, "sans", "variant"),
hover: {
...text(layer, "sans", "on"),
activeLanguage: interactive({
base: {
padding: { left: 6, right: 6 },
...text(layer, "sans", "variant"),
},
},
state: {
hovered: {
...text(layer, "sans", "on"),
},
},
}),
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: {
lspStatus: interactive({
base: {
...diagnosticStatusContainer,
iconSpacing: 4,
iconWidth: 14,
height: 18,
message: text(layer, "sans"),
iconColor: foreground(layer),
background: background(layer, "hovered"),
},
},
diagnosticMessage: {
...text(layer, "sans"),
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: {
cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 },
state: {
hovered: {
message: text(layer, "sans"),
iconColor: foreground(layer),
background: background(layer, "hovered"),
},
},
containerWarning: {
...diagnosticStatusContainer,
background: background(layer, "warning"),
border: border(layer, "warning"),
}),
diagnosticMessage: interactive({
base: {
...text(layer, "sans"),
},
containerError: {
...diagnosticStatusContainer,
background: background(layer, "negative"),
border: border(layer, "negative"),
},
hover: {
iconColorOk: foreground(layer, "on"),
state: { hovered: text(layer, "sans", "hovered") },
}),
diagnosticSummary: interactive({
base: {
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 },
background: background(layer, "on", "hovered"),
},
containerWarning: {
...diagnosticStatusContainer,
background: background(layer, "warning", "hovered"),
border: border(layer, "warning", "hovered"),
background: background(layer, "warning"),
border: border(layer, "warning"),
},
containerError: {
...diagnosticStatusContainer,
background: background(layer, "negative", "hovered"),
border: border(layer, "negative", "hovered"),
background: background(layer, "negative"),
border: border(layer, "negative"),
},
},
},
state: {
hovered: {
iconColorOk: foreground(layer, "on"),
containerOk: {
background: background(layer, "on", "hovered"),
},
containerWarning: {
background: background(layer, "warning", "hovered"),
border: border(layer, "warning", "hovered"),
},
containerError: {
background: background(layer, "negative", "hovered"),
border: border(layer, "negative", "hovered"),
},
},
},
}),
panelButtons: {
groupLeft: {},
groupBottom: {},
groupRight: {},
button: {
...statusContainer,
iconSize: 16,
iconColor: foreground(layer, "variant"),
label: {
margin: { left: 6 },
...text(layer, "sans", { size: "sm" }),
button: toggleable({
base: interactive({
base: {
...statusContainer,
iconSize: 16,
iconColor: foreground(layer, "variant"),
label: {
margin: { left: 6 },
...text(layer, "sans", { size: "sm" }),
},
},
state: {
hovered: {
iconColor: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
},
}),
state: {
active: {
default: {
iconColor: foreground(layer, "active"),
background: background(layer, "active"),
},
hovered: {
iconColor: foreground(layer, "hovered"),
background: background(layer, "hovered"),
},
clicked: {
iconColor: foreground(layer, "pressed"),
background: background(layer, "pressed"),
},
},
},
hover: {
iconColor: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
active: {
iconColor: foreground(layer, "active"),
background: background(layer, "active"),
},
},
}),
badge: {
cornerRadius: 3,
padding: 2,

View File

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { text, border, background, foreground } from "./components"
import { interactive, toggleable } from "../element"
export default function tabBar(colorScheme: ColorScheme) {
const height = 32
@ -87,17 +88,36 @@ export default function tabBar(colorScheme: ColorScheme) {
inactiveTab: inactivePaneInactiveTab,
},
draggedTab,
paneButton: {
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: activePaneActiveTab.height,
hover: {
color: foreground(layer, "hovered"),
paneButton: toggleable({
base: interactive({
base: {
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: activePaneActiveTab.height,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "accent"),
},
hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
},
},
active: {
color: foreground(layer, "accent"),
},
},
}),
paneButtonContainer: {
background: tab.background,
border: {

View File

@ -0,0 +1,47 @@
import merge from "ts-deepmerge"
type ToggleState = "inactive" | "active"
type Toggleable<T> = Record<ToggleState, T>
const NO_INACTIVE_OR_BASE_ERROR =
"A toggleable object must have an inactive state, or a base property."
const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
interface ToggleableProps<T> {
base?: T
state: Partial<Record<ToggleState, T>>
}
/**
* Helper function for creating Toggleable objects.
* @template T The type of the object being toggled.
* @param props Object containing the base (inactive) state and state modifications to create the active state.
* @returns A Toggleable object containing both the inactive and active states.
* @example
* ```
* toggleable({
* base: { background: "#000000", text: "#CCCCCC" },
* state: { active: { text: "#CCCCCC" } },
* })
* ```
*/
export function toggleable<T extends object>(
props: ToggleableProps<T>
): Toggleable<T> {
const { base, state } = props
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
const inactiveState = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
const toggleObj: Toggleable<T> = {
inactive: inactiveState,
active: merge(base ?? {}, state.active) as T,
}
return toggleObj
}

View File

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
export default function dropdownMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle
@ -9,38 +9,56 @@ export default function dropdownMenu(colorScheme: ColorScheme) {
background: background(layer),
border: border(layer),
shadow: colorScheme.popoverShadow,
header: {
...text(layer, "sans", { size: "sm" }),
secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }),
secondaryTextSpacing: 10,
padding: { left: 8, right: 8, top: 2, bottom: 2 },
cornerRadius: 6,
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
hover: {
background: background(layer, "hovered"),
...text(layer, "sans", "hovered", { size: "sm" }),
}
},
header: interactive({
base: {
...text(layer, "sans", { size: "sm" }),
secondaryText: text(layer, "sans", {
size: "sm",
color: "#aaaaaa",
}),
secondaryTextSpacing: 10,
padding: { left: 8, right: 8, top: 2, bottom: 2 },
cornerRadius: 6,
background: background(layer, "on"),
},
state: {
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
},
}),
sectionHeader: {
...text(layer, "sans", { size: "sm" }),
padding: { left: 8, right: 8, top: 8, bottom: 8 },
},
item: {
...text(layer, "sans", { size: "sm" }),
secondaryTextSpacing: 10,
secondaryText: text(layer, "sans", { size: "sm" }),
padding: { left: 18, right: 18, top: 2, bottom: 2 },
hover: {
background: background(layer, "hovered"),
...text(layer, "sans", "hovered", { size: "sm" }),
item: toggleable({
base: interactive({
base: {
...text(layer, "sans", { size: "sm" }),
secondaryTextSpacing: 10,
secondaryText: text(layer, "sans", { size: "sm" }),
padding: { left: 18, right: 18, top: 2, bottom: 2 },
},
state: {
hovered: {
background: background(layer, "hovered"),
...text(layer, "sans", "hovered", { size: "sm" }),
},
},
}),
state: {
active: {
default: {
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
},
},
active: {
background: background(layer, "active"),
},
activeHover: {
background: background(layer, "active"),
},
},
}),
}
}

View File

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { foreground, text } from "./components"
import { interactive } from "../element"
const headerPadding = 8
@ -10,22 +11,30 @@ export default function updateNotification(colorScheme: ColorScheme): Object {
...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"),
actionMessage: interactive({
base: {
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, top: 6, bottom: 6 },
},
},
dismissButton: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
state: {
hovered: {
color: foreground(layer, "hovered"),
},
},
},
}),
dismissButton: interactive({
base: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
},
}),
}
}

View File

@ -8,6 +8,7 @@ import {
TextProperties,
svg,
} from "./components"
import { interactive } from "../element"
export default function welcome(colorScheme: ColorScheme) {
let layer = colorScheme.highest
@ -63,27 +64,31 @@ export default function welcome(colorScheme: ColorScheme) {
bottom: 2,
},
},
button: {
background: background(layer),
border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", interactive_text_size),
hover: {
...text(layer, "sans", "default", interactive_text_size),
background: background(layer, "hovered"),
button: interactive({
base: {
background: background(layer),
border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", interactive_text_size),
},
},
state: {
hovered: {
...text(layer, "sans", "default", interactive_text_size),
background: background(layer, "hovered"),
},
},
}),
usageNote: {
...text(layer, "sans", "variant", { size: "2xs" }),
padding: {

View File

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { toggleable } from "../element"
import {
background,
border,
@ -10,38 +11,53 @@ import {
} from "./components"
import statusBar from "./statusBar"
import tabBar from "./tabBar"
import { interactive } from "../element"
import merge from "ts-deepmerge"
export default function workspace(colorScheme: ColorScheme) {
const layer = colorScheme.lowest
const isLight = colorScheme.isLight
const itemSpacing = 8
const titlebarButton = {
cornerRadius: 6,
padding: {
top: 1,
bottom: 1,
left: 8,
right: 8,
const titlebarButton = toggleable({
base: interactive({
base: {
cornerRadius: 6,
padding: {
top: 1,
bottom: 1,
left: 8,
right: 8,
},
...text(layer, "sans", "variant", { size: "xs" }),
background: background(layer, "variant"),
border: border(layer),
},
state: {
hovered: {
...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"),
},
},
}),
state: {
active: {
default: {
...text(layer, "sans", "variant", "active", { size: "xs" }),
background: background(layer, "variant", "active"),
border: border(layer, "variant", "active"),
},
},
},
...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
@ -78,19 +94,24 @@ export default function workspace(colorScheme: ColorScheme) {
},
cornerRadius: 4,
},
keyboardHint: {
...text(layer, "sans", "variant", { size: "sm" }),
padding: {
top: 3,
left: 8,
right: 8,
bottom: 3,
keyboardHint: interactive({
base: {
...text(layer, "sans", "variant", { size: "sm" }),
padding: {
top: 3,
left: 8,
right: 8,
bottom: 3,
},
cornerRadius: 8,
},
cornerRadius: 8,
hover: {
...text(layer, "sans", "active", { size: "sm" }),
state: {
hovered: {
...text(layer, "sans", "active", { size: "sm" }),
},
},
},
}),
keyboardHintWidth: 320,
},
joiningProjectAvatar: {
@ -201,12 +222,15 @@ export default function workspace(colorScheme: ColorScheme) {
// Sign in buttom
// FlatButton, Variant
signInPrompt: {
margin: {
left: itemSpacing,
signInPrompt: merge(titlebarButton, {
inactive: {
default: {
margin: {
left: itemSpacing,
},
},
},
...titlebarButton,
},
}),
// Offline Indicator
offlineIcon: {
@ -234,40 +258,68 @@ export default function workspace(colorScheme: ColorScheme) {
},
cornerRadius: 6,
},
callControl: {
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: 20,
hover: {
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
callControl: interactive({
base: {
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: 20,
},
state: {
hovered: {
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
},
},
}),
toggleContactsButton: toggleable({
base: interactive({
base: {
margin: { left: itemSpacing },
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 14,
buttonWidth: 20,
},
state: {
clicked: {
background: background(layer, "variant", "pressed"),
},
hovered: {
background: background(layer, "variant", "hovered"),
},
},
}),
state: {
active: {
default: {
background: background(layer, "on", "default"),
},
hovered: {
background: background(layer, "on", "hovered"),
},
clicked: {
background: background(layer, "on", "pressed"),
},
},
},
}),
userMenuButton: merge(titlebarButton, {
inactive: {
default: {
buttonWidth: 20,
iconWidth: 12,
},
},
},
toggleContactsButton: {
margin: { left: itemSpacing },
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 14,
buttonWidth: 20,
active: {
background: background(layer, "variant", "active"),
color: foreground(layer, "variant", "active"),
// posiewic: these properties are not currently set on main
default: {
iconWidth: 12,
button_width: 20,
},
},
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,
@ -285,12 +337,45 @@ export default function workspace(colorScheme: ColorScheme) {
background: background(colorScheme.highest),
border: border(colorScheme.highest, { bottom: true }),
itemSpacing: 8,
navButton: {
color: foreground(colorScheme.highest, "on"),
iconWidth: 12,
buttonWidth: 24,
navButton: interactive({
base: {
color: foreground(colorScheme.highest, "on"),
iconWidth: 12,
buttonWidth: 24,
cornerRadius: 6,
},
state: {
hovered: {
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 },
},
breadcrumbHeight: 24,
breadcrumbs: interactive({
base: {
...text(colorScheme.highest, "sans", "variant"),
cornerRadius: 6,
hover: {
padding: {
left: 6,
right: 6,
},
},
state: {
hovered: {
color: foreground(colorScheme.highest, "on", "hovered"),
background: background(
colorScheme.highest,
@ -298,25 +383,8 @@ export default function workspace(colorScheme: ColorScheme) {
"hovered"
),
},
disabled: {
color: foreground(colorScheme.highest, "on", "disabled"),
},
},
padding: { left: 8, right: 8, top: 4, bottom: 4 },
},
breadcrumbHeight: 24,
breadcrumbs: {
...text(colorScheme.highest, "sans", "variant"),
cornerRadius: 6,
padding: {
left: 6,
right: 6,
},
hover: {
color: foreground(colorScheme.highest, "on", "hovered"),
background: background(colorScheme.highest, "on", "hovered"),
},
},
}),
disconnectedOverlay: {
...text(layer, "sans"),
background: withOpacity(background(layer), 0.8),

View File

@ -1,9 +1,19 @@
import { SingleBoxShadowToken, SingleColorToken, SingleOtherToken, TokenTypes } from "@tokens-studio/types"
import { ColorScheme, Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../colorScheme"
import {
SingleBoxShadowToken,
SingleColorToken,
SingleOtherToken,
TokenTypes,
} from "@tokens-studio/types"
import {
ColorScheme,
Shadow,
SyntaxHighlightStyle,
ThemeSyntax,
} from "../colorScheme"
import { LayerToken, layerToken } from "./layer"
import { PlayersToken, playersToken } from "./players"
import { colorToken } from "./token"
import { Syntax } from "../syntax";
import { Syntax } from "../syntax"
import editor from "../../styleTree/editor"
interface ColorSchemeTokens {
@ -18,27 +28,32 @@ interface ColorSchemeTokens {
syntax?: Partial<ThemeSyntaxColorTokens>
}
const createShadowToken = (shadow: Shadow, tokenName: string): SingleBoxShadowToken => {
const createShadowToken = (
shadow: Shadow,
tokenName: string
): SingleBoxShadowToken => {
return {
name: tokenName,
type: TokenTypes.BOX_SHADOW,
value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`
};
};
value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`,
}
}
const popoverShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
const shadow = colorScheme.popoverShadow;
return createShadowToken(shadow, "popoverShadow");
};
const shadow = colorScheme.popoverShadow
return createShadowToken(shadow, "popoverShadow")
}
const modalShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
const shadow = colorScheme.modalShadow;
return createShadowToken(shadow, "modalShadow");
};
const shadow = colorScheme.modalShadow
return createShadowToken(shadow, "modalShadow")
}
type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
function syntaxHighlightStyleColorTokens(syntax: Syntax): ThemeSyntaxColorTokens {
function syntaxHighlightStyleColorTokens(
syntax: Syntax
): ThemeSyntaxColorTokens {
const styleKeys = Object.keys(syntax) as (keyof Syntax)[]
return styleKeys.reduce((acc, styleKey) => {
@ -46,13 +61,16 @@ function syntaxHighlightStyleColorTokens(syntax: Syntax): ThemeSyntaxColorTokens
// This can happen because we have a "constructor" property on the syntax object
// and a "constructor" property on the prototype of the syntax object
// To work around this just assert that the type of the style is not a function
if (!syntax[styleKey] || typeof syntax[styleKey] === 'function') return acc;
const { color } = syntax[styleKey] as Required<SyntaxHighlightStyle>;
return { ...acc, [styleKey]: colorToken(styleKey, color) };
}, {} as ThemeSyntaxColorTokens);
if (!syntax[styleKey] || typeof syntax[styleKey] === "function")
return acc
const { color } = syntax[styleKey] as Required<SyntaxHighlightStyle>
return { ...acc, [styleKey]: colorToken(styleKey, color) }
}, {} as ThemeSyntaxColorTokens)
}
const syntaxTokens = (colorScheme: ColorScheme): ColorSchemeTokens['syntax'] => {
const syntaxTokens = (
colorScheme: ColorScheme
): ColorSchemeTokens["syntax"] => {
const syntax = editor(colorScheme).syntax
return syntaxHighlightStyleColorTokens(syntax)

View File

@ -1,11 +1,11 @@
import { SingleColorToken } from "@tokens-studio/types";
import { Layer, Style, StyleSet } from "../colorScheme";
import { colorToken } from "./token";
import { SingleColorToken } from "@tokens-studio/types"
import { Layer, Style, StyleSet } from "../colorScheme"
import { colorToken } from "./token"
interface StyleToken {
background: SingleColorToken,
border: SingleColorToken,
foreground: SingleColorToken,
background: SingleColorToken
border: SingleColorToken
foreground: SingleColorToken
}
interface StyleSetToken {
@ -37,24 +37,27 @@ export const styleToken = (style: Style, name: string): StyleToken => {
return token
}
export const styleSetToken = (styleSet: StyleSet, name: string): StyleSetToken => {
const token: StyleSetToken = {} as StyleSetToken;
export const styleSetToken = (
styleSet: StyleSet,
name: string
): StyleSetToken => {
const token: StyleSetToken = {} as StyleSetToken
for (const style in styleSet) {
const s = style as keyof StyleSet;
token[s] = styleToken(styleSet[s], `${name}${style}`);
const s = style as keyof StyleSet
token[s] = styleToken(styleSet[s], `${name}${style}`)
}
return token;
return token
}
export const layerToken = (layer: Layer, name: string): LayerToken => {
const token: LayerToken = {} as LayerToken;
const token: LayerToken = {} as LayerToken
for (const styleSet in layer) {
const s = styleSet as keyof Layer;
token[s] = styleSetToken(layer[s], `${name}${styleSet}`);
const s = styleSet as keyof Layer
token[s] = styleSetToken(layer[s], `${name}${styleSet}`)
}
return token;
return token
}

View File

@ -6,13 +6,21 @@ export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
export type PlayersToken = Record<keyof Players, PlayerToken>
function buildPlayerToken(colorScheme: ColorScheme, index: number): PlayerToken {
function buildPlayerToken(
colorScheme: ColorScheme,
index: number
): PlayerToken {
const playerNumber = index.toString() as keyof Players
return {
selection: colorToken(`player${index}Selection`, colorScheme.players[playerNumber].selection),
cursor: colorToken(`player${index}Cursor`, colorScheme.players[playerNumber].cursor),
selection: colorToken(
`player${index}Selection`,
colorScheme.players[playerNumber].selection
),
cursor: colorToken(
`player${index}Cursor`,
colorScheme.players[playerNumber].cursor
),
}
}
@ -24,5 +32,5 @@ export const playersToken = (colorScheme: ColorScheme): PlayersToken => ({
"4": buildPlayerToken(colorScheme, 4),
"5": buildPlayerToken(colorScheme, 5),
"6": buildPlayerToken(colorScheme, 6),
"7": buildPlayerToken(colorScheme, 7)
"7": buildPlayerToken(colorScheme, 7),
})

View File

@ -1,6 +1,10 @@
import { SingleColorToken, TokenTypes } from "@tokens-studio/types"
export function colorToken(name: string, value: string, description?: string): SingleColorToken {
export function colorToken(
name: string,
value: string,
description?: string
): SingleColorToken {
const token: SingleColorToken = {
name,
type: TokenTypes.COLOR,
@ -8,7 +12,8 @@ export function colorToken(name: string, value: string, description?: string): S
description,
}
if (!token.value || token.value === '') throw new Error("Color token must have a value")
if (!token.value || token.value === "")
throw new Error("Color token must have a value")
return token
}

View File

@ -1 +1,10 @@
export function slugify(t: string): string { return t.toString().toLowerCase().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/^-+/, '').replace(/-+$/, '') }
export function slugify(t: string): string {
return t
.toString()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w\-]+/g, "")
.replace(/\-\-+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
}

View File

@ -20,7 +20,17 @@
"noFallthroughCasesInSwitch": false,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"skipLibCheck": true
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@element/*": ["./src/element/*"],
"@component/*": ["./src/component/*"],
"@styleTree/*": ["./src/styleTree/*"],
"@theme/*": ["./src/theme/*"],
"@themes/*": ["./src/themes/*"],
"@util/*": ["./src/util/*"]
}
},
"exclude": ["node_modules"]
}

8
styles/vitest.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { configDefaults, defineConfig } from "vitest/config"
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, "target/*"],
include: ["src/**/*.{spec,test}.ts"],
},
})