Recompose sync page in design's marketing layout

Sync now opens with the design's two-column structure: a serif headline
and Cloudflare Sync status pill on the left with a local-queue card
showing pending/failed counts and a reset action, and a "What syncs"
card on the right that lists every SyncObjectKind with a state dot,
local count, and a working policy toggle.

The connection label is honest: until Better Auth integration ships, it
reads "Local-only · sign-in coming soon" rather than displaying a fake
sign-in form. All toggles dispatch real shell.set_sync_object_policy
calls so the state changes are persisted through the existing core API.
This commit is contained in:
2026-05-09 18:16:41 -04:00
parent cc43c1ed01
commit 42f67b794e
+218 -136
View File
@@ -4,15 +4,10 @@ use ely_domain::{
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
}; };
use gpui::{ use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
px, rgb, StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba,
};
use gpui::{StatefulInteractiveElement, prelude::FluentBuilder};
use gpui_component::{
IconName, Sizable, StyledExt,
button::{Button, ButtonVariants},
scroll::ScrollableElement,
}; };
use gpui_component::{IconName, scroll::ScrollableElement};
use crate::brand::SYNC_SERVICE_NAME; use crate::brand::SYNC_SERVICE_NAME;
@@ -27,119 +22,207 @@ impl ElyShell {
render_canvas_surface( render_canvas_surface(
div() div()
.size_full() .size_full()
.p_8() .pt(px(40.0))
.px(px(56.0))
.pb(px(32.0))
.flex() .flex()
.flex_col() .justify_center()
.gap_5() .child(
.child(render_sync_header(snapshot)) div()
.child(render_sync_queue(snapshot, cx)) .max_w(px(960.0))
.child(render_sync_objects(snapshot, cx)), .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<ElyShell>) -> AnyElement {
div()
.flex()
.items_end()
.justify_between()
.child(
div() div()
.flex() .flex()
.flex_col() .flex_col()
.gap_2() .items_start()
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Sync")) .gap(px(20.0))
.child( .child(render_status_pill(snapshot))
div() .child(render_serif_headline())
.text_sm() .child(render_intro_paragraph())
.text_color(rgb(colors::MUTED)) .child(render_metrics_card(snapshot, cx))
.child(format!("Profile: {}", snapshot.active_profile_name)), .into_any_element()
), }
)
.child( fn render_status_pill(snapshot: &BrowserSnapshot) -> AnyElement {
div() div()
.flex() .flex()
.items_center() .items_center()
.gap_2() .gap(px(8.0))
.text_xs() .px(px(12.0))
.font_semibold() .py(px(5.0))
.text_color(rgb(colors::MUTED)) .rounded(px(999.0))
.child(IconName::Globe) .bg(rgba(PILL_BG))
.text_size(px(11.0))
.text_color(rgb(colors::INK_3))
.child(
div()
.text_color(rgb(colors::ACCENT))
.child(IconName::Globe),
)
.child(format!( .child(format!(
"{SYNC_SERVICE_NAME}: {}", "{SYNC_SERVICE_NAME} · {}",
connection_label(snapshot.sync_status.connection()) 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(
"ELY keeps tabs, workspaces, pinned items, and history mirrored across your \
devices — encrypted in your hands and replayed at the edge.",
) )
.into_any_element() .into_any_element()
} }
fn render_sync_queue(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement { fn render_metrics_card(
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div() div()
.rounded_md() .max_w(px(380.0))
.border_1() .p(px(20.0))
.border_color(rgb(colors::HAIRLINE)) .rounded(px(16.0))
.bg(rgb(colors::CANVAS_SOFT)) .bg(rgba(CARD_BG))
.px_4()
.py_3()
.flex() .flex()
.items_center() .flex_col()
.justify_between() .gap(px(16.0))
.gap_4()
.child( .child(
div() div()
.flex() .text_size(px(12.5))
.items_center() .text_color(rgb(colors::INK_3))
.gap_4() .child("Local queue"),
.child(metric_block( )
"Pending objects", .child(
div()
.grid()
.grid_cols(2)
.gap(px(12.0))
.child(render_metric(
"Pending",
snapshot.sync_status.pending_objects(), snapshot.sync_status.pending_objects(),
colors::INK, colors::INK,
)) ))
.child(metric_block( .child(render_metric(
"Failed objects", "Failed",
snapshot.sync_status.failed_objects(), snapshot.sync_status.failed_objects(),
colors::ERROR, colors::ERROR,
)), )),
) )
.child( .child(render_reset_button(cx))
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);
})),
)
.into_any_element() .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() div()
.flex() .flex()
.flex_col() .flex_col()
.gap_1() .gap_1()
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(label))
.child( .child(
div() div()
.text_size(px(18.0)) .text_size(px(10.5))
.font_semibold() .text_color(rgb(colors::INK_4))
.child(label),
)
.child(
div()
.text_size(px(20.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(color)) .text_color(rgb(color))
.child(value.to_string()), .child(value.to_string()),
) )
.into_any_element() .into_any_element()
} }
fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement { fn render_reset_button(cx: &mut Context<ElyShell>) -> 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<ElyShell>) -> AnyElement {
div() div()
.flex_1()
.min_h_0()
.flex() .flex()
.flex_col() .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<ElyShell>,
) -> 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() .overflow_y_scrollbar()
.border_t_1() .child(
.border_color(rgb(colors::HAIRLINE)) 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( .children(
snapshot snapshot
.sync_status .sync_status
@@ -147,6 +230,7 @@ fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, status)| render_sync_object_row(index, status, cx)), .map(|(index, status)| render_sync_object_row(index, status, cx)),
),
) )
.into_any_element() .into_any_element()
} }
@@ -157,105 +241,114 @@ fn render_sync_object_row(
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
div() div()
.py_3()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.flex() .flex()
.items_center() .items_center()
.justify_between() .gap(px(10.0))
.gap_4() .py(px(8.0))
.border_b_1()
.border_color(rgba(colors::DIVIDER))
.child(render_state_dot(status.state()))
.child( .child(
div() div()
.flex_1()
.min_w_0() .min_w_0()
.flex() .flex()
.flex_col() .flex_col()
.gap_1() .gap_1()
.child( .child(
div() div()
.text_sm() .text_size(px(13.0))
.font_semibold() .font_weight(FontWeight(500.0))
.truncate()
.text_color(rgb(colors::INK)) .text_color(rgb(colors::INK))
.child(sync_object_kind_label(status.kind())), .child(sync_object_kind_label(status.kind())),
) )
.child( .child(
div() div()
.text_xs() .text_size(px(11.0))
.truncate() .text_color(rgb(colors::INK_4))
.text_color(rgb(colors::MUTED)) .child(format!(
.child(format!("{} local objects", status.local_count())), "{} local · {}",
status.local_count(),
sync_object_state_label(status.state())
)),
), ),
) )
.child( .child(render_policy_toggle(index, status, cx))
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)),
)
.into_any_element() .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, index: usize,
status: &SyncObjectStatus, status: &SyncObjectStatus,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
let enabled = status.policy() == SyncObjectPolicy::Enabled; 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 kind = status.kind();
let track_color = if enabled {
colors::ACCENT
} else {
0x281e1426
};
div() div()
.id(SharedString::from(format!("sync-policy-toggle-{index}"))) .id(SharedString::from(format!("sync-policy-{index}")))
.w(px(38.0)) .w(px(34.0))
.h(px(22.0)) .h(px(20.0))
.rounded_full() .rounded_full()
.border_1() .bg(rgba(track_color))
.border_color(rgb(sync_policy_border_color(enabled)))
.bg(rgb(sync_policy_track_color(enabled)))
.p(px(2.0)) .p(px(2.0))
.cursor_pointer() .cursor_pointer()
.hover(|style| style.opacity(0.9)) .hover(|style| style.opacity(0.9))
.active(|style| style.opacity(0.78)) .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| { .on_click(cx.listener(move |shell, _, _, cx| {
shell.set_sync_object_policy(kind, next_policy, 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() .into_any_element()
} }
fn connection_label(connection: &SyncConnectionState) -> &'static str { fn connection_label(connection: &SyncConnectionState) -> &'static str {
match connection { match connection {
SyncConnectionState::SignedOut => "Signed out", SyncConnectionState::SignedOut => "Local-only · sign-in coming soon",
} }
} }
fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str { fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
match kind { match kind {
SyncObjectKind::Spaces => "Spaces", SyncObjectKind::Spaces => "Spaces",
SyncObjectKind::Tabs => "Tabs", SyncObjectKind::Tabs => "Tabs & pinned",
SyncObjectKind::Bookmarks => "Bookmarks", SyncObjectKind::Bookmarks => "Bookmarks",
SyncObjectKind::Notes => "Notes", SyncObjectKind::Notes => "Notes",
SyncObjectKind::ReadingList => "Reading List", SyncObjectKind::ReadingList => "Reading list",
SyncObjectKind::Profiles => "Profiles", SyncObjectKind::Profiles => "Profiles",
SyncObjectKind::SitePermissions => "Site permissions", SyncObjectKind::SitePermissions => "Site permissions",
SyncObjectKind::History => "History", 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 { const PILL_BG: u32 = 0xffffffb3;
match state { const CARD_BG: u32 = 0xffffffd9;
SyncObjectState::LocalOnly => colors::MUTED, const BUTTON_BG: u32 = 0xffffffd9;
SyncObjectState::Paused => colors::PRIMARY, const BUTTON_BG_HOVER: u32 = 0xffffffeb;
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 }
}