Forráskód Böngészése

app: feed styling fixes

darkfi 5 napja
szülő
commit
ed1a910db8

+ 26 - 6
bin/app/src/app/schema/wallet/data.rs

@@ -39,11 +39,21 @@ mod android_ui_consts {
     pub const TOKEN_NAME_OFFSET: f32 = 200.;
     pub const PADDING_X: f32 = 40.;
     pub const PADDING_Y: f32 = 30.;
-    /// Padding around the feed layer on the main wallet screen.
-    pub const FEED_LAYER_PADDING: f32 = 30.;
+    /// Margin between the feed layers and the screen edges.
+    pub const FEED_LAYER_MARGIN: f32 = 30.;
+    /// Inset padding of the feed view inside its layer (sides and
+    /// bottom; the top is flush under the header).
+    pub const FEED_LAYER_PADDING: f32 = 20.;
     /// Header band inside the feed layer (the feed chatview starts
     /// below it).
-    pub const FEED_HEADER_HEIGHT: f32 = 60.;
+    pub const FEED_HEADER_HEIGHT: f32 = 100.;
+    /// Horizontal padding of the header's contents.
+    pub const FEED_HEADER_PAD: f32 = 30.;
+    /// "CHAT" header label font size.
+    pub const FEED_HEADER_FONTSIZE: f32 = 40.;
+    /// The header's down-arrow glyph scale.
+    pub const FEED_ARROW_SCALE: f32 = 60.;
+    pub const FEED_ARROW_Y: f32 = 44.;
     // Feed chatview styling (mirrors the chat screen's values).
     pub const FEED_FONTSIZE: f32 = 50.;
     pub const FEED_TIMESTAMP_FONTSIZE: f32 = 24.;
@@ -110,11 +120,21 @@ mod ui_consts {
     pub const TOKEN_NAME_OFFSET: f32 = 90.;
     pub const PADDING_X: f32 = 20.;
     pub const PADDING_Y: f32 = 15.;
-    /// Padding around the feed layer on the main wallet screen.
-    pub const FEED_LAYER_PADDING: f32 = 15.;
+    /// Margin between the feed layers and the screen edges.
+    pub const FEED_LAYER_MARGIN: f32 = 15.;
+    /// Inset padding of the feed view inside its layer (sides and
+    /// bottom; the top is flush under the header).
+    pub const FEED_LAYER_PADDING: f32 = 10.;
     /// Header band inside the feed layer (the feed chatview starts
     /// below it).
-    pub const FEED_HEADER_HEIGHT: f32 = 30.;
+    pub const FEED_HEADER_HEIGHT: f32 = 50.;
+    /// Horizontal padding of the header's contents.
+    pub const FEED_HEADER_PAD: f32 = 15.;
+    /// "CHAT" header label font size.
+    pub const FEED_HEADER_FONTSIZE: f32 = 20.;
+    /// The header's down-arrow glyph scale.
+    pub const FEED_ARROW_SCALE: f32 = 30.;
+    pub const FEED_ARROW_Y: f32 = 22.;
     // Feed chatview styling (mirrors the chat screen's values).
     pub const FEED_FONTSIZE: f32 = 25.;
     pub const FEED_TIMESTAMP_FONTSIZE: f32 = 12.;

+ 218 - 16
bin/app/src/app/schema/wallet/main.rs

@@ -27,7 +27,7 @@ use crate::{
         App,
     },
     expr,
-    gfx::gfxtag,
+    gfx::{gfxtag, Point},
     mesh::{rgba, COLOR_CYAN, COLOR_TEAL},
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr, Slot},
@@ -81,28 +81,41 @@ pub async fn make(
     // ============================================
     // Feed Layer (bottom half of the screen)
     // ============================================
+    cc.add_const_f32("FEED_LAYER_MARGIN", FEED_LAYER_MARGIN);
     cc.add_const_f32("FEED_LAYER_PADDING", FEED_LAYER_PADDING);
     cc.add_const_f32("FEED_HEADER_HEIGHT", FEED_HEADER_HEIGHT);
+    cc.add_const_f32("FEED_HEADER_PAD", FEED_HEADER_PAD);
+    cc.add_const_f32("FEED_ARROW_SCALE", FEED_ARROW_SCALE);
+
+    // Renders above the chat-menu button bg and the logo (z 3/4) so
+    // the feed always stays on top of them.
+    const FEED_LAYER_Z: u32 = 2;
+
     let feed_layer = create_layer("feed_layer");
     let prop = feed_layer.get_property("rect").unwrap();
-    prop.set_default_f32(0, FEED_LAYER_PADDING).unwrap();
-    let code = cc.compile("h / 2 + FEED_LAYER_PADDING").unwrap();
+    prop.set_default_f32(0, FEED_LAYER_MARGIN).unwrap();
+    let code = cc.compile("h / 2 + FEED_LAYER_MARGIN").unwrap();
     prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - 2 * FEED_LAYER_PADDING").unwrap();
+    let code = cc.compile("w - 2 * FEED_LAYER_MARGIN").unwrap();
     prop.set_default_expr(2, code).unwrap();
-    let code = cc.compile("h / 2 - 2 * FEED_LAYER_PADDING").unwrap();
+    let code = cc.compile("h / 2 - 2 * FEED_LAYER_MARGIN").unwrap();
     prop.set_default_expr(3, code).unwrap();
     feed_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-    feed_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    feed_layer.set_property_u32(atom, Role::App, "z_index", FEED_LAYER_Z).unwrap();
     let feed_layer = feed_layer
         .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
     // Child of the main wallet layer: shows with the main screen and
     // hides with it when navigating to send/receive screens.
     main_layer.link(feed_layer.clone());
-
-    // Feed layer background: covers the entire layer. Transparent grey
-    // by default; the active theme may restyle it (scifi recolors it).
+    let feed_layer_is_visible =
+        PropertyBool::wrap(&feed_layer, Role::App, "is_visible", 0).unwrap();
+
+    // Feed layer background: covers the entire layer, header band
+    // included — everything background-ish renders in this one shape.
+    // Minimal (dark) default: slightly lighter than the dark baseline
+    // with a one-shade-lighter header band; the active theme may
+    // restyle it (scifi gradient/darkens it, light lightens it).
     let node = create_vector_art("bg");
     let prop = node.get_property("rect").unwrap();
     prop.set_default_f32(0, 0.).unwrap();
@@ -116,7 +129,14 @@ pub async fn make(
         expr::const_f32(0.),
         expr::load_var("w"),
         expr::load_var("h"),
-        [0.5, 0.5, 0.5, 0.5],
+        [0.24, 0.24, 0.24, 1.],
+    );
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(FEED_HEADER_HEIGHT),
+        [0.28, 0.28, 0.28, 1.],
     );
     node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
@@ -124,14 +144,17 @@ pub async fn make(
     feed_layer.link(node);
 
     // The global feed chatview, bound to the reserved `*` pseudo-channel.
-    // Starts below the feed header band and fills the rest of the layer.
-    // Channel labels are forced on (the feed is unread without them).
+    // Starts below the feed header band; inset from the layer's sides
+    // and bottom by FEED_LAYER_PADDING (the top is flush under the
+    // header). Channel labels are forced on (the feed is unread
+    // without them).
     let node = create_chatview("feed_chatty");
     let prop = node.get_property("rect").unwrap();
-    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(0, FEED_LAYER_PADDING).unwrap();
     prop.set_default_f32(1, FEED_HEADER_HEIGHT).unwrap();
-    prop.set_default_expr(2, expr::load_var("w")).unwrap();
-    let code = cc.compile("h - FEED_HEADER_HEIGHT").unwrap();
+    let code = cc.compile("w - 2 * FEED_LAYER_PADDING").unwrap();
+    prop.set_default_expr(2, code).unwrap();
+    let code = cc.compile("h - FEED_HEADER_HEIGHT - FEED_LAYER_PADDING").unwrap();
     prop.set_default_expr(3, code).unwrap();
     node.get_property("font_size").unwrap().set_default_f32(0, FEED_FONTSIZE).unwrap();
     node.get_property("timestamp_font_size")
@@ -260,6 +283,99 @@ pub async fn make(
     let Pimpl::ChatView(feed_view) = feed_chatty.pimpl() else { unreachable!() };
     feed_view.handle_set_channel(FEED_CHANNEL.to_string()).await;
 
+    // The feed header: bg band, "CHAT" label on the left, a down
+    // arrow on the right, and a full-header toggle button. Built into
+    // both the expanded feed layer and the rolled-up strip below.
+    let hide_feed_btn =
+        make_feed_header(app, &feed_layer, &cc, &theme, &window_scale, i18n_fish, false)
+            .await
+            .expect("feed header");
+
+    // ============================================
+    // Feed Rolled Layer (header-only strip)
+    // ============================================
+    // Sits at the bottom edge of where the feed layer ends: the layer
+    // bottom minus the header height, exactly one header tall.
+    let feed_rolled_layer = create_layer("feed_rolled_layer");
+    let prop = feed_rolled_layer.get_property("rect").unwrap();
+    prop.set_default_f32(0, FEED_LAYER_MARGIN).unwrap();
+    let code = cc.compile("h - FEED_LAYER_MARGIN - FEED_HEADER_HEIGHT").unwrap();
+    prop.set_default_expr(1, code).unwrap();
+    let code = cc.compile("w - 2 * FEED_LAYER_MARGIN").unwrap();
+    prop.set_default_expr(2, code).unwrap();
+    prop.set_default_f32(3, FEED_HEADER_HEIGHT).unwrap();
+    feed_rolled_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    feed_rolled_layer.set_property_u32(atom, Role::App, "z_index", FEED_LAYER_Z).unwrap();
+    let feed_rolled_layer = feed_rolled_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    main_layer.link(feed_rolled_layer.clone());
+    let feed_rolled_layer_is_visible =
+        PropertyBool::wrap(&feed_rolled_layer, Role::App, "is_visible", 0).unwrap();
+
+    // Rolled strip background: the whole strip is header, so its bg is
+    // the header-band shade. One shape, restyled by the theme.
+    let node = create_vector_art("bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [0.28, 0.28, 0.28, 1.],
+    );
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    feed_rolled_layer.link(node);
+
+    // Same header construction as the expanded layer.
+    let show_feed_btn =
+        make_feed_header(app, &feed_rolled_layer, &cc, &theme, &window_scale, i18n_fish, true)
+            .await
+            .expect("feed header");
+
+    // Clicking the expanded header rolls the feed up (full layer
+    // hides, header-only strip shows); clicking the rolled header
+    // expands it again.
+    {
+        let (slot, recvr) = Slot::new("hide_feed");
+        hide_feed_btn.register("click", slot).unwrap();
+        let feed_vis = feed_layer_is_visible.clone();
+        let rolled_vis = feed_rolled_layer_is_visible.clone();
+        let redraw = app.redraw_trigger.clone();
+        let listen = app.ex.spawn(async move {
+            while let Ok(_) = recvr.recv().await {
+                let atom = &mut redraw.make_guard(gfxtag!("hide_feed"));
+                feed_vis.set(atom, false);
+                rolled_vis.set(atom, true);
+            }
+        });
+        app.tasks.lock().push(listen);
+    }
+
+    {
+        let (slot, recvr) = Slot::new("show_feed");
+        show_feed_btn.register("click", slot).unwrap();
+        let feed_vis = feed_layer_is_visible.clone();
+        let rolled_vis = feed_rolled_layer_is_visible.clone();
+        let redraw = app.redraw_trigger.clone();
+        let listen = app.ex.spawn(async move {
+            while let Ok(_) = recvr.recv().await {
+                let atom = &mut redraw.make_guard(gfxtag!("show_feed"));
+                feed_vis.set(atom, true);
+                rolled_vis.set(atom, false);
+            }
+        });
+        app.tasks.lock().push(listen);
+    }
+
     // Back arrow
     let node = create_vector_art("wallet_back_btn_bg");
     let prop = node.get_property("rect").unwrap();
@@ -395,7 +511,7 @@ pub async fn make(
     prop.set_default_f32(1, HEADER_HEIGHT / 2.).unwrap();
     prop.set_default_f32(2, BACKARROW_BG_W).unwrap();
     prop.set_default_f32(3, HEADER_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 4).unwrap();
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     let shape = shape::create_darkfi_logo([1., 1., 1., 1.]).scaled(DARKFI_LOGO_SCALE);
     node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
@@ -811,3 +927,89 @@ async fn create_chat_btn(
         node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(node);
 }
+
+/// Build the feed header into `parent`: a "CHAT" label on the left, a
+/// chevron arrow on the right (pointing down in the expanded layer, up
+/// in the rolled strip), and the full-header `hide_feed` toggle button
+/// (returned; the caller wires its click — expansion vs. rolling
+/// depends on which layer this is). The header band's background is
+/// part of the layer's `bg` shape, not a node of its own.
+async fn make_feed_header(
+    app: &App,
+    parent: &SceneNodePtr,
+    cc: &expr::Compiler,
+    theme: &SceneNodePtr,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    arrow_up: bool,
+) -> crate::error::Result<SceneNodePtr> {
+    let atom = &mut PropertyAtomicGuard::none();
+
+    // "CHAT" label on the left, vertically centered.
+    let node = create_text("feed_label");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_default_f32(0, FEED_HEADER_PAD).unwrap();
+    prop.set_default_f32(1, (FEED_HEADER_HEIGHT - FEED_HEADER_FONTSIZE) / 2.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_f32(3, FEED_HEADER_FONTSIZE).unwrap();
+    node.get_property("font_size").unwrap().set_default_f32(0, FEED_HEADER_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "CHAT").unwrap();
+    node.set_property_bool(atom, Role::App, "use_i18n", false).unwrap();
+    wire_color(&node, "text_color", theme, "text_color")?;
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
+        .await;
+    parent.link(node);
+
+    // Down arrow on the right, vertically centered.
+    let node = create_vector_art("feed_arrow");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - FEED_ARROW_SCALE - FEED_HEADER_PAD")?;
+    prop.set_default_expr(0, code)?;
+    prop.set_default_f32(1, FEED_ARROW_Y)?;
+    prop.set_default_f32(2, FEED_ARROW_SCALE)?;
+    prop.set_default_f32(3, FEED_ARROW_SCALE)?;
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    // Chevron arrow: two lines from the apex to the wings; the rolled
+    // strip flips the y values so it points up. Native space is ~30
+    // units wide; scaled to FEED_ARROW_SCALE.
+    let (apex, left, right) = if arrow_up {
+        (Point::new(-0., -12.), Point::new(-15., 7.5), Point::new(15., 7.5))
+    } else {
+        (Point::new(-0., 12.), Point::new(-15., -7.5), Point::new(15., -7.5))
+    };
+    let mut shape = VectorShape::new();
+    shape.add_line(apex, left, 1., rgba!(0x00F0FFff));
+    shape.add_line(apex, right, 1., rgba!(0x00F0FFff));
+    let shape = shape.scaled(FEED_ARROW_SCALE / 30.);
+    node.set_property_shape(atom, Role::App, "shape", shape)?;
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    parent.link(node);
+
+    // The toggle button covers the entire header. Priority above the
+    // chatview (0) so header taps don't reach the feed view beneath.
+    let node = create_button("hide_feed");
+    node.set_property_bool(atom, Role::App, "is_active", true)?;
+    let prop = node.get_property("rect").unwrap();
+    prop.set_default_f32(0, 0.)?;
+    prop.set_default_f32(1, 0.)?;
+    prop.set_default_expr(2, expr::load_var("w"))?;
+    prop.set_default_f32(3, FEED_HEADER_HEIGHT)?;
+    node.set_property_u32(atom, Role::App, "z_index", 3)?;
+    node.set_property_u32(atom, Role::App, "priority", 1)?;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    parent.link(node.clone());
+
+    Ok(node)
+}

+ 42 - 0
bin/app/src/theme/light.rs

@@ -32,6 +32,7 @@ use crate::{
         schema::{
             chat::ui_consts::{BACKARROW_SCALE, EMOJI_CLOSE_SCALE, EMOJI_SCALE},
             menu::ui_consts::{MENU_ICON_SCALE, SETTINGS_ICON_SCALE},
+            wallet::data::FEED_HEADER_HEIGHT,
         },
     },
     error::{Error, Result},
@@ -72,6 +73,7 @@ async fn light_apply(ctx: &ThemeCtx) -> Result<()> {
         //("menu.role1_color", [0.40, 0.40, 0.40, 1.]),
         //("menu.role2_color", [0.25, 0.25, 0.25, 1.]),
         ("chatview.timestamp_color", [0.45, 0.45, 0.45, 1.]),
+        ("chatview.channel_color", [0.45, 0.45, 0.45, 1.]),
         ("chatview.text_color", [0.08, 0.08, 0.08, 1.]),
         ("chatview.hi_bg_color", [0.75, 0.75, 0.75, 1.]),
         ("chatview.action_text_color", [0.20, 0.20, 0.20, 1.]),
@@ -85,6 +87,46 @@ async fn light_apply(ctx: &ThemeCtx) -> Result<()> {
 
     let sg_root = ctx.sg_root();
 
+    // Feed layers on the wallet main screen: slightly darker than the
+    // white baseline (single color; the header band a shade darker
+    // still — drawn inside the one bg shape per layer, header band
+    // included). Journaled as touched so unload reverts to the
+    // schema's minimal defaults.
+    //
+    // Expanded layer: body + header band. Rolled strip: the whole
+    // strip is header.
+    let feed_bg = sg_root.lookup_node("/window/content/wallet/main_layer/feed_layer/bg").unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [0.87, 0.87, 0.87, 1.],
+    );
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(FEED_HEADER_HEIGHT),
+        [0.82, 0.82, 0.82, 1.],
+    );
+    let prop = feed_bg.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    let rolled_bg =
+        sg_root.lookup_node("/window/content/wallet/main_layer/feed_rolled_layer/bg").unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [0.82, 0.82, 0.82, 1.],
+    );
+    let prop = rolled_bg.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
     let node = sg_root.lookup_node("/window/content/wallet/main_layer/chat_btn_shape").unwrap();
     let color = rgba!(0xF41740ff);
     let mut shape = shape::create_netlogo1(color);

+ 78 - 20
bin/app/src/theme/scifi.rs

@@ -32,13 +32,14 @@ use crate::{
     app::{
         node::{create_vector_art, create_video},
         schema::{
-            wallet::data::{CHAT_BTN_MARGIN, CHAT_BTN_SIZE},
+            wallet::data::{CHAT_BTN_MARGIN, CHAT_BTN_SIZE, FEED_HEADER_HEIGHT},
             MAIN_MENU_HEADER_HEIGHT, VID_ASPECT_RATIO, VID_PATH,
         },
     },
     error::{Error, Result},
     expr::{self, Compiler},
     gfx::gfxtag,
+    mesh::rgba,
     prop::{
         Property, PropertyAtomicGuard, PropertyFloat32, PropertyPermission, PropertySubType,
         PropertyType, PropertyValue, Role,
@@ -117,11 +118,10 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
     // Private-token proof: the netstatus overlay's P2P/OUTBOUND labels
     // follow `label_glow` through journaled edges (removed on unload).
     for label in ["p2p_label", "outbound_label"] {
-        let Some(node) =
-            ctx.sg_root().lookup_node(&format!("/window/content/chat/netstatus_overlay/{label}"))
-        else {
-            continue;
-        };
+        let node = ctx
+            .sg_root()
+            .lookup_node(&format!("/window/content/chat/netstatus_overlay/{label}"))
+            .unwrap();
         let Some(prop) = node.get_property("text_color") else { continue };
         for i in 0..4 {
             let local = format!("th_label_glow_{i}");
@@ -161,10 +161,16 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
     king_video_node(ctx).await;
     bg_fade_node(ctx, "/window/content/chat/main_chat_layer", "bg").await?;
     bg_fade_node(ctx, "/window/content/wallet/main_layer", "wallet_bg").await?;
-
-    // Feed layer bg on the main wallet screen: restyle the schema's
-    // neutral fill with the scifi dark teal.
-    if let Some(node) = ctx.sg_root().lookup_node("/window/content/wallet/main_layer/feed_layer/bg")
+    // Feed bgs on the main wallet screen — everything (header band
+    // included) renders in the one bg shape per layer, journaled so
+    // unload reverts to the schema defaults:
+    // - feed_layer: teal body fill, header gradient in the top
+    //   FEED_HEADER_HEIGHT band, one cyan outline around it all.
+    // - rolled strip: the whole strip is header — gradient across it,
+    //   same outline.
+    // The "CHAT" labels get the scifi mint.
+    let node =
+        ctx.sg_root().lookup_node("/window/content/wallet/main_layer/feed_layer/bg").unwrap();
     {
         let mut shape = VectorShape::new();
         shape.add_filled_box(
@@ -172,10 +178,65 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
             expr::const_f32(0.),
             expr::load_var("w"),
             expr::load_var("h"),
-            [0., 0.09, 0.106, 1.],
+            rgba!(0x14212499),
         );
-        node.set_property_shape(&mut PropertyAtomicGuard::none(), Role::Theme, "shape", shape)
-            .unwrap();
+        shape.add_smooth_vertical_gradient(
+            expr::const_f32(0.),
+            expr::const_f32(0.),
+            expr::load_var("w"),
+            expr::const_f32(FEED_HEADER_HEIGHT),
+            rgba!(0x0b4237ff),
+            rgba!(0x0c1d20ff),
+            8,
+            0.2,
+        );
+        shape.add_outline(
+            expr::const_f32(0.),
+            expr::const_f32(0.),
+            expr::load_var("w"),
+            expr::load_var("h"),
+            1.,
+            rgba!(0x00F0FFff),
+        );
+        let prop = node.get_property("shape").unwrap();
+        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(std::sync::Arc::new(shape)))?;
+    }
+    let node = ctx
+        .sg_root()
+        .lookup_node("/window/content/wallet/main_layer/feed_rolled_layer/bg")
+        .unwrap();
+    {
+        let mut shape = VectorShape::new();
+        shape.add_smooth_vertical_gradient(
+            expr::const_f32(0.),
+            expr::const_f32(0.),
+            expr::load_var("w"),
+            expr::load_var("h"),
+            rgba!(0x0b4237ff),
+            rgba!(0x0c1d20ff),
+            8,
+            0.2,
+        );
+        shape.add_outline(
+            expr::const_f32(0.),
+            expr::const_f32(0.),
+            expr::load_var("w"),
+            expr::load_var("h"),
+            1.,
+            rgba!(0x00F0FFff),
+        );
+        let prop = node.get_property("shape").unwrap();
+        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(std::sync::Arc::new(shape)))?;
+    }
+    for path in [
+        "/window/content/wallet/main_layer/feed_layer",
+        "/window/content/wallet/main_layer/feed_rolled_layer",
+    ] {
+        let Some(layer) = ctx.sg_root().lookup_node(path) else { continue };
+        if let Some(node) = layer.lookup_node("/feed_label") {
+            let prop = node.get_property("text_color").unwrap();
+            ctx.set_touched_color(atom, &prop, rgba!(0x77FFBEff))?;
+        }
     }
 
     // Bg for wallet btn to navigate to chat screen
@@ -187,7 +248,7 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
     prop.set_default_expr(1, code).unwrap();
     prop.set_default_f32(2, CHAT_BTN_SIZE).unwrap();
     prop.set_default_f32(3, CHAT_BTN_SIZE).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let mut shape = VectorShape::new();
 
     shape.add_filled_box(
@@ -220,12 +281,9 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
     //    the ctx (touched-set dedups the per-step journal entries).
     //    Replaces the in-handler fade from the reconnect click handler.
     // ------------------------------------------------------------------
-    let overlay = ctx
-        .sg_root()
-        .lookup_node("/window/content/chat/netstatus_overlay")
-        .ok_or(Error::NodeNotFound)?;
-    let is_visible = overlay.get_property("is_visible").ok_or(Error::PropertyNotFound)?;
-    let alpha = overlay.get_property("alpha").ok_or(Error::PropertyNotFound)?;
+    let overlay = ctx.sg_root().lookup_node("/window/content/chat/netstatus_overlay").unwrap();
+    let is_visible = overlay.get_property("is_visible").unwrap();
+    let alpha = overlay.get_property("alpha").unwrap();
     let sub = is_visible.subscribe_modify();
     let redraw = ctx.redraw().clone();
     let ctx2 = ThemeCtx::clone_shallow(ctx);