AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/ui/src/lib.rs

Latest change d7f31233a0aaba65d741a85ac631a0e9004dba49 - akurai-ui kit: chat components (thread, message, tool_call, system_line, composer, presence) with Normal/Tv scale by Ólafur Búi Ólafsson

//! AkurAI-Framework UI — a pure-Rust, React-style reactive UI for the browser.
//!
//! Components are Rust functions; state is fine-grained [`signal`]s; markup is
//! a chainable element API (`div().class("flex gap-2").child(…)`) styled with
//! the framework's utility CSS engine (`akurai-css`, Tailwind-class). The app
//! compiles to `wasm32-unknown-unknown` with **no wasm-bindgen and no external
//! crates**: [`host.js`](HOST_JS), a ~150-line shim, is the entire JavaScript.
//!
//! - [`reactive`] — signals, effects, memos, scopes, context.
//! - [`dom`] — views, elements, dynamic regions, keyed lists, portals.
//! - [`kit`] — accessible components in the spirit of Radix + shadcn/ui
//!   (button, card, dialog, tabs, checkbox, toast, …), flat-cornered.
//! - [`router`], [`http`], [`platform`] — hash routing, fetch, timers.
//! - [`testing`] — an in-memory DOM so components are tested with `cargo test`.
//!
//! An app crate is a `cdylib` that calls [`app!`] with its root component:
//!
//! ```ignore
//! fn root() -> impl akurai_ui::IntoView { "hello" }
//! akurai_ui::app!(root);
//! ```

mod callbacks;
pub mod dom;
mod host;
pub mod html;
pub mod http;
pub mod kit;
pub mod platform;
pub mod reactive;
pub mod router;
#[cfg(not(target_arch = "wasm32"))]
pub mod testing;

pub use callbacks::live as live_callbacks;
pub use dom::listen;
pub use dom::{each, show, when, El, Event, IntoView, NodeRef, View};
pub use host::{Node, BODY, DOCUMENT, WINDOW};
pub use reactive::{batch, effect, memo, signal, untrack, Signal};

/// Everything a component file usually needs.
pub mod prelude {
    pub use crate::dom::{dynamic, each, portal, show, when, El, Event, IntoView, NodeRef, View};
    pub use crate::html::*;
    pub use crate::reactive::{
        batch, effect, expect_context, memo, on_cleanup, provide_context, signal, untrack,
        use_context, Signal,
    };
    pub use crate::router::{navigate, use_route};
}

/// The browser host shim. Serve it next to the app's `.wasm`.
pub const HOST_JS: &str = include_str!("host.js");

/// Reset and base element styles the kit assumes. Prepend to the app's CSS.
pub const BASE_CSS: &str = include_str!("base.css");

/// The kit's source, for the utility-CSS scanner: an app's stylesheet must
/// include the classes the kit uses, so builds scan these alongside the app.
pub const STYLE_SOURCES: &[&str] = &[
    include_str!("kit/mod.rs"),
    include_str!("kit/button.rs"),
    include_str!("kit/card.rs"),
    include_str!("kit/chat.rs"),
    include_str!("kit/form.rs"),
    include_str!("kit/dialog.rs"),
    include_str!("kit/tabs.rs"),
    include_str!("kit/toast.rs"),
    include_str!("kit/misc.rs"),
    include_str!("kit/icons.rs"),
];

/// Mount `root` into `#app` (or `<body>`), inside a root scope that lives for
/// the page's lifetime. Called by [`app!`].
pub fn start<V: IntoView>(root: impl FnOnce() -> V) {
    std::panic::set_hook(Box::new(|info| host::log(&format!("panic: {info}"))));
    let target = host::get_by_id("app").unwrap_or(host::BODY);
    let ((), scope) = reactive::create_root(|| dom::mount(target, root().into_view(), None));
    let _ = scope; // the root scope lives as long as the page
}

/// The wasm ABI entry points `host.js` calls. Not for direct use.
#[doc(hidden)]
pub mod abi {
    /// Allocate `len` bytes for JS to write a string into.
    pub fn alloc(len: usize) -> *mut u8 {
        Box::into_raw(vec![0u8; len].into_boxed_slice()) as *mut u8
    }

    pub fn dispatch(callback: u32, event: u32) {
        crate::callbacks::dispatch(callback, crate::host::EventRef(event));
    }

    /// # Safety
    /// `ptr`/`len` must come from [`alloc`] and be passed exactly once.
    pub unsafe fn complete(callback: u32, status: u32, ptr: *mut u8, len: usize) {
        // SAFETY: per the contract, this reclaims the box `alloc` leaked.
        let bytes = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) };
        let body = String::from_utf8(bytes.into_vec()).unwrap_or_default();
        crate::callbacks::complete(callback, status as u16, body);
    }
}

/// Export the wasm entry points for an app whose root component is `$root`.
#[macro_export]
macro_rules! app {
    ($root:path) => {
        #[no_mangle]
        pub extern "C" fn akurai_start() {
            $crate::start($root);
        }
        #[no_mangle]
        pub extern "C" fn akurai_alloc(len: usize) -> *mut u8 {
            $crate::abi::alloc(len)
        }
        #[no_mangle]
        pub extern "C" fn akurai_dispatch(callback: u32, event: u32) {
            $crate::abi::dispatch(callback, event)
        }
        /// # Safety
        /// Called by `host.js` with a buffer from `akurai_alloc`.
        #[no_mangle]
        pub unsafe extern "C" fn akurai_complete(
            callback: u32,
            status: u32,
            ptr: *mut u8,
            len: usize,
        ) {
            unsafe { $crate::abi::complete(callback, status, ptr, len) }
        }
    };
}