% intro_mode = Current.user&.ui_layout_intro? %>
<% home_path = intro_mode ? chats_path : root_path %>
<% mobile_nav_items = if intro_mode
[
{ name: t(".nav.home"), path: chats_path, icon: "home", icon_custom: false, active: page_active?(chats_path) },
{ name: "Intro", path: intro_path, icon: "sparkles", icon_custom: false, active: page_active?(intro_path) }
]
else
[
{ name: t(".nav.home"), path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: t(".nav.transactions"), path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: t(".nav.reports"), path: reports_path, icon: "chart-bar", icon_custom: false, active: page_active?(reports_path) },
{ name: t(".nav.budgets"), path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
preview_gated_nav_item({ name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path) }),
{ name: t(".nav.assistant"), path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
].compact
end %>
<% desktop_nav_items = mobile_nav_items.reject { |item| item[:mobile_only] } %>
<% expanded_sidebar_class = "w-full" %>
<% collapsed_sidebar_class = "w-0 overflow-hidden" %>
<%= render "layouts/shared/htmldoc" do %>
<%= link_to t("layouts.application.skip_to_main"), "#main",
class: "sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:rounded-lg focus:bg-container focus:text-primary focus:shadow-border-xs" %>
<% unless intro_mode %>
<%= icon("x", as_button: true, data: { action: "app-layout#closeMobileSidebar" }) %>
<% end %>
<%= render(
"accounts/account_sidebar_tabs",
family: Current.family,
active_tab: @account_group_tab,
mobile: true
) %>
<%# MOBILE - Top nav %>
<% if intro_mode %>
<% else %>
<%= icon("panel-left", as_button: true, data: { action: "app-layout#openMobileSidebar"}) %>
<% end %>
<%= link_to home_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
"
aria-label="<%= t("layouts.application.privacy_mode") %>"
aria-pressed="false">
<%= icon("eye-off", size: "sm", data: { privacy_mode_target: "iconOff" }) %>
<%= icon("eye", size: "sm", class: "hidden", data: { privacy_mode_target: "iconOn" }) %>
<%= render "users/user_menu", user: Current.user, placement: "bottom-end", offset: 12, intro_mode: intro_mode %>
<%# DESKTOP - Left navbar %>
<%= link_to home_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
<% desktop_nav_items.reject { |item| item[:mobile_only] }.each do |nav_item| %>
<%= render "layouts/shared/nav_item", **nav_item %>
<% end %>
<%= render DS::Link.new(
variant: "icon",
icon: "message-circle-question",
href: "https://discord.gg/36ZGBsxYEK",
target: "_blank"
) %>
<%= render "users/user_menu", user: Current.user, intro_mode: intro_mode %>
<%# DESKTOP - Left sidebar %>
<%= tag.div class: class_names(
"hidden lg:block relative py-4 shrink-0 transition-all duration-300 border-divider",
Current.user.show_sidebar? ? [expanded_sidebar_class, "border-r"] : [collapsed_sidebar_class, "border-r-0"],
),
style: "max-width: var(--left-sidebar-width, 320px)",
inert: !Current.user.show_sidebar?,
data: { app_layout_target: "leftSidebar", sidebar_resize_target: "leftSidebar" } do %>
<%= render "layouts/shared/sidebar_resize_handle", side: "left" %>
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_tab: @account_group_tab %>
<% if Current.family.trialing? && !self_hosted? %>
<%= t("layouts.trial.open_demo") %>
<%= t("layouts.trial.data_deleted_in_days", days: Current.family.days_left_in_trial) %>
<%= render DS::Link.new(
text: t("layouts.trial.contribute"),
href: upgrade_subscription_path,
) %>
<% end %>
<% end %>
<% end %>
<%# SHARED - Main content %>
<%= tag.main id: "main", class: class_names("grow overflow-y-auto px-3 lg:px-10 w-full mx-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0"), data: { app_layout_target: "content", viewport_target: "content" } do %>
<% unless intro_mode %>
<%= icon("panel-left", as_button: true, data: { action: "app-layout#toggleLeftSidebar" }) %>
<% if content_for?(:breadcrumbs) %>
<%= yield :breadcrumbs %>
<% else %>
<%= render "layouts/shared/breadcrumbs", breadcrumbs: breadcrumbs %>
<% end %>
<%# Preview-gated. Gating the whole block (rather than just the
link) also keeps the unread COUNT off every page render for
users who haven't opted in. No preview dot on the icon: the
unread badge already occupies that corner. %>
<% if Current.family && preview_features_enabled? %>
<% unread_insights_count = Current.family.insights.active.count %>
<%# Prefetch disabled: /insights marks insights read on real visits
and deliberately skips prefetch requests, so a prefetched
response served on click would never clear the badge. %>
<%= link_to insights_path,
class: "relative inline-flex items-center justify-center w-9 h-9 pointer-coarse:w-11 pointer-coarse:h-11 rounded-lg text-secondary hover:text-primary hover:bg-container-inset-hover transition-colors focus-ring",
title: t("layouts.application.insights"),
aria: { label: t("layouts.application.insights") },
data: { turbo_prefetch: false } do %>
<%= icon("lightbulb", size: "sm") %>
<% if unread_insights_count.positive? %>
<%= unread_insights_count > 9 ? "9+" : unread_insights_count %>
<% end %>
<% end %>
<% end %>
"
aria-label="<%= t("layouts.application.privacy_mode") %>"
aria-pressed="false">
<%= icon("eye-off", size: "sm", data: { privacy_mode_target: "iconOff" }) %>
<%= icon("eye", size: "sm", class: "hidden", data: { privacy_mode_target: "iconOn" }) %>
<%= icon("panel-right", as_button: true, data: { action: "app-layout#toggleRightSidebar" }) %>
<% end %>
<% if content_for?(:page_header) %>
<%= yield :page_header %>
<% end %>
<%= yield %>
<% end %>
<%# DESKTOP - Right sidebar %>
<%= tag.div class: class_names(
"hidden lg:block relative h-full shrink-0 transition-all duration-300 border-divider",
Current.user.show_ai_sidebar? ? [expanded_sidebar_class, "border-l"] : [collapsed_sidebar_class, "border-l-0"],
),
style: "max-width: var(--right-sidebar-width, 400px)",
inert: !Current.user.show_ai_sidebar?,
data: { app_layout_target: "rightSidebar", sidebar_resize_target: "rightSidebar" } do %>
<%= render "layouts/shared/sidebar_resize_handle", side: "right" %>
<%= tag.div id: "chat-container", class: "relative h-full px-4 overflow-y-auto", data: { controller: "chat hotkey", turbo_permanent: true } do %>
<%= turbo_frame_tag chat_frame, src: chat_view_path(@chat), loading: "lazy", class: "h-full" do %>
<%= icon("loader-circle", class: "animate-spin") %>
<% end %>
<% unless Current.user.ai_enabled? %>
<%= render "chats/ai_consent" %>
<% end %>
<% end %>
<% end %>
<%# MOBILE - Bottom Nav %>
<%= tag.nav class: "lg:hidden fixed bottom-0 left-0 right-0 bg-surface z-10 border-t border-tertiary flex justify-around pb-[env(safe-area-inset-bottom)]",
data: { viewport_target: "bottomNav" } do %>
<% mobile_nav_items.each do |nav_item| %>
<%= render "layouts/shared/nav_item", **nav_item %>
<% end %>
<% end %>
<% end %>