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,
|
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,126 +22,215 @@ 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()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_end()
|
.flex_col()
|
||||||
.justify_between()
|
.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(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.text_color(rgb(colors::ACCENT))
|
||||||
.flex_col()
|
.child(IconName::Globe),
|
||||||
.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(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(
|
.child(
|
||||||
div()
|
"ELY keeps tabs, workspaces, pinned items, and history mirrored across your \
|
||||||
.flex()
|
devices — encrypted in your hands and replayed at the edge.",
|
||||||
.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())
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.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()
|
||||||
.children(
|
.flex()
|
||||||
snapshot
|
.items_center()
|
||||||
.sync_status
|
.gap(px(8.0))
|
||||||
.objects()
|
.child(
|
||||||
.iter()
|
div()
|
||||||
.enumerate()
|
.text_size(px(13.0))
|
||||||
.map(|(index, status)| render_sync_object_row(index, status, cx)),
|
.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()
|
.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 }
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user