Browse / github.com/storytold/artcraft / patch
[skip netlify] Align ArtCraft launcher dark roles across native and web
[skip netlify] Align ArtCraft launcher dark roles across native and web The launcher supplied the first-party colors used for the suite-wide standard dark theme, but its active web CSS and native widget states still differed on inputs, selected rows, borders and blue-button foregrounds. This aligns those roles with the same palette while leaving the launcher layout and branding intact. Validation: active web CSS role check, native formatting and 63 native tests passed (one existing ignored test). A backend-free source-component render also passed and was visually inspected; it does not start the launcher backend. Repository-wide checks remain pending. The `s
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- pourie-he (imported from pull request #2290, open)
- Base
d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373- Size
- +404 −50 in 11 files
- Review
- pending
- Shared
- 2026-10-28
- Id
6e7aab40460de17bb0fd96d3
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock artcraft-native/Cargo.toml artcraft-native/src/feed/grid.rs artcraft-native/src/main.rs artcraft-native/src/overlays/library_picker.rs artcraft-native/src/prompt_box/deck.rs artcraft-native/src/shell.rs artcraft-native/src/theme.rs artcraft-native/src/theme_render_tests.rs artcraft-native/src/ui/widgets.rs frontend/apps/artcraft/app/src/styles/base.css
@@ -491,6 +491,7 @@ dependencies = [ "chrono", "cookie", "cookie_store_wrapper",+ "craft-ui", "directories", "eframe", "egui",@@ -553,6 +554,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]]+name = "ash"+version = "0.38.0+1.3.281"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"+dependencies = [+ "libloading 0.8.6",+]+ [[package]] name = "ashpd" version = "0.11.1"@@ -1554,6 +1564,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [+ "serde",+ "termcolor", "unicode-width", ] @@ -1812,6 +1824,14 @@ dependencies = [ "libc", ] +[[package]]+name = "craft-ui"+version = "0.1.0"+source = "git+https://github.com/storytold/craft-libs?rev=8134693a69e0fca8ec7913ad5a95dedc05daad74#8134693a69e0fca8ec7913ad5a95dedc05daad74"+dependencies = [+ "egui",+]+ [[package]] name = "crc" version = "3.0.1"@@ -2352,6 +2372,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding",+ "pollster 1.0.1", "profiling", "raw-window-handle", "ron",@@ -2360,6 +2381,7 @@ dependencies = [ "wasm-bindgen", "web-sys", "web-time",+ "wgpu", "windows-sys 0.61.2", "winit", ]@@ -2471,11 +2493,16 @@ version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eb3f3effb2726fee23449293e67acbf6cc6ce35525b1722bd5743bb348114d5" dependencies = [+ "eframe", "egui",+ "egui-wgpu",+ "image", "kittest", "log",+ "pollster 1.0.1", "serde", "toml 1.1.8+spec-1.1.0",+ "wgpu", ] [[package]]@@ -3569,6 +3596,20 @@ dependencies = [ "system-deps", ] +[[package]]+name = "gpu-allocator"+version = "0.28.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"+dependencies = [+ "ash",+ "hashbrown 0.16.1",+ "log",+ "presser",+ "thiserror 2.0.18",+ "windows 0.54.0",+]+ [[package]] name = "grok_api_client" version = "0.0.1"@@ -3763,6 +3804,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [+ "allocator-api2",+ "equivalent", "foldhash 0.2.0", ] @@ -4608,6 +4651,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]]+name = "khronos-egl"+version = "6.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"+dependencies = [+ "libc",+ "libloading 0.8.6",+ "pkg-config",+]+ [[package]] name = "khronos_api" version = "3.1.0"@@ -5141,6 +5195,7 @@ dependencies = [ "num-traits", "once_cell", "rustc-hash 1.1.0",+ "spirv", "thiserror 2.0.18", "unicode-ident", ]@@ -5516,7 +5571,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2",- "objc2-metal",+ "objc2-metal 0.2.2", ] [[package]]@@ -5639,6 +5694,18 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]]+name = "objc2-metal"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"+dependencies = [+ "bitflags 2.13.2",+ "block2 0.6.2",+ "objc2 0.6.4",+ "objc2-foundation 0.3.2",+]+ [[package]] name = "objc2-quartz-core" version = "0.2.2"@@ -5649,7 +5716,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2",- "objc2-metal",+ "objc2-metal 0.2.2", ] [[package]]@@ -5661,7 +5728,9 @@ dependencies = [ "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation",+ "objc2-core-graphics", "objc2-foundation 0.3.2",+ "objc2-metal 0.3.2", ] [[package]]@@ -5923,6 +5992,15 @@ dependencies = [ "num-traits", ] +[[package]]+name = "ordered-float"+version = "5.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"+dependencies = [+ "num-traits",+]+ [[package]] name = "ordered-stream" version = "0.2.0"@@ -6278,6 +6356,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]]+name = "pollster"+version = "1.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336"+ [[package]] name = "polycool" version = "0.4.0"@@ -6320,6 +6404,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]]+name = "presser"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"+ [[package]] name = "pretty_assertions" version = "1.4.1"@@ -6681,6 +6771,12 @@ dependencies = [ "getrandom 0.3.1", ] +[[package]]+name = "range-alloc"+version = "0.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"+ [[package]] name = "rav1e" version = "0.7.1"@@ -6737,6 +6833,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]]+name = "raw-window-metal"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"+dependencies = [+ "objc2 0.6.4",+ "objc2-core-foundation",+ "objc2-foundation 0.3.2",+ "objc2-quartz-core 0.3.2",+]+ [[package]] name = "rayon" version = "1.10.0"@@ -7000,7 +7108,7 @@ dependencies = [ "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2",- "pollster",+ "pollster 0.4.0", "raw-window-handle", "urlencoding", "wasm-bindgen",@@ -7542,7 +7650,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [- "ordered-float",+ "ordered-float 2.10.1", "serde", ] @@ -8037,6 +8145,15 @@ dependencies = [ "lock_api", ] +[[package]]+name = "spirv"+version = "0.4.0+sdk-1.4.341.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"+dependencies = [+ "bitflags 2.13.2",+]+ [[package]] name = "spki" version = "0.7.3"@@ -9110,6 +9227,15 @@ dependencies = [ "utf-8", ] +[[package]]+name = "termcolor"+version = "1.4.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"+dependencies = [+ "winapi-util",+]+ [[package]] name = "test_data" version = "0.0.1"@@ -10587,6 +10713,8 @@ dependencies = [ "hashbrown 0.17.1", "js-sys", "log",+ "naga",+ "parking_lot", "portable-atomic", "profiling", "raw-window-handle",@@ -10626,12 +10754,42 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.18",+ "wgpu-core-deps-apple",+ "wgpu-core-deps-emscripten",+ "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", "wgpu-types", ] +[[package]]+name = "wgpu-core-deps-apple"+version = "30.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"+dependencies = [+ "wgpu-hal",+]++[[package]]+name = "wgpu-core-deps-emscripten"+version = "30.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"+dependencies = [+ "wgpu-hal",+]++[[package]]+name = "wgpu-core-deps-wasm"+version = "30.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae"+dependencies = [+ "wgpu-hal",+]+ [[package]] name = "wgpu-core-deps-windows-linux-android" version = "30.0.1"@@ -10647,22 +10805,54 @@ version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" dependencies = [+ "android_system_properties",+ "arrayvec",+ "ash",+ "bit-set 0.10.0", "bitflags 2.13.2",+ "block2 0.6.2",+ "bytemuck", "cfg-if", "cfg_aliases",+ "glow",+ "glutin_wgl_sys",+ "gpu-allocator", "hashbrown 0.17.1",+ "js-sys",+ "khronos-egl",+ "libc", "libloading 0.8.6", "log", "naga", "naga-types",+ "ndk-sys 0.6.0+11769913",+ "objc2 0.6.4",+ "objc2-core-foundation",+ "objc2-core-graphics",+ "objc2-foundation 0.3.2",+ "objc2-metal 0.3.2",+ "objc2-quartz-core 0.3.2",+ "once_cell",+ "ordered-float 5.5.0",+ "parking_lot", "portable-atomic", "portable-atomic-util",+ "profiling",+ "range-alloc", "raw-window-handle",+ "raw-window-metal", "renderdoc-sys",+ "smallvec", "static_assertions", "thiserror 2.0.18",+ "wasm-bindgen",+ "wayland-sys",+ "web-sys", "wgpu-naga-bridge", "wgpu-types",+ "windows 0.62.2",+ "windows-core 0.62.2",+ "windows-result 0.4.1", ] [[package]]@@ -10,6 +10,7 @@ name = "artcraft-native" path = "src/main.rs" [dependencies]+craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "8134693a69e0fca8ec7913ad5a95dedc05daad74" } # Internal (Tauri-free) API crates shared with the Tauri app. artcraft_api_defs.workspace = true artcraft_client.workspace = true@@ -50,4 +51,4 @@ arboard = { version = "3", default-features = false, features = ["image-data"] } wry = "0.55" [dev-dependencies]-egui_kittest = "0.36"+egui_kittest = { version = "0.36", features = ["wgpu"] }@@ -259,7 +259,7 @@ fn item_card(ui: &mut Ui, view: &FeedView<'_>, rect: Rect, item: &FeedItem, cach let (fill, border) = if selected { (theme::ACCENT_400, theme::ACCENT_400) } else { (Color32::from_black_alpha(102), theme::fade(Color32::WHITE, 0.6)) }; ui.painter().rect(chip, theme::RADIUS, fill, Stroke::new(1.0, border), egui::StrokeKind::Inside); if selected {- icons::paint(ui.painter(), chip.shrink(4.0), Icon::Check, Color32::WHITE);+ icons::paint(ui.painter(), chip.shrink(4.0), Icon::Check, theme::ACCENT_ON); } } if view.last_viewed == Some(item.token.as_str()) {@@ -13,6 +13,9 @@ mod shell; mod theme; mod ui; +#[cfg(test)]+mod theme_render_tests;+ const APP_ID: &str = "ai.artcraft.native"; fn main() -> eframe::Result {@@ -189,7 +189,7 @@ fn tile_ui(ui: &mut egui::Ui, rect: Rect, item: &MediaFile, kind: RefKind, attac p.rect_stroke(rect, 0.0, Stroke::new(2.0, theme::ACCENT_400), egui::StrokeKind::Inside); let chip = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(22.0, 22.0)); p.rect_filled(chip, theme::RADIUS, theme::ACCENT_400);- p.text(chip.center(), Align2::CENTER_CENTER, (i + 1).to_string(), FontId::new(11.0, theme::semibold()), Color32::WHITE);+ p.text(chip.center(), Align2::CENTER_CENTER, (i + 1).to_string(), FontId::new(11.0, theme::semibold()), theme::ACCENT_ON); } else if resp.hovered() { p.rect_stroke(rect, 0.0, Stroke::new(2.0, theme::fade(theme::ACCENT_400, 0.6)), egui::StrokeKind::Inside); }@@ -117,7 +117,7 @@ pub fn reference_deck(ui: &mut Ui, id: Id, refs: &References, limits: &DeckLimit let badge_text = items.len().to_string(); let badge = Rect::from_center_size(pos2(stack.right() + 2.0, stack.top() + 2.0), vec2(8.0 + 7.0 * badge_text.len() as f32, 18.0)); painter.rect_filled(badge, CornerRadius::same(9), theme::ACCENT);- painter.text(badge.center(), Align2::CENTER_CENTER, badge_text, FontId::new(10.0, theme::semibold()), Color32::WHITE);+ painter.text(badge.center(), Align2::CENTER_CENTER, badge_text, FontId::new(10.0, theme::semibold()), theme::ACCENT_ON); } if !open_kinds.is_empty() { let plus = Rect::from_min_size(pos2(stack.right() - 12.0, stack.bottom() - 20.0), vec2(24.0, 24.0));@@ -280,7 +280,7 @@ fn account_row(ui: &mut Ui, name: &str, credits: Option<u64>) -> egui::Response let avatar = Rect::from_center_size(pos2(rect.left() + 22.0, rect.center().y), vec2(28.0, 28.0)); p.circle_filled(avatar.center(), 14.0, theme::ACCENT); let initial = name.chars().next().map(|c| c.to_uppercase().to_string()).unwrap_or_default();- p.text(avatar.center(), Align2::CENTER_CENTER, initial, FontId::new(13.0, theme::semibold()), Color32::WHITE);+ p.text(avatar.center(), Align2::CENTER_CENTER, initial, FontId::new(13.0, theme::semibold()), theme::ACCENT_ON); p.text(pos2(avatar.right() + 10.0, rect.center().y - 8.0), Align2::LEFT_CENTER, name, FontId::new(13.5, theme::medium()), theme::INK); let sub = credits.map_or_else(|| "Account".to_owned(), |c| format!("{} credits", group_thousands(c))); p.text(pos2(avatar.right() + 10.0, rect.center().y + 9.0), Align2::LEFT_CENTER, sub, FontId::new(11.5, egui::FontFamily::Proportional), theme::MUTED);@@ -1,10 +1,11 @@ //! Colours, type and shapes of the ArtCraft webapp's brutalist system-//! (`artcraft-services/frontend/apps/artcraft-webapp`): near-black surfaces, white/15 hairlines,+//! (`artcraft-services/frontend/apps/artcraft-webapp`): near-black surfaces, fine hairlines, //! Archivo display type, Inter body text, Geist Mono labels and 3 px corners on anything //! pressable. Large surfaces (panels, cards, media tiles) stay square. use std::sync::Arc; +use craft_ui::theme::SuitePalette; use egui::epaint::text::VariationCoords; use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, FontTweak, Stroke, TextStyle, Visuals}; @@ -19,51 +20,55 @@ const fn alpha(rgb: u32, a: u8) -> Color32 { } /// `ui-background`: the page behind the feed.-pub const BG: Color32 = hex(0x121316);+pub const BG: Color32 = SuitePalette::ARTCRAFT_DARK.window; /// `ui-panel`: the top bar, modals and the lightbox.-pub const PANEL: Color32 = hex(0x101014);+pub const PANEL: Color32 = SuitePalette::ARTCRAFT_DARK.panel; /// `ui-controls`: the sidebar, the prompt box and toolbar buttons.-pub const CONTROLS: Color32 = hex(0x1e1f22);+pub const CONTROLS: Color32 = SuitePalette::ARTCRAFT_DARK.raised; /// The window chrome behind the content panel: the title bar and the sidebar.-pub const CHROME: Color32 = hex(0x0a0a0c);+pub const CHROME: Color32 = SuitePalette::ARTCRAFT_DARK.chrome; /// `ui-sunken`: wells and inputs.-pub const SUNKEN: Color32 = hex(0x060607);+pub const SUNKEN: Color32 = SuitePalette::ARTCRAFT_DARK.input; /// `base-fg`: body text.-pub const INK: Color32 = hex(0xf2f1ee);-pub const INK_STRONG: Color32 = Color32::WHITE;-/// `base-fg/60`: secondary text and placeholders.-pub const MUTED: Color32 = alpha(0xf2f1ee, 153);+pub const INK: Color32 = SuitePalette::ARTCRAFT_DARK.text;+pub const INK_STRONG: Color32 = SuitePalette::ARTCRAFT_DARK.text_strong;+/// Secondary text and placeholders in the standard dark palette.+pub const MUTED: Color32 = SuitePalette::ARTCRAFT_DARK.text_secondary; /// `base-fg/50`: idle toolbar icons. pub const DIM: Color32 = alpha(0xf2f1ee, 128);-/// `base-fg/40`: counters and captions.-pub const FAINT: Color32 = alpha(0xf2f1ee, 102);+/// Disabled labels and captions.+pub const FAINT: Color32 = SuitePalette::ARTCRAFT_DARK.text_disabled; /// The sidebar's link colour (`white/72`). pub const SIDEBAR_INK: Color32 = alpha(0xffffff, 184); -/// `white/15`: every hairline.-pub const LINE: Color32 = alpha(0xffffff, 38);+/// Border used by the shared standard dark interface.+pub const LINE: Color32 = SuitePalette::ARTCRAFT_DARK.border; /// `white/25`: dashed drop targets. pub const LINE_DASHED: Color32 = alpha(0xffffff, 64);-/// `base-fg/40`: outlined buttons and corner marks.-pub const LINE_STRONG: Color32 = alpha(0xf2f1ee, 102);+/// Strong border on controls and focusable fields.+pub const LINE_STRONG: Color32 = SuitePalette::ARTCRAFT_DARK.border_strong; /// `white/5`: quiet fills. pub const WASH: Color32 = alpha(0xffffff, 13);-/// `white/8`: the selected sidebar row.-pub const WASH_SELECTED: Color32 = alpha(0xffffff, 20);-/// `white/10`: hovered menu rows.-pub const WASH_HOVER: Color32 = alpha(0xffffff, 26);+/// Selected sidebar row.+pub const WASH_SELECTED: Color32 = SuitePalette::ARTCRAFT_DARK.selection;+/// Hovered menu rows.+pub const WASH_HOVER: Color32 = SuitePalette::ARTCRAFT_DARK.hover;+/// Pressed controls and open menus.+pub const WASH_PRESSED: Color32 = SuitePalette::ARTCRAFT_DARK.pressed; /// The brand blue (`primary`).-pub const ACCENT: Color32 = hex(0x2d81ff);+pub const ACCENT: Color32 = SuitePalette::ARTCRAFT_DARK.accent; /// `primary-400`: progress bars and selection rings.-pub const ACCENT_400: Color32 = hex(0x59a7ff);+pub const ACCENT_400: Color32 = SuitePalette::ARTCRAFT_DARK.accent_hover; /// `ui-accent-ink`: blue text and focus rings.-pub const ACCENT_INK: Color32 = hex(0x74aaff);+pub const ACCENT_INK: Color32 = SuitePalette::ARTCRAFT_DARK.focus;+/// Text and symbols on a solid blue accent fill.+pub const ACCENT_ON: Color32 = SuitePalette::ARTCRAFT_DARK.on_accent; /// The inverted primary button (white block, near-black text).-pub const INVERT_BG: Color32 = Color32::WHITE;-pub const INVERT_FG: Color32 = hex(0x0b0b0c);+pub const INVERT_BG: Color32 = SuitePalette::ARTCRAFT_DARK.primary;+pub const INVERT_FG: Color32 = SuitePalette::ARTCRAFT_DARK.on_primary; /// `red-400`: failure text. pub const BAD: Color32 = hex(0xf87171);@@ -137,13 +142,13 @@ fn visuals() -> Visuals { v.popup_shadow = shadow(); v.override_text_color = Some(INK); v.hyperlink_color = ACCENT_INK;- v.selection.bg_fill = ACCENT;- v.selection.stroke = Stroke::new(1.0, Color32::WHITE);+ v.selection.bg_fill = WASH_SELECTED;+ v.selection.stroke = Stroke::new(1.0, SuitePalette::ARTCRAFT_DARK.selection_text); v.text_cursor.stroke = Stroke::new(1.5, Color32::WHITE); v.warn_fg_color = BADGE_BETA; v.error_fg_color = BAD; v.striped = false;- for (w, fill, stroke, fg) in [(&mut v.widgets.noninteractive, CONTROLS, LINE, MUTED), (&mut v.widgets.inactive, CONTROLS, LINE, INK), (&mut v.widgets.hovered, WASH_HOVER, LINE_STRONG, INK_STRONG), (&mut v.widgets.active, WASH_HOVER, LINE_STRONG, INK_STRONG), (&mut v.widgets.open, WASH_HOVER, LINE_STRONG, INK_STRONG)] {+ for (w, fill, stroke, fg) in [(&mut v.widgets.noninteractive, CONTROLS, LINE, MUTED), (&mut v.widgets.inactive, CONTROLS, LINE, INK), (&mut v.widgets.hovered, WASH_HOVER, LINE_STRONG, INK_STRONG), (&mut v.widgets.active, WASH_PRESSED, LINE_STRONG, INK_STRONG), (&mut v.widgets.open, WASH_PRESSED, LINE_STRONG, INK_STRONG)] { w.bg_fill = fill; w.weak_bg_fill = fill; w.bg_stroke = Stroke::new(1.0, stroke);@@ -191,3 +196,55 @@ fn install_fonts(ctx: &egui::Context) { } ctx.set_fonts(fonts); }++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn shared_roles_preserve_inverted_controls_media_and_semantic_colors() {+ let roles = SuitePalette::ARTCRAFT_DARK;+ assert_eq!(BG, roles.window);+ assert_eq!(PANEL, roles.panel);+ assert_eq!(CONTROLS, roles.raised);+ assert_eq!(CHROME, roles.chrome);+ assert_eq!(SUNKEN, roles.input);+ assert_eq!(INK, roles.text);+ assert_eq!(INK_STRONG, roles.text_strong);+ assert_eq!(MUTED, roles.text_secondary);+ assert_eq!(FAINT, roles.text_disabled);+ assert_eq!(LINE, roles.border);+ assert_eq!(LINE_STRONG, roles.border_strong);+ assert_eq!(WASH_SELECTED, roles.selection);+ assert_eq!(WASH_HOVER, roles.hover);+ assert_eq!(ACCENT, roles.accent);+ assert_eq!(ACCENT_400, roles.accent_hover);+ assert_eq!(ACCENT_INK, roles.focus);+ assert_eq!(ACCENT_ON, roles.on_accent);+ assert_eq!(INVERT_BG, roles.primary);+ assert_eq!(INVERT_FG, roles.on_primary);+ let v = visuals();+ assert_eq!(v.widgets.hovered.bg_fill, roles.hover);+ assert_eq!(v.widgets.active.bg_fill, roles.pressed);+ assert_eq!(v.widgets.open.bg_fill, roles.pressed);+ assert_eq!(v.selection.stroke.color, roles.selection_text);+ assert_eq!((BAD, DANGER, BADGE_BETA), (hex(0xf87171), hex(0xd33242), hex(0xffc14d)));+ assert_eq!(DIM, alpha(0xf2f1ee, 128));+ assert_eq!(LINE_DASHED, alpha(0xffffff, 64));+ assert_eq!(RADIUS, CornerRadius::same(3));+ assert_eq!(v.text_edit_bg_color, Some(Color32::TRANSPARENT));+ assert_eq!(v.text_cursor.stroke.color, Color32::WHITE);+ }++ #[test]+ fn standard_dark_roles_keep_selected_rows_distinct_from_blue_actions() {+ let v = visuals();+ assert_eq!(BG, hex(0x121316));+ assert_eq!(PANEL, hex(0x101014));+ assert_eq!(ACCENT, hex(0x2d81ff));+ assert_eq!(ACCENT_ON, hex(0x060607));+ assert_eq!(v.selection.bg_fill, hex(0x203b65));+ assert_eq!(v.selection.stroke.color, INK);+ assert_eq!(v.widgets.inactive.bg_stroke.color, hex(0x36373a));+ }+}new file mode 100644@@ -0,0 +1,84 @@+//! Opt-in source render of production shell/widgets; never starts the backend or reads accounts.++use egui_kittest::Harness;+use crate::shell::{self, Account, Page};+use crate::theme;+use crate::ui::widgets::{self, Kind, Leading, RowLead};++#[derive(Default)]+struct State {+ fonts_ready: bool,+ prompt: String,+}++/// `ARTCRAFT_COMPONENT_SCREENSHOT=/absolute/file.png cargo test -p artcraft_native+/// theme_render_tests::dark_components_source_render -- --exact`.+/// This is a production-component review fixture, not a full app/session screenshot.+#[test]+fn dark_components_source_render() {+ let Some(path) = std::env::var_os("ARTCRAFT_COMPONENT_SCREENSHOT") else {+ return;+ };+ let mut harness = Harness::builder().with_size(egui::vec2(1100.0, 900.0)).build_ui_state(+ |ui, state: &mut State| {+ if !state.fonts_ready {+ theme::apply(ui.ctx());+ state.fonts_ready = true;+ return;+ }+ ui.painter().rect_filled(ui.max_rect(), 0, theme::CHROME);+ ui.horizontal_top(|ui| {+ ui.allocate_ui_with_layout(egui::vec2(shell::SIDEBAR_WIDTH, 860.0), egui::Layout::top_down(egui::Align::Min), |ui| {+ shell::sidebar(ui, Page::CreateImage, &Account { username: None, display_name: None, credits: None });+ // Production sidebar paints into child UIs; reserve its extent in this review layout.+ ui.allocate_space(egui::vec2(shell::SIDEBAR_WIDTH, 860.0));+ });+ egui::Frame::new().fill(theme::BG).inner_margin(24).show(ui, |ui| {+ ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {+ ui.set_width(770.0);+ ui.heading("ArtCraft native source components");+ ui.label("Production sidebar and widget render • backend disconnected");+ ui.add_space(24.0);+ egui::Frame::new().fill(theme::PANEL).inner_margin(16).show(ui, |ui| {+ ui.set_width(720.0);+ widgets::hud(ui, "Image prompt", theme::MUTED);+ ui.add_space(8.0);+ ui.add(egui::TextEdit::multiline(&mut state.prompt).desired_width(700.0).desired_rows(4).hint_text("Describe what you want in the image...")).request_focus();+ ui.add_space(12.0);+ ui.horizontal(|ui| {+ widgets::pill(ui, Leading::None, "Nano Banana Pro", false);+ widgets::pill(ui, Leading::Aspect(Some((1.0, 1.0))), "Square", true);+ widgets::generate_button(ui, true, false, "Generate");+ });+ });+ ui.add_space(24.0);+ ui.horizontal(|ui| {+ widgets::button(ui, None, "Continue", Kind::Accent, 150.0, 38.0);+ widgets::button(ui, None, "Download", Kind::Primary, 150.0, 38.0);+ ui.add_enabled_ui(false, |ui| {+ widgets::button(ui, None, "Unavailable", Kind::Secondary, 150.0, 38.0);+ });+ });+ ui.add_space(24.0);+ widgets::popover_frame().show(ui, |ui| {+ ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {+ ui.set_width(340.0);+ widgets::menu_header(ui, "Aspect ratio");+ widgets::menu_row(ui, RowLead::Aspect(Some((1.0, 1.0))), "Square", Some("Selected option"), true, false, false);+ widgets::menu_row(ui, RowLead::Aspect(Some((16.0, 9.0))), "16:9 (Wide)", Some("Hovered option"), false, false, true);+ ui.add_enabled_ui(false, |ui| {+ widgets::menu_row(ui, RowLead::None, "Unavailable", None, false, false, false);+ });+ });+ });+ });+ });+ });+ },+ State::default(),+ );+ harness.run();+ let image = harness.render().expect("source component render");+ assert_eq!(image.dimensions(), (1100, 900));+ image.save(path).expect("save source component screenshot");+}@@ -161,7 +161,7 @@ pub fn generate_button(ui: &mut Ui, enabled: bool, loading: bool, tip: &str) -> (true, false) => theme::ACCENT, }; ui.painter().circle_filled(rect.center(), 18.0, fill);- let ink = if enabled { Color32::WHITE } else { theme::fade(Color32::WHITE, 0.5) };+ let ink = if enabled { theme::ACCENT_ON } else { theme::fade(theme::ACCENT_ON, 0.5) }; let inner = Rect::from_center_size(rect.center(), vec2(18.0, 18.0)); if loading { icons::paint_spinner(ui, inner, ink);@@ -201,7 +201,7 @@ pub fn button(ui: &mut Ui, icon: Option<Icon>, text: &str, kind: Kind, width: f3 let hovered = resp.hovered() && enabled; let (fill, stroke, ink) = match kind { Kind::Primary => (theme::fade(theme::INVERT_BG, if hovered { 0.88 } else { 1.0 }), Stroke::NONE, theme::INVERT_FG),- Kind::Accent => (if hovered { theme::ACCENT_400 } else { theme::ACCENT }, Stroke::NONE, Color32::WHITE),+ Kind::Accent => (if hovered { theme::ACCENT_400 } else { theme::ACCENT }, Stroke::NONE, theme::ACCENT_ON), Kind::Danger => (theme::fade(theme::DANGER, if hovered { 0.85 } else { 1.0 }), Stroke::NONE, Color32::WHITE), Kind::Secondary if hovered => (theme::INVERT_BG, Stroke::NONE, theme::INVERT_FG), Kind::Secondary => (Color32::TRANSPARENT, Stroke::new(1.0, theme::LINE_STRONG), theme::INK),@@ -93,8 +93,8 @@ body { } ::selection {- background: #2d81ff;- color: #ffffff;+ background: var(--st-selection-bg, #2d81ff);+ color: var(--st-selection-fg, #ffffff); } :focus-visible {@@ -129,20 +129,20 @@ body { /* ===================== THEME VARIABLES ===================== */ /* The default (the "gray" theme id) is the brutalist system shared with- artcraft-webapp: near-black surfaces, white/15 hairline borders. */+ artcraft-webapp: near-black surfaces and fine hairline borders. */ :root { --st-bg: #121316; --st-panel: #101014;- --st-panel-border: rgba(255, 255, 255, 0.15);+ --st-panel-border: #36373a; --st-modal: #101014; /* default modal surface (can be gradient per theme) */ --st-controls: #1e1f22; --st-controls-rgb: 30 31 34;- --st-controls-border: rgba(255, 255, 255, 0.15);+ --st-controls-border: #36373a; --st-sunken: #060607; --st-fg: #f2f1ee; --st-fg-rgb: 242 241 238;- --st-divider: rgba(255, 255, 255, 0.15);+ --st-divider: #36373a; --st-gallery-vignette-alpha: 0.6; /* Photo tint (default neutral) */ --st-photo-tint-rgb: 0 0 0;@@ -151,8 +151,7 @@ body { /* Theme-aware badge alpha (default dark) */ --st-badge-alpha: 0.4; - /* shadcn design tokens consumed by @storyteller/ui-video-editor.- Same values as artcraft-webapp's styles.css. */+ /* Existing video-editor defaults also apply to the alternate themes. */ --background: 225 10% 8%; --foreground: 0 0% 98%; --card: 240 11% 7%;@@ -218,23 +217,43 @@ body { /* Default dark (brutalist, matches artcraft-webapp) */ .theme-gray {+ --st-selection-bg: #203b65;+ --st-selection-fg: #f2f1ee; --st-bg: #121316; --st-panel: #101014;- --st-panel-border: rgba(255, 255, 255, 0.15);+ --st-panel-border: #36373a; --st-modal: #101014; --st-controls: #1e1f22; --st-controls-rgb: 30 31 34;- --st-controls-border: rgba(255, 255, 255, 0.15);+ --st-controls-border: #36373a; --st-sunken: #060607; --st-fg: #f2f1ee; --st-fg-rgb: 242 241 238;- --st-divider: rgba(255, 255, 255, 0.15);+ --st-divider: #36373a; --st-color-scheme: dark; --st-gallery-vignette-alpha: 0.3; --st-photo-tint-rgb: 0 0 0; --st-photo-tint-alpha: 0.6; --st-photo-grayscale: 1; --st-badge-alpha: 0.4;+ /* Canonical dark HSL counterparts used by @storyteller/ui-video-editor. */+ --background: 225 10% 7.8431%;+ --foreground: 45 13.3333% 94.1176%;+ --card: 240 11.1111% 7.0588%;+ --card-foreground: 45 13.3333% 94.1176%;+ --popover: 240 11.1111% 7.0588%;+ --popover-foreground: 45 13.3333% 94.1176%;+ --primary: 216 100% 58.8235%;+ --primary-foreground: 240 7.6923% 2.549%;+ --secondary: 225 6.25% 12.549%;+ --secondary-foreground: 45 13.3333% 94.1176%;+ --muted: 225 6.25% 12.549%;+ --muted-foreground: 240 1.1364% 65.4902%;+ --accent: 228 4.7619% 20.5882%;+ --accent-foreground: 45 13.3333% 94.1176%;+ --border: 225 3.5714% 21.9608%;+ --input: 225 3.5714% 21.9608%;+ --ring: 216.6906 100% 72.7451%; } /* Full black */