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,
};
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,119 +22,207 @@ 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 {
div()
.flex()
.items_end()
.justify_between()
.child(
fn render_left_column(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
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)),
),
)
.child(
.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_2()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(IconName::Globe)
.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()
.text_color(rgb(colors::ACCENT))
.child(IconName::Globe),
)
.child(format!(
"{SYNC_SERVICE_NAME}: {}",
"{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(
"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<ElyShell>) -> AnyElement {
fn render_metrics_card(
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> 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<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()
.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<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()
.border_t_1()
.border_color(rgb(colors::HAIRLINE))
.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
@@ -147,6 +230,7 @@ fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -
.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<ElyShell>,
) -> 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<ElyShell>,
) -> 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;