mod context;
pub use self::context::HtmlActixPresentationContext;
use std::net::IpAddr;
use std::sync::LazyLock;
use bcode_client::ClientError;
use bcode_session_models::SessionSummary;
use bcode_session_view_models::SessionViewSnapshot;
use hyperchad::app::{App, AppBuilder, renderer::DefaultRenderer};
use hyperchad::color::Color;
use crate::{HyperChadAppState, router, router_from_state};
static BACKGROUND_COLOR: LazyLock<Color> = LazyLock::new(|| Color::from_hex("#0d1117"));
const ACCESSIBILITY_CSS: &str = r#"
:where(a, button, input, select, textarea, summary):focus-visible {
outline: 3px solid #58a6ff;
outline-offset: 3px;
}
:where(button, input:not([type="hidden"]), select, summary) {
min-height: 44px;
}
:where(button, input:not([type="hidden"]), select, textarea, summary) {
min-width: 44px;
}
:where(button, input, select, textarea) {
font: inherit;
}
textarea {
min-height: 88px;
resize: vertical;
}
html, body {
margin: 0;
max-width: 100%;
overflow-x: hidden;
}
*, *::before, *::after {
box-sizing: border-box;
}
#bcode-web-shell, #bcode-application-content, #application-layout, #conversation-main {
max-width: 100%;
min-width: 0;
}
#application-layout > *, #conversation-main > * {
min-width: 0;
}
@media (max-width: 900px) {
#application-layout {
flex-direction: column;
}
#session-navigation, #conversation-main {
width: 100%;
}
}
@media (max-width: 600px) {
#bcode-web-shell {
padding: 16px;
}
button, input, select, textarea {
max-width: 100%;
}
}
:where(pre, code) {
max-width: 100%;
overflow-x: auto;
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
}
:where(p, li, dd, blockquote, a) {
overflow-wrap: anywhere;
}
img {
height: auto;
max-width: 100%;
}
"#;
#[cfg(test)]
pub const fn accessibility_css() -> &'static str {
ACCESSIBILITY_CSS
}
pub const DEFAULT_BIND_ADDRESS: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
pub static VIEWPORT: LazyLock<String> =
LazyLock::new(|| "width=device-width, initial-scale=1".to_string());
#[must_use]
pub fn build_launch_url(
address: std::net::SocketAddr,
access_token: &str,
initial_session_id: Option<bcode_session_models::SessionId>,
) -> String {
let query = initial_session_id.map_or_else(
|| format!("token={access_token}"),
|session_id| {
format!("token={access_token}&hyperchad-event-scope={access_token}:{session_id}")
},
);
format!("http://{address}/?{query}")
}
pub const fn validate_bind_address(
address: IpAddr,
allow_non_loopback: bool,
) -> Result<IpAddr, &'static str> {
if address.is_loopback() || allow_non_loopback {
Ok(address)
} else {
Err("non-loopback web binds require explicit opt-in")
}
}
fn with_browser_runtime(builder: AppBuilder) -> AppBuilder {
builder
.with_inline_css(ACCESSIBILITY_CSS)
.with_static_asset_route(hyperchad::renderer::assets::StaticAssetRoute {
route: format!(
"js/{}",
hyperchad::renderer_vanilla_js::SCRIPT_NAME_HASHED.as_str()
),
target: hyperchad::renderer::assets::AssetPathTarget::FileContents(
hyperchad::renderer_vanilla_js::SCRIPT.as_bytes().into(),
),
not_found_behavior: None,
})
}
#[must_use]
pub fn init_with_snapshot(
snapshot: SessionViewSnapshot,
sessions: Vec<SessionSummary>,
) -> AppBuilder {
with_browser_runtime(
AppBuilder::new()
.with_actix_bind_address(DEFAULT_BIND_ADDRESS.to_string())
.with_router(router(snapshot, sessions))
.with_background(*BACKGROUND_COLOR)
.with_title("bcode web".to_string())
.with_description("HyperChad application for Bcode sessions".to_string())
.with_viewport(VIEWPORT.clone())
.with_size(1200.0, 800.0),
)
}
pub async fn init(state: &HyperChadAppState) -> Result<AppBuilder, ClientError> {
state.client().ensure_daemon_available().await?;
Ok(with_browser_runtime(
AppBuilder::new()
.with_actix_bind_address(DEFAULT_BIND_ADDRESS.to_string())
.with_router(router_from_state(state.clone()))
.with_background(*BACKGROUND_COLOR)
.with_title("bcode web".to_string())
.with_description("HyperChad application for Bcode sessions".to_string())
.with_viewport(VIEWPORT.clone())
.with_size(1200.0, 800.0),
))
}
pub fn build_app_with_runtime(
builder: AppBuilder,
) -> Result<(App<DefaultRenderer>, switchy::unsync::runtime::Runtime), hyperchad::app::Error> {
let runtime = switchy::unsync::runtime::Builder::new().build()?;
let app = build_app(builder.with_runtime_handle(runtime.handle()))?;
Ok((app, runtime))
}
pub fn build_app(builder: AppBuilder) -> Result<App<DefaultRenderer>, hyperchad::app::Error> {
use hyperchad::renderer::Renderer as _;
let mut app = builder.build_default()?;
app.renderer.add_responsive_trigger(
"narrow".to_owned(),
hyperchad::renderer::transformer::ResponsiveTrigger::MaxWidth(
hyperchad::renderer::transformer::Number::Integer(640),
),
);
app.renderer.add_responsive_trigger(
"tablet".to_owned(),
hyperchad::renderer::transformer::ResponsiveTrigger::MaxWidth(
hyperchad::renderer::transformer::Number::Integer(960),
),
);
Ok(app)
}