Home

lasse / loora

publiclassejlv/loora
Code Branches Pull requestsIssuesInsights
loora-rs
Home Code PRsIssues

refactor(canvas): split native canvas into focused modules

9015ccbby Lasse2 days ago

README.md

Loora

Native design canvas built with GPUI.

Workspace

crates/
  ui/        # Component library + canvas (`loora-ui`)
  router/    # React-Router style routing (`gpui-router`)
  engine/    # Document model, camera, local save (`loora-engine`)
  mcp/       # Built-in local MCP server (`loora-mcp`)
  desktop/   # App binary (`loora`)

loora-ui exports reusable primitives (Theme, Icon, Button, Sidebar, Motion, canvas workspace, …). The desktop crate owns window chrome and app startup.

Routing

gpui_router::init(cx);

Routes::new().child(
    Route::new().path("/").layout(AppShell::new(theme)).children([
        Route::new().index().element(|_, _| home()),
        Route::new().path("about").element(|_, _| about()),
        Route::new().path("{*rest}").element(|_, _| not_found()),
    ]),
)

Navigate with NavLink::new().to("/about") or use_navigate(cx). Nested layouts use Outlet.

Sidebar (library)

Sidebar::new(theme)
    .title("Loora")
    .subtitle("Workspace")
    .selected("home")
    .items([
        SidebarEntry::new("home", "Home", IconName::Home),
        SidebarEntry::new("search", "Search", IconName::Search),
    ])
    .on_select(|id, window, cx| { /* navigate */ })

Motion

loora_ui::init_motion(cx);

Motion::new()
    .id("page")
    .variants(Variants::new()
        .set("hidden", MotionStyle::new().opacity(0.).y(px(16.)))
        .set("visible", MotionStyle::new().opacity(1.).y(px(0.))))
    .initial_variant("hidden")
    .animate_variant("visible")
    .exit(MotionStyle::new().opacity(0.).y(px(-12.)))
    .while_hover(MotionStyle::new().scale(1.02))
    .while_tap(MotionStyle::new().scale(0.97))
    .transition(Transition::spring().stiffness(280.).damping(24.))
    .child(content)

AnimatePresence::new("main")
    .mode(PresenceMode::Wait)
    .child(pathname, |_, _| page)

Stagger::new()
    .transition(Transition::spring().stagger_children(Duration::from_millis(60)))
    .motion(Motion::new().id("a").fade_up().child(...))

let v = use_motion_value(cx, "scroll");
v.set_with_notify(0.5, cx);
Motion::new().id("x").opacity_from(v).fade_in()

Run

just run          # release
just dev          # debug
just check        # typecheck

Or:

cargo run --release -p loora-desktop

While the desktop app is running, its no-auth MCP endpoint is available only on loopback at http://127.0.0.1:6767/mcp. Set LOORA_MCP_PORT to another valid local port before launch if needed. MCP mutations execute against the same live canvas engine and are persisted to the local design store. Open the command palette from the document title to copy the MCP URL or add Loora directly to Claude, Codex, Cursor, or OpenCode.

Quit with ⌘Q / Ctrl+Q.

Stack

  • GPUI + gpui_platform from Zed
  • Dark / light theme colors (crates/ui/themes/dark.json, crates/ui/themes/light.json)

Clone

git clone https://git.open-git.com/lasse/loora.git

About

Visibilitypublic
Branches17
Commits30
ChecksNone
Created8/2/2026
Age
12 days

Languages

  • PNG39.9%
  • Rust28.0%
  • ICNS17.5%
  • ICO6.3%
  • JSON3.8%
  • LOCK3.1%

Contributors1

  • Lasse