commit 13d1a073c1f0e2ac4c9fc42daef44bfd9c035816
parent 95c96717c1f100115a5e70bf7bcc729d60042df1
Author: triesap <tyson@radroots.org>
Date: Fri, 17 Apr 2026 16:47:10 +0000
ui: add shared shell theme tokens
- split radroots_app_ui into theme, primitives, and placeholder modules
- move window sizing, surface colors, and typography into APP_UI_THEME
- render the placeholder shell through shared window and center-stage primitives
- point the desktop launcher at shared ui window tokens for startup sizing
Diffstat:
6 files changed, 227 insertions(+), 44 deletions(-)
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -1,6 +1,6 @@
use gpui::{AppContext, Application, WindowOptions, px, size};
-use radroots_app_core::{APP_ID, HOME_WINDOW_METRICS};
-use radroots_app_ui::{HOME_WINDOW_MIN_HEIGHT_PX, HOME_WINDOW_MIN_WIDTH_PX, PlaceholderView};
+use radroots_app_core::APP_ID;
+use radroots_app_ui::{APP_UI_THEME, PlaceholderView};
fn titlebar_options() -> gpui::TitlebarOptions {
gpui::TitlebarOptions {
@@ -26,8 +26,8 @@ pub fn launch() {
WindowOptions {
app_id: Some(APP_ID.to_owned()),
window_min_size: Some(size(
- px(HOME_WINDOW_METRICS.min_width_px.max(HOME_WINDOW_MIN_WIDTH_PX)),
- px(HOME_WINDOW_METRICS.min_height_px.max(HOME_WINDOW_MIN_HEIGHT_PX)),
+ px(APP_UI_THEME.windows.home_min_width_px),
+ px(APP_UI_THEME.windows.home_min_height_px),
)),
titlebar: Some(titlebar_options()),
..Default::default()
diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs
@@ -2,14 +2,3 @@
pub const APP_ID: &str = "org.radroots.app";
pub const APP_NAME: &str = "radroots";
-
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub struct AppWindowMetrics {
- pub min_width_px: f32,
- pub min_height_px: f32,
-}
-
-pub const HOME_WINDOW_METRICS: AppWindowMetrics = AppWindowMetrics {
- min_width_px: 640.0,
- min_height_px: 480.0,
-};
diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs
@@ -1,32 +1,15 @@
#![forbid(unsafe_code)]
-use gpui::{
- Context, FontWeight, IntoElement, ParentElement, Render, Styled, Window, div, px, rgb,
+mod placeholder;
+mod primitives;
+mod theme;
+
+pub use placeholder::PlaceholderView;
+pub use primitives::{
+ LabelValueRow, app_card, app_center_stage, app_window_shell, label_value_list, section_divider,
+ utility_title_row,
+};
+pub use theme::{
+ APP_UI_THEME, AppLayoutTokens, AppSurfaceTokens, AppTextTokens, AppTypographyTokens,
+ AppUiTheme, AppWindowTokens,
};
-use radroots_app_i18n::{AppTextKey, app_text};
-
-pub const HOME_WINDOW_MIN_WIDTH_PX: f32 = 640.0;
-pub const HOME_WINDOW_MIN_HEIGHT_PX: f32 = 480.0;
-pub const WINDOW_BACKGROUND: u32 = 0xF5F1E8;
-pub const TEXT_PRIMARY: u32 = 0x1F2C23;
-pub const BRAND_TEXT_SIZE_PX: f32 = 20.0;
-
-pub struct PlaceholderView;
-
-impl Render for PlaceholderView {
- fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
- div()
- .size_full()
- .flex()
- .items_center()
- .justify_center()
- .bg(rgb(WINDOW_BACKGROUND))
- .child(
- div()
- .text_size(px(BRAND_TEXT_SIZE_PX))
- .font_weight(FontWeight::SEMIBOLD)
- .text_color(rgb(TEXT_PRIMARY))
- .child(app_text(AppTextKey::Brand)),
- )
- }
-}
diff --git a/crates/shared/ui/src/placeholder.rs b/crates/shared/ui/src/placeholder.rs
@@ -0,0 +1,21 @@
+use gpui::{Context, FontWeight, IntoElement, ParentElement, Render, Styled, Window, div, px, rgb};
+use radroots_app_i18n::{AppTextKey, app_text};
+
+use crate::{APP_UI_THEME, app_center_stage, app_window_shell};
+
+pub struct PlaceholderView;
+
+impl Render for PlaceholderView {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ app_window_shell(
+ APP_UI_THEME.surfaces.window_background,
+ app_center_stage(
+ div()
+ .text_size(px(APP_UI_THEME.typography.brand_text_px))
+ .font_weight(FontWeight::SEMIBOLD)
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(app_text(AppTextKey::Brand)),
+ ),
+ )
+ }
+}
diff --git a/crates/shared/ui/src/primitives.rs b/crates/shared/ui/src/primitives.rs
@@ -0,0 +1,93 @@
+use gpui::{IntoElement, ParentElement, SharedString, Styled, div, px, rgb};
+
+use crate::APP_UI_THEME;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct LabelValueRow {
+ pub label: SharedString,
+ pub value: SharedString,
+}
+
+impl LabelValueRow {
+ pub fn new(label: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ value: value.into(),
+ }
+ }
+}
+
+pub fn app_window_shell(background: u32, content: impl IntoElement) -> impl IntoElement {
+ div()
+ .size_full()
+ .overflow_hidden()
+ .bg(rgb(background))
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(content)
+}
+
+pub fn app_center_stage(content: impl IntoElement) -> impl IntoElement {
+ div()
+ .size_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .p(px(APP_UI_THEME.layout.home_window_padding_px))
+ .child(content)
+}
+
+pub fn app_card(content: impl IntoElement) -> impl IntoElement {
+ div()
+ .w_full()
+ .max_w(px(APP_UI_THEME.layout.home_card_max_width_px))
+ .mx_auto()
+ .bg(rgb(APP_UI_THEME.surfaces.card_background))
+ .overflow_hidden()
+ .child(
+ div()
+ .w_full()
+ .p(px(APP_UI_THEME.layout.home_card_padding_px))
+ .child(content),
+ )
+}
+
+pub fn section_divider() -> impl IntoElement {
+ div()
+ .w_full()
+ .h(px(APP_UI_THEME.layout.divider_thickness_px))
+ .bg(rgb(APP_UI_THEME.surfaces.divider))
+}
+
+pub fn utility_title_row(title: impl Into<SharedString>) -> impl IntoElement {
+ div()
+ .w_full()
+ .h(px(APP_UI_THEME.layout.utility_title_row_height_px))
+ .flex()
+ .justify_center()
+ .items_center()
+ .text_size(px(APP_UI_THEME.typography.utility_title_text_px))
+ .font_weight(gpui::FontWeight::BOLD)
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(title.into())
+}
+
+pub fn label_value_list(rows: impl IntoIterator<Item = LabelValueRow>) -> impl IntoElement {
+ let rows = rows
+ .into_iter()
+ .map(|row| {
+ let line = format!("{}: {}", row.label, row.value);
+ div()
+ .w_full()
+ .text_size(px(APP_UI_THEME.typography.body_text_px))
+ .text_color(rgb(APP_UI_THEME.text.secondary))
+ .child(line)
+ })
+ .collect::<Vec<_>>();
+
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME.layout.metadata_row_gap_px))
+ .children(rows)
+}
diff --git a/crates/shared/ui/src/theme.rs b/crates/shared/ui/src/theme.rs
@@ -0,0 +1,97 @@
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppUiTheme {
+ pub windows: AppWindowTokens,
+ pub surfaces: AppSurfaceTokens,
+ pub text: AppTextTokens,
+ pub typography: AppTypographyTokens,
+ pub layout: AppLayoutTokens,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppWindowTokens {
+ pub home_min_width_px: f32,
+ pub home_min_height_px: f32,
+ pub settings_width_px: f32,
+ pub settings_height_px: f32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppSurfaceTokens {
+ pub window_background: u32,
+ pub chrome_background: u32,
+ pub panel_background: u32,
+ pub card_background: u32,
+ pub divider: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppTextTokens {
+ pub primary: u32,
+ pub secondary: u32,
+ pub accent: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppTypographyTokens {
+ pub utility_title_text_px: f32,
+ pub body_text_px: f32,
+ pub brand_text_px: f32,
+ pub settings_row_text_px: f32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppLayoutTokens {
+ pub divider_thickness_px: f32,
+ pub home_window_padding_px: f32,
+ pub home_card_max_width_px: f32,
+ pub home_card_padding_px: f32,
+ pub home_stack_gap_px: f32,
+ pub metadata_row_gap_px: f32,
+ pub utility_title_row_height_px: f32,
+ pub settings_chrome_height_px: f32,
+ pub settings_section_gap_px: f32,
+ pub settings_navigation_row_padding_px: f32,
+ pub settings_navigation_row_gap_px: f32,
+ pub settings_content_padding_px: f32,
+}
+
+pub const APP_UI_THEME: AppUiTheme = AppUiTheme {
+ windows: AppWindowTokens {
+ home_min_width_px: 640.0,
+ home_min_height_px: 480.0,
+ settings_width_px: 600.0,
+ settings_height_px: 540.0,
+ },
+ surfaces: AppSurfaceTokens {
+ window_background: 0xF5F1E8,
+ chrome_background: 0xEAE5D8,
+ panel_background: 0xF8F4EC,
+ card_background: 0xEFE8D8,
+ divider: 0xD4CCBA,
+ },
+ text: AppTextTokens {
+ primary: 0x1F2C23,
+ secondary: 0x5D665B,
+ accent: 0x3B6A3E,
+ },
+ typography: AppTypographyTokens {
+ utility_title_text_px: 12.0,
+ body_text_px: 14.0,
+ brand_text_px: 20.0,
+ settings_row_text_px: 13.0,
+ },
+ layout: AppLayoutTokens {
+ divider_thickness_px: 1.0,
+ home_window_padding_px: 24.0,
+ home_card_max_width_px: 960.0,
+ home_card_padding_px: 24.0,
+ home_stack_gap_px: 12.0,
+ metadata_row_gap_px: 12.0,
+ utility_title_row_height_px: 24.0,
+ settings_chrome_height_px: 88.0,
+ settings_section_gap_px: 8.0,
+ settings_navigation_row_padding_px: 8.0,
+ settings_navigation_row_gap_px: 8.0,
+ settings_content_padding_px: 24.0,
+ },
+};