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:
@@ -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<ElyShell>) -> 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<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))
|
||||
.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<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;
|
||||
|
||||
Reference in New Issue
Block a user