codex-skills/yororen-ui-recipes/SKILL.md
End-to-end recipe patterns for end users building gpui apps with Yororen UI (yororen_ui). Use when the user asks for a complete working example, wants a screen layout pattern, or needs guidance composing components, modals, forms, list rendering, virtualized lists, notifications, theme switching, or i18n. Triggers include "show me a full example", "copy from the gallery", "how do I do theme switching", "how do I compose a modal + form", "how do I virtualize a long list". Not for contributing to yororen-ui itself.
npx skillsauth add meowlynxsea/yororen-ui yororen-ui-recipesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Prefer copying and adapting proven patterns from the demos rather than inventing new architectures. The five live demos converge on a small set of working patterns; this skill points to the right demo for each need and distills the cross-cutting composition rules.
All five are under crates/yororen-ui-demos/. The first four are
members of the workspace Cargo.toml; the fifth is excluded but
still builds in isolation.
| Demo | What it shows | Copy from when you want… |
|---|---|---|
| counter | minimal bootstrap, single Entity<T> global, three buttons, cx.notify() | a starter template for any app |
| layers_demo | the 3 render pathways (headless / default-render / caller-custom Material ripple) side by side | to understand the visual flexibility of headless props |
| inputs_demo | all 7 text inputs wired with cx.entity().clone() on_change closures | an input form, a settings page, anything input-heavy |
| gallery_demo | the full 54-component showcase, theme switching, i18n, notification host, virtualized list | a kitchen-sink reference for any pattern |
| theme_showcase | per-render theme::install(cx, theme) for live theme switching | a "Next theme" toolbar, A/B theme testing |
The gallery is the most complete — start there if you're building a real app. The counter is the smallest — start there if you're new.
Files: crates/yororen-ui-demos/counter/src/.
// main.rs
use gpui::{App, AppContext, Application, WindowBounds, WindowOptions, px, size};
use yororen_ui::assets::UiAsset;
use yororen_ui::locale_en;
use yororen_ui::renderer;
mod counter_app;
mod state;
fn main() {
let app = Application::new().with_assets(UiAsset);
app.run(|cx: &mut App| {
renderer::install(cx, cx.window_appearance());
locale_en::install(cx);
cx.set_global(state::AppState::new(cx));
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(
gpui::Bounds::centered(None, size(px(400.0), px(300.0)), cx),
)),
..Default::default()
};
let _ = cx.open_window(options, |_, cx| cx.new(|_cx| counter_app::CounterApp));
});
}
// state.rs
use gpui::{App, AppContext, Entity, Global};
#[derive(Default)] pub struct Counter { pub value: i32 }
pub struct AppState { pub counter: Entity<Counter> }
impl AppState {
pub fn new(cx: &mut App) -> Self { Self { counter: cx.new(|_| Counter::default()) } }
}
impl Global for AppState {}
// counter_app.rs — three buttons, each with its own Entity clone.
use gpui::{Context, IntoElement, ParentElement, Render, Styled, Window, div};
use yororen_ui::headless::button::button;
use yororen_ui::headless::label::label;
use crate::state::AppState;
pub struct CounterApp;
impl Render for CounterApp {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let count = cx.global::<AppState>().counter.read(cx).value;
let inc = cx.global::<AppState>().counter.clone();
let dec = cx.global::<AppState>().counter.clone();
let reset = cx.global::<AppState>().counter.clone();
div().size_full().flex().flex_col().items_center().justify_center().gap_3().p_4()
.child(label("count", count.to_string(), cx).render(cx).text_size(gpui::px(20.)))
.child(
div().flex().gap_2()
.child(button("dec", cx).on_click(move |_, _, cx| {
dec.update(cx, |c, cx| { c.value -= 1; cx.notify(); });
}).render(cx).child("-"))
.child(button("reset", cx).on_click(move |_, _, cx| {
reset.update(cx, |c, cx| { c.value = 0; cx.notify(); });
}).render(cx).child("Reset"))
.child(button("inc", cx).on_click(move |_, _, cx| {
inc.update(cx, |c, cx| { c.value += 1; cx.notify(); });
}).render(cx).child("+")),
)
}
}
Three takeaways from the counter:
Entity<Counter> inside a Global wrapper. The
global is registered with cx.set_global, read with
cx.global::<AppState>().move closure clones the Entity<Counter> once at
construction. The closure owns the clone.cx.notify(). gpui-ce 0.3 does not auto-notify
on Entity::update.Files: crates/yororen-ui-demos/layers_demo/src/.
One window, three columns:
| Column | Render pathway | What it shows |
|---|---|---|
| 1 | headless::button(id, cx).on_click(...).apply(div().bg(…).p_2().child("click me")) | A11y only — caller owns every visual. The button does not respond to hover/press; the cursor changes to a pointer. |
| 2 | headless::button(id, cx).variant(ActionVariantKind::Neutral).on_click(...).render(cx).child("Click me") | The default renderer paints bg / border / padding / radius / hover / active from the theme JSON. |
| 3 | material_button(id, "Click me".into(), cx, window) (a custom Material-ripple painter defined in material_button.rs) | Caller writes a bespoke gpui::Element for the ripple animation. Headless apply is still called for focus + click. |
Copy from this demo when you need to:
.apply does and doesn't do (it doesn't paint).Element (the ripple) that uses
window.request_animation_frame to advance an f32 progress and
PathBuilder to draw a true circle.material_button function calls headless::button(id, cx).on_click(...).apply(div()...)
internally — the focus + click is the headless layer's job; the
visual is the bespoke painter's job).The demo also has a fourth panel: a text_input with .render(cx, window)
to prove the headless / renderer split works the same for inputs, not
just buttons.
Files: crates/yororen-ui-demos/inputs_demo/src/.
Seven panels, one per text input. The pattern is the same for all:
text_input("demo-text-input")
.placeholder("Type here…")
.on_change({
let entity = cx.entity(); // <-- the canonical pattern
move |new: &str, _window, cx| {
entity.update(cx, |s, _cx| s.text_value = new.to_string());
}
})
.render(cx, window)
Plus a status line that reads the value back from state:
fn status_line(text: &str) -> Div {
div().text_color(hsla(0.0, 0.0, 0.4, 1.0)).text_size(px(12.)).child(text.to_string())
}
// ...
.child(status_line(&format!("text_input value: {:?}", self.text_value)))
Two non-obvious things the demo shows:
number_input has three callbacks — on_change(f64, …) for
the typed value, on_increment(f64, …) and on_decrement(f64, …)
for the stepper buttons. All three write to the same field.keybinding_input has its own mode: KeybindingInputMode
(Idle or Capturing). The renderer drives the mode transitions
but you wire the on_start_capture / on_cancel_capture hooks to
update your app state so the status line shows "Capturing…" vs "Idle".Files: crates/yororen-ui-demos/gallery_demo/src/.
By far the largest demo. It is the canonical reference for:
cell() and input_cell() helper functions that wrap a single
component in a labelled card with a status line — the fastest way
to compose a "compare every variant" page.cx.renderer_arc and per-render
theme::install(cx, theme).locale::install_with_translations(cx, "en"|"zh-CN"|"ar", app_map).NotificationCenter host rendered with
gpui::deferred(...).with_priority(3) so toasts float above
modals.modal_state.set_on_close +
ModalCloseReason::Escape | ScrimClick | Programmatic.on_visible_range_change to bump the controller's count.When the user asks "how do I do X", the answer is almost always in the gallery. Open it before writing new code.
Files: crates/yororen-ui-demos/theme_showcase/src/ (excluded from
workspace but still builds; check the file before reaching for it).
Two-window pattern for theme switching:
fn main() {
let app = Application::new().with_assets(UiAsset);
app.run(|cx: &mut App| {
// Register the 54 default renderers ONCE (no theme yet).
default_renderer::install(cx, cx.window_appearance());
let app_entity = cx.new(|_cx| theme_app::ThemeApp::new());
let _ = cx.open_window(options, |_, _cx| app_entity);
});
}
impl Render for ThemeApp {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Re-install the theme on every render — cheap, and lets a
// toolbar button advance `self.current` and retheme the window.
theme::install(cx, self.current_theme());
let surface = cx.theme().get_color("surface.base").unwrap_or_default();
// ... layout ...
}
}
The four themes are usually system-light, system-dark, and two
inline const CATPPUCCIN: &str = r##"{...}"##; JSON strings. Bump
self.current = (self.current + 1) % themes.len() in a click handler,
cx.notify(), and the next render re-themes the window.
These are the rules the demos converge on. Violate them and the UI becomes hard to maintain or visibly broken.
Stable identity. Every stateful child in a list, virtualized
view, or any tree that can be reordered needs .id(...). Use the
row index as the id when the data is append-only; use the data's
primary key when the data is mutable.
Example: text_input(format!("row-email-{ix}")) in a virtual list.
Render overlays at the scroll root, not inside a section. A
modal that lives inside a scrollable column will be clipped and
won't receive Escape reliably. Build the scroll root, then
.child(content).child(modal).child(toast_host) as siblings, with
the modal and toast wrapped in gpui::deferred(...).with_priority(N).
&mut **cx for minting entities from Context<T>.
cx.focus_handle() and XxxState::new(cx) need &mut App. Inside
a Render::render closure you have &mut Context<MyApp>;
Context<T>: DerefMut<Target = App>, so &mut **cx is a
&mut App. Use it inline at the call site, never stored in a let.
Stable identifier scope. Two inputs with the same id share
the same keyed state. If you render the same logical input twice
(in a list of email rows, say), include the row index or some
unique key in the id: format!("email-{row_id}").
No render() side effects. Render closures must be pure with
respect to gpui's redraw cycle. Spawning tasks, calling cx.spawn,
doing file I/O — all of these go in event handlers, not in
Render::render.
One renderer per (Marker, dyn Trait) slot. If you register
BrutalButtonRenderer for the Button marker, the previous
TokenButtonRenderer is overwritten. There's no layering. If you
want a hybrid, write a new renderer that does both.
| Renderer | When to use | Install |
|---|---|---|
| default (yororen-ui-default-renderer) | Standard look, full coverage, well-tested | yororen_ui::renderer::install(cx, cx.window_appearance()) |
| brutalism (yororen-ui-brutalism-renderer, feature-gated) | Sharp corners, hard shadows, monospace, high-contrast | yororen_ui::brutalism_renderer::install(cx) |
| custom (your own crate) | Brand identity, accessibility contrast requirements, animation customization | Write cx.register_renderer_arc::<m::X, dyn XRenderer>(Arc::new(MyRenderer)) for each marker you implement |
The same headless::button("save", cx).on_click(...).render(cx) works
in all three. The visual is what changes.
To make the choice at runtime (a "Switch theme" button), see
$yororen-ui-app-core § 11 and the theme_switcher.rs file in
gallery_demo.
$yororen-ui-user — entry point, hard rules$yororen-ui-app-core — bootstrap, theme authoring, state, i18n$yororen-ui-state-inputs — inputs, forms, modals, compositesreferences/overlay-z-order.md — paint priority for popovers, modals, toastsreferences/custom-renderer.md — writing your own 54-marker renderer cratedevelopment
High-quality app code generation for end users building Rust desktop GUIs with gpui + Yororen UI (yororen_ui). Use when a user asks to build, scaffold, or modify an application using Yororen UI/gpui, or when working in a Rust project that depends on yororen_ui. Triggers include "build a counter with yororen ui", "make a form with TextInput", "add a modal", "add a theme switcher", "use yororen_ui". Routes to sibling skills ($yororen-ui-app-core, $yororen-ui-state-inputs, $yororen-ui-recipes) for deep dives. Not for contributing to yororen-ui itself.
tools
State management and interactive components for end users building Yororen UI apps with gpui. Use when implementing TextInput/TextArea/SearchInput/PasswordInput/NumberInput/FilePathInput/KeybindingInput/ComboBox, building forms (form + form_field + submit_button), wiring on_change/on_submit/on_toggle/on_pick handlers, opening/closing modals/popovers/dropdowns/tooltips/select/menus, configuring the AnimatedVisibility lifecycle on composites, using virtual_list with infinite loading, or debugging typing lag and focus issues. Not for contributing to yororen-ui itself.
development
App bootstrap and core architecture for end users building a gpui desktop app with Yororen UI (yororen_ui). Use when generating or refactoring main.rs, window setup, the one-call renderer install, the i18n bootstrap, NotificationCenter global, custom theme JSON, project module layout (state.rs, *app.rs, components/), Entity<T> state pattern, or theme reads via cx.theme(). Not for contributing to yororen-ui itself.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.