Skip to content

[00135] Implement Rust Widget APIs for Blade Navigation and Advanced Layout Components - #132

Merged
rorychatt merged 7 commits into
mainfrom
tendril/00135-ImplementRustWidgetAPIsForBladeNavigationAndAdvancedLayoutCo
Aug 10, 2026
Merged

[00135] Implement Rust Widget APIs for Blade Navigation and Advanced Layout Components#132
rorychatt merged 7 commits into
mainfrom
tendril/00135-ImplementRustWidgetAPIsForBladeNavigationAndAdvancedLayoutCo

Conversation

@rorychatt

Copy link
Copy Markdown
Contributor

Fixes #125

00135 — Implement Rust Widget APIs for Blade Navigation and Advanced Layout Components

Issue #125. Worktree:
Worktrees/Rusty-Framework, branched from c528820 on main.

What shipped

Five Rust widgets that previously existed only as React components, plus the adapter, harness,
test and doc work that makes them usable and gated.

File Change
rusty/src/widgets/blade.rs new — Blade, BladeContainer
rusty/src/widgets/breadcrumbs.rs new — Breadcrumbs, BreadcrumbItem
rusty/src/widgets/pagination.rs new — Pagination
rusty/src/widgets/toolbar.rs new — Toolbar, ToolbarItem, ToolbarItemVariant
rusty/src/widgets/mod.rs four pub mod + four pub use lines
rusty/src/shared/widget_names.rs five ivy_widget entries; three counts 38 → 43, 25 → 30
rusty/src/shared/ivy_node.rs IVY_EVENT_NAMES 8 → 12; five to_ivy_node tests
rusty/src/core/event_registry.rs four EventName variants + case-insensitive On strip (deviation, below)
rusty-server/src/bin/widget_harness.rs four WidgetKind variants, arms and impl View apps
e2e/app/index.html five renderWidget case arms + CSS
e2e/tests/widgets/navigation.spec.ts new — 10 specs
rusty-docs/docs/03_widgets/34_blade.md37_toolbar.md new — four pages

Commits: 59eeb8e (widgets), 659f5dc (adapter), 45bfa47 (event names), 4d079bc (e2e),
d81cc62 (docs), fb73f7c (cargo fmt).

Design decisions worth knowing

Items are props, not widgets. BreadcrumbItem and ToolbarItem are plain serde structs, so
they carry no widget id and cannot hold a closure. The frontend fires one event on the widget
OnItemClick with an index, OnSelect with a tag — and reads per-item booleans
(hasOnClick) to decide how to render. ToolbarItem.children is Vec<ToolbarItem>, not
Vec<Element>, which is also what keeps it clear of widget_checks.rs's rule that a Vec<Element>
field not named children silently loses ids.

ToolbarItemVariant serializes PascalCase on purpose. ToolbarWidget compares
item.variant === "Group" / "Separator", and that comparison is on a nested value.
ivy_node's ENUM_PROPS recasing only reaches top-level props, so a rename_all here would
produce "group" and silently break group rendering.

page is 1-based, 0 means nothing selected. That is how PaginationWidget reads it (it
tests !page), so Pagination::new(0, n) is a meaningful state, not a bug.

No BladeHeader slot and no use_blades. BladeWidget's header slot needs an Ivy.Slot
child node and Rust has no Slot widget; Ivy's UseBlades push/pop controller is the app's own
use_state. Both are recorded in blade.rs's doc comments, as the plan required.

Deviation from the plan: EventName

The plan stated that no EventName variant was needed, because "the custom-name fallback in
event_registry.rs covers it" and a browser OnItemClick "normalizes to itemclick and reaches the
handler".

That was wrong about the code as it stood. normalize used s.strip_prefix("on"), which is
case-sensitive: "OnClose" lowercased to "onclose", matched no variant, and canonicalize
returned the raw "OnClose" — never equal to the registered "close". The plan's own Tests
section requires registry.dispatch("w-0", "OnClose", Null) to fire on_close, and ivy_node's
module doc already claimed PascalCase wire names are accepted.

45bfa47 therefore:

  • adds Close, Refresh, ItemClick, Select variants with as_str / from_str arms and
    extends the existing round-trip test's all array;
  • makes the prefix strip accept On as well as on, gated on the following character being
    uppercase, so Online / online keep their on;
  • adds test_from_str_accepts_ivy_pascal_case_wire_names.

This makes the plan's specified tests pass rather than rewriting them to match broken behaviour. It
also fixes the same latent gap for every pre-existing PascalCase event name (OnClick, OnChange,
…), which is why it is called out here rather than buried.

Verification

Verification Result Evidence
RustFmt Pass 1 diff fixed (fb73f7c), re-check exit 0
RustClippy Pass --workspace --all-targets --no-default-features -- -D warnings, exit 0, 0 warnings
VitePlusCheck Skipped no src/frontend change; run anyway — 563 files formatted, 548 lint-clean, tsc -b exit 0
RustBuild Pass exit 0, 0 warnings
NpmBuild Pass tsc -b && vp build exit 0; only the pre-existing chunk-size warning
RustTest Pass 771 passed, 0 failed, 4 ignored; 37 tests added; inventory gate: 741 → 778, nothing removed
VitePlusTest Skipped no src/frontend change; 1 pre-existing failure (ci-step.test.ts), proven out of scope
PlaywrightE2E Pass 152 passed, 1 flaky, 0 failed of 153, including 10 new; check-harness-script.js passes
CheckResult Pass plan matched section by section

The one red test anywhere is src/frontend/src/__tests__/ci-step.test.ts, which asserts the
literal gate commands in .github/workflows/ci.yml. Those gained --no-default-features and the
assertion was never updated. git diff --name-only c528820..HEAD touches neither file, so every
input is byte-identical to main — it fails there too. Details in Verification/VitePlusTest.md,
carried into Artifacts/recommendations.md.

Environment notes

  • npm ci in e2e/ fails with UNABLE_TO_GET_ISSUER_CERT_LOCALLY unless you pass Node's bundled
    CA store: NODE_OPTIONS=--use-bundled-ca npm ci works and is what the recorded run used.
    pnpm install in src/frontend needs no flag (it resolves from the local store).
  • One pre-existing Playwright flake, text_input read_only › is writable when read_only is not set
    — a page.goto ERR_ABORTED inside navigateToHarness, passing on the configured retry. The
    same suite ran clean earlier in the session on the identical tree.
  • Everything ran in the worktree; the original repo at /Users/rorychatt/.tendril/Repos/Rusty-Framework
    was read only. Worktree left on disk, git status clean.

Commits:


Created using Ivy Tendril.

rorychatt and others added 6 commits August 10, 2026 11:02
…r widgets

Five navigation/layout widgets that the React frontend already ships but Rust
could not build (issue #125). Each is a #[derive(Widget)] struct with a fluent
builder, wire props matching the frontend's expectations (numPages camelCased,
per-item hasOnClick on a breadcrumb, PascalCase ToolbarItemVariant because
ToolbarWidget compares it verbatim on a nested value), and state event callbacks.

BreadcrumbItem and ToolbarItem are plain serde props rather than child widgets:
they carry no id, so the widget fires one event carrying the index or tag.
ivy_widget gains the five mechanical entries, and the two hardcoded counts
(>= 38, assert_eq!(.., 38)) move to 43 alongside the five new constructors.
IVY_EVENT_NAMES grows to 12 with OnClose/OnRefresh (BladeWidget.tsx),
OnItemClick (BreadcrumbsWidget.tsx) and OnSelect (ToolbarWidget.tsx), each of
which some frontend widget really reads via events.includes(..).
EventName gains Close, Refresh, ItemClick and Select, and normalize now strips a
leading `On` as well as `on`. Without both, a widget registered under the
derive's `close` was unreachable from the `OnClose` the Ivy frontend sends and
that shared::ivy_node emits in its events array -- canonicalize fell through to
the raw string and the lookup missed, despite ivy_node's module doc claiming
`OnClick`, `onClick` and `click` were all accepted.

The uppercase-letter guard still applies to both prefixes, so `online` and
`Online` keep their `on`.
Adds a widget_harness app per widget, the matching render arms in the e2e
harness page, and navigation.spec.ts.

The harness page mirrors the frontend's own conditionals rather than
rendering everything the node carries: BladeWidget hides Close on index 0,
BreadcrumbsWidget never links the last crumb, and ToolbarWidget ignores an
item with no tag. A spec that asserted otherwise would pass against the
harness and fail against Ivy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four pages under 03_widgets, following 27_list.md. Each records where the
Rust API departs from Ivy's: there is no use_blades hook (the stack is view
state), and crumbs and toolbar items are props rather than widgets, so a
single widget-level handler receives an index or tag instead of one closure
per item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rorychatt rorychatt self-assigned this Aug 10, 2026
Combines this plan's blade/breadcrumbs/pagination/toolbar widgets with
main's animation/confetti/stacked_progress/wireframe widgets (PR #131),
re-derives the widget_names.rs mapped-type count (38 -> 48) from the
actual merged list, and renumbers this plan's doc pages (34-37 ->
37-40) to avoid colliding with main's newly added 34-36.
@rorychatt
rorychatt merged commit 6fdeff9 into main Aug 10, 2026
@rorychatt
rorychatt deleted the tendril/00135-ImplementRustWidgetAPIsForBladeNavigationAndAdvancedLayoutCo branch August 10, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Widgets] Implement Rust Widget APIs for Blade Navigation & Advanced Layout Components

1 participant