diff --git a/crates/ely_app/src/shell/internal_pages/sync.rs b/crates/ely_app/src/shell/internal_pages/sync.rs index ca60856..defa8d0 100644 --- a/crates/ely_app/src/shell/internal_pages/sync.rs +++ b/crates/ely_app/src/shell/internal_pages/sync.rs @@ -4,15 +4,10 @@ use ely_domain::{ SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, }; use gpui::{ - AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, - px, rgb, -}; -use gpui::{StatefulInteractiveElement, prelude::FluentBuilder}; -use gpui_component::{ - IconName, Sizable, StyledExt, - button::{Button, ButtonVariants}, - scroll::ScrollableElement, + AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba, }; +use gpui_component::{IconName, scroll::ScrollableElement}; use crate::brand::SYNC_SERVICE_NAME; @@ -27,126 +22,215 @@ impl ElyShell { render_canvas_surface( div() .size_full() - .p_8() + .pt(px(40.0)) + .px(px(56.0)) + .pb(px(32.0)) .flex() - .flex_col() - .gap_5() - .child(render_sync_header(snapshot)) - .child(render_sync_queue(snapshot, cx)) - .child(render_sync_objects(snapshot, cx)), + .justify_center() + .child( + div() + .max_w(px(960.0)) + .grid() + .grid_cols(2) + .gap(px(32.0)) + .child(render_left_column(snapshot, cx)) + .child(render_right_column(snapshot, cx)), + ), ) } } -fn render_sync_header(snapshot: &BrowserSnapshot) -> AnyElement { +fn render_left_column(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { div() .flex() - .items_end() - .justify_between() + .flex_col() + .items_start() + .gap(px(20.0)) + .child(render_status_pill(snapshot)) + .child(render_serif_headline()) + .child(render_intro_paragraph()) + .child(render_metrics_card(snapshot, cx)) + .into_any_element() +} + +fn render_status_pill(snapshot: &BrowserSnapshot) -> AnyElement { + div() + .flex() + .items_center() + .gap(px(8.0)) + .px(px(12.0)) + .py(px(5.0)) + .rounded(px(999.0)) + .bg(rgba(PILL_BG)) + .text_size(px(11.0)) + .text_color(rgb(colors::INK_3)) .child( div() - .flex() - .flex_col() - .gap_2() - .child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Sync")) - .child( - div() - .text_sm() - .text_color(rgb(colors::MUTED)) - .child(format!("Profile: {}", snapshot.active_profile_name)), - ), + .text_color(rgb(colors::ACCENT)) + .child(IconName::Globe), ) + .child(format!( + "{SYNC_SERVICE_NAME} · {}", + connection_label(snapshot.sync_status.connection()) + )) + .into_any_element() +} + +fn render_serif_headline() -> AnyElement { + div() + .text_size(px(46.0)) + .font_weight(FontWeight(400.0)) + .text_color(rgb(colors::INK)) + .child("Your tabs, on every device.") + .into_any_element() +} + +fn render_intro_paragraph() -> AnyElement { + div() + .max_w(px(440.0)) + .text_size(px(14.0)) + .text_color(rgb(colors::INK_2)) .child( - div() - .flex() - .items_center() - .gap_2() - .text_xs() - .font_semibold() - .text_color(rgb(colors::MUTED)) - .child(IconName::Globe) - .child(format!( - "{SYNC_SERVICE_NAME}: {}", - connection_label(snapshot.sync_status.connection()) - )), + "ELY keeps tabs, workspaces, pinned items, and history mirrored across your \ + devices — encrypted in your hands and replayed at the edge.", ) .into_any_element() } -fn render_sync_queue(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { +fn render_metrics_card( + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { div() - .rounded_md() - .border_1() - .border_color(rgb(colors::HAIRLINE)) - .bg(rgb(colors::CANVAS_SOFT)) - .px_4() - .py_3() + .max_w(px(380.0)) + .p(px(20.0)) + .rounded(px(16.0)) + .bg(rgba(CARD_BG)) .flex() - .items_center() - .justify_between() - .gap_4() + .flex_col() + .gap(px(16.0)) .child( div() - .flex() - .items_center() - .gap_4() - .child(metric_block( - "Pending objects", + .text_size(px(12.5)) + .text_color(rgb(colors::INK_3)) + .child("Local queue"), + ) + .child( + div() + .grid() + .grid_cols(2) + .gap(px(12.0)) + .child(render_metric( + "Pending", snapshot.sync_status.pending_objects(), colors::INK, )) - .child(metric_block( - "Failed objects", + .child(render_metric( + "Failed", snapshot.sync_status.failed_objects(), colors::ERROR, )), ) - .child( - Button::new("reset-sync-settings") - .ghost() - .xsmall() - .icon(IconName::Undo2) - .label("Reset") - .tooltip("Restore Sync Defaults") - .on_click(cx.listener(|shell, _, _, cx| { - shell.reset_sync_settings(cx); - })), - ) + .child(render_reset_button(cx)) .into_any_element() } -fn metric_block(label: &'static str, value: usize, color: u32) -> AnyElement { +fn render_metric(label: &'static str, value: usize, color: u32) -> AnyElement { div() .flex() .flex_col() .gap_1() - .child(div().text_xs().text_color(rgb(colors::MUTED)).child(label)) .child( div() - .text_size(px(18.0)) - .font_semibold() + .text_size(px(10.5)) + .text_color(rgb(colors::INK_4)) + .child(label), + ) + .child( + div() + .text_size(px(20.0)) + .font_weight(FontWeight(500.0)) .text_color(rgb(color)) .child(value.to_string()), ) .into_any_element() } -fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { +fn render_reset_button(cx: &mut Context) -> AnyElement { + div() + .id(SharedString::from("sync-reset")) + .px(px(12.0)) + .py(px(7.0)) + .rounded(px(8.0)) + .bg(rgba(BUTTON_BG)) + .text_size(px(12.0)) + .font_weight(FontWeight(500.0)) + .text_color(rgb(colors::INK_2)) + .cursor_pointer() + .hover(|style| style.bg(rgba(BUTTON_BG_HOVER))) + .active(|style| style.opacity(0.85)) + .on_click(cx.listener(|shell, _, _, cx| shell.reset_sync_settings(cx))) + .child("Reset to defaults") + .into_any_element() +} + +fn render_right_column(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { div() - .flex_1() - .min_h_0() .flex() .flex_col() + .gap(px(14.0)) + .child(render_what_syncs_card(snapshot, cx)) + .into_any_element() +} + +fn render_what_syncs_card( + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { + div() + .p(px(18.0)) + .rounded(px(16.0)) + .bg(rgba(CARD_BG)) + .flex() + .flex_col() + .gap(px(12.0)) + .max_h(px(640.0)) .overflow_y_scrollbar() - .border_t_1() - .border_color(rgb(colors::HAIRLINE)) - .children( - snapshot - .sync_status - .objects() - .iter() - .enumerate() - .map(|(index, status)| render_sync_object_row(index, status, cx)), + .child( + div() + .flex() + .items_center() + .gap(px(8.0)) + .child( + div() + .text_size(px(13.0)) + .font_weight(FontWeight(500.0)) + .text_color(rgb(colors::INK)) + .child("What syncs"), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(rgb(colors::INK_3)) + .child(format!( + "{} kinds tracked", + snapshot.sync_status.objects().len() + )), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(2.0)) + .children( + snapshot + .sync_status + .objects() + .iter() + .enumerate() + .map(|(index, status)| render_sync_object_row(index, status, cx)), + ), ) .into_any_element() } @@ -157,105 +241,114 @@ fn render_sync_object_row( cx: &mut Context, ) -> AnyElement { div() - .py_3() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) .flex() .items_center() - .justify_between() - .gap_4() + .gap(px(10.0)) + .py(px(8.0)) + .border_b_1() + .border_color(rgba(colors::DIVIDER)) + .child(render_state_dot(status.state())) .child( div() + .flex_1() .min_w_0() .flex() .flex_col() .gap_1() .child( div() - .text_sm() - .font_semibold() - .truncate() + .text_size(px(13.0)) + .font_weight(FontWeight(500.0)) .text_color(rgb(colors::INK)) .child(sync_object_kind_label(status.kind())), ) .child( div() - .text_xs() - .truncate() - .text_color(rgb(colors::MUTED)) - .child(format!("{} local objects", status.local_count())), + .text_size(px(11.0)) + .text_color(rgb(colors::INK_4)) + .child(format!( + "{} local · {}", + status.local_count(), + sync_object_state_label(status.state()) + )), ), ) - .child( - div() - .flex() - .items_center() - .gap_3() - .child( - div() - .text_xs() - .font_semibold() - .text_color(rgb(sync_object_state_color(status.state()))) - .child(sync_object_state_label(status.state())), - ) - .child(render_sync_policy_toggle(index, status, cx)), - ) + .child(render_policy_toggle(index, status, cx)) .into_any_element() } -fn render_sync_policy_toggle( +fn render_state_dot(state: SyncObjectState) -> AnyElement { + let color = match state { + SyncObjectState::LocalOnly => colors::INK_4, + SyncObjectState::Paused => colors::INK_5, + SyncObjectState::PrivacyControlled => colors::ACCENT, + }; + + div() + .size(px(8.0)) + .rounded_full() + .bg(rgb(color)) + .into_any_element() +} + +fn render_policy_toggle( index: usize, status: &SyncObjectStatus, cx: &mut Context, ) -> AnyElement { let enabled = status.policy() == SyncObjectPolicy::Enabled; - let next_policy = if enabled { SyncObjectPolicy::Paused } else { SyncObjectPolicy::Enabled }; + let next_policy = if enabled { + SyncObjectPolicy::Paused + } else { + SyncObjectPolicy::Enabled + }; let kind = status.kind(); + let track_color = if enabled { + colors::ACCENT + } else { + 0x281e1426 + }; div() - .id(SharedString::from(format!("sync-policy-toggle-{index}"))) - .w(px(38.0)) - .h(px(22.0)) + .id(SharedString::from(format!("sync-policy-{index}"))) + .w(px(34.0)) + .h(px(20.0)) .rounded_full() - .border_1() - .border_color(rgb(sync_policy_border_color(enabled))) - .bg(rgb(sync_policy_track_color(enabled))) + .bg(rgba(track_color)) .p(px(2.0)) .cursor_pointer() .hover(|style| style.opacity(0.9)) .active(|style| style.opacity(0.78)) - .child( - div() - .w(px(16.0)) - .h(px(16.0)) - .rounded_full() - .bg(rgb(colors::SURFACE_CARD)) - .shadow_sm() - .when(enabled, |this| this.ml(px(16.0))), - ) .on_click(cx.listener(move |shell, _, _, cx| { shell.set_sync_object_policy(kind, next_policy, cx); })) + .child( + div() + .size(px(16.0)) + .rounded_full() + .bg(rgb(0xffffff)) + .when(enabled, |this| this.ml(px(14.0))), + ) .into_any_element() } fn connection_label(connection: &SyncConnectionState) -> &'static str { match connection { - SyncConnectionState::SignedOut => "Signed out", + SyncConnectionState::SignedOut => "Local-only · sign-in coming soon", } } fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str { match kind { SyncObjectKind::Spaces => "Spaces", - SyncObjectKind::Tabs => "Tabs", + SyncObjectKind::Tabs => "Tabs & pinned", SyncObjectKind::Bookmarks => "Bookmarks", SyncObjectKind::Notes => "Notes", - SyncObjectKind::ReadingList => "Reading List", + SyncObjectKind::ReadingList => "Reading list", SyncObjectKind::Profiles => "Profiles", SyncObjectKind::SitePermissions => "Site permissions", SyncObjectKind::History => "History", - SyncObjectKind::PluginSettings => "Plugin settings", + SyncObjectKind::PluginSettings => "Plugins", } } @@ -267,18 +360,7 @@ fn sync_object_state_label(state: SyncObjectState) -> &'static str { } } -fn sync_object_state_color(state: SyncObjectState) -> u32 { - match state { - SyncObjectState::LocalOnly => colors::MUTED, - SyncObjectState::Paused => colors::PRIMARY, - SyncObjectState::PrivacyControlled => colors::PRIMARY, - } -} - -fn sync_policy_track_color(enabled: bool) -> u32 { - if enabled { colors::SUCCESS } else { colors::CANVAS_SOFT } -} - -fn sync_policy_border_color(enabled: bool) -> u32 { - if enabled { colors::SUCCESS } else { colors::HAIRLINE_STRONG } -} +const PILL_BG: u32 = 0xffffffb3; +const CARD_BG: u32 = 0xffffffd9; +const BUTTON_BG: u32 = 0xffffffd9; +const BUTTON_BG_HOVER: u32 = 0xffffffeb;