Skip to content

dwui 0.10.0: visual overhaul and suite expansion - #26

Merged
JedimEmO merged 14 commits into
mainfrom
dwui-visual-overhaul
Jul 26, 2026
Merged

dwui 0.10.0: visual overhaul and suite expansion#26
JedimEmO merged 14 commits into
mainfrom
dwui-visual-overhaul

Conversation

@JedimEmO

Copy link
Copy Markdown
Owner

Summary

A visual and accessibility overhaul of all 21 dwui components, plus eight new ones. Breaking where it needed to be — the gallery app migrates in the same series. See the dwui 0.10.0 CHANGELOG entry for the full details.

Design-language foundation

  • Replaces the global *:focus { outline: none } reset with an opt-in, outline-based focus standard (dwui-focusable, dwui-field-surface). Text inputs, selects, sliders, the date-picker field, and breadcrumb links are keyboard-visible for the first time, and rings can no longer be defeated by shadow-* utilities.
  • New semantic tokens: --dwui-on-accent, tinted dwui-surface-* status backgrounds, and a dwui-scrim backdrop — all hard-coded whites/blacks now follow the theme.
  • Documented radius/height/padding/transition scales and a theme::layers z-index scale.

Field system

  • The notched label mixin (label.len() * 9px) is replaced by field_surface_mixin + field_error_row: transform-only floating label, always-reserved error row, no layout shift on validation. Invalid inputs no longer shrink.
  • text_input/select/slider gain disabled; slider gains a real is_valid; the select's native option popup is theme-colored via color-scheme.

Control chrome & surfaces

  • Themed slider track/thumb (per-vendor rules, UA-gated so insertRule can never panic either engine), themed select chevron, on-accent checkbox check and switch knob.
  • Card default padding + completed schemes, modal focus trap + scrim, per-variant tinted alerts with icon dismiss, bare-element headings.

New components

text_area!, number_input!, radio_group!, pagination!, popover!, dropdown_menu!, drawer!, and toasts!/Toaster — full ARIA/keyboard patterns, all in the gallery.

Overlay portals

modal!, drawer!, and toasts! render through a new utils::body_portal: position: fixed resolves against the nearest transformed ancestor, and the gallery's route transition was capturing overlays. Falls back to the document element for apps that replace <body> wholesale.

Test plan

  • wasm-pack test --headless --firefox crates/dwui: 51 component + 19 styling browser tests, all passing.
  • cargo test -p dwui: 17 native unit tests.
  • Visual QA in real Chrome via playwright-core: dark + light gallery, keyboard focus rings, drawer/toast/menu/select interactions — zero console errors (this pass caught and fixed two Chrome-only panics the Firefox suite can't see).

🤖 Generated with Claude Code

JedimEmO and others added 14 commits July 25, 2026 19:13
Replace the global *:focus{outline:none} reset with an opt-in focus
standard: dwui-focusable (outline-based :focus-visible ring) and
dwui-field-surface (:focus-within ring), both driven by the primary
palette. Every interactive component swaps its box-shadow ring + inline
outline suppression for the marker class; breadcrumb links, text_input,
select, and the date-picker field gain keyboard focus for the first time.

Add semantic tokens: --dwui-on-accent (color painted on primary-filled
controls) with a with_on_accent builder, tinted dwui-surface-* status
backgrounds via color-mix, and a dwui-scrim backdrop token. Remove the
now-dead dwui-ring-* utility maps.

Document the radius/height/padding/transition scales and add a
theme::layers z-index scale for upcoming overlay work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old labelled_rect_mixin drew its outline notch from four absolutely
positioned divs sized by label.len() * 9px, which broke on proportional
fonts and multi-byte labels, and its error message appeared and
disappeared, shifting layout — text_input even shrank from h-10 to h-6
while invalid.

The new mixins/field_mixin.rs provides field_surface_mixin (filled
surface, dwui-field-surface :focus-within ring, state-colored underline,
floating label positioned purely by transform) and field_error_row (an
always-present message row with reserved height, role=alert only while
it has a message). text_input, select, slider, and date_picker migrate
onto it; select's render_function= oddity becomes render_fn=, the
date-picker weekday header stops leaking the app's font-code class, and
text_input/select/slider gain a disabled prop. Slider validation is now
a real is_valid prop instead of hard-wired Valid.

New form components sharing the system: text_area! (rows, resize,
disabled), number_input! (native spinbutton semantics, themed
non-tabbable steppers, min/max clamping), and radio_group! (WAI-ARIA
radio pattern with roving tabindex; selection follows focus).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slider was a bare native range input themed only by accent-color; it
now gets a proper track and thumb via theme::controls, driven by a
--dwui-slider-fill custom property so the filled portion of the track
follows the value. Every vendor pseudo-element rule is inserted
separately and -moz/-webkit rules are gated by user agent, because
dominator panics when insertRule rejects a selector and each engine
rejects the other's vendor pseudo-elements.

The select drops its native chrome (appearance: none) for a themed
chevron, the checkbox checkmark and switch knob switch from hard-coded
white to the --dwui-on-accent token, and the switch knob now animates by
transform instead of magic left offsets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Card gains a default p-4 content padding (CardPadding::None restores the
old look), loses the dead ColorScheme::Secondary variant, and completes
the Primary scheme with light-mode and text colors. Heading renders the
bare h1-h6 element — no wrapper div, no fixed h-12 — and finally takes
its color from the theme.

Modal's backdrop uses the dwui-scrim token instead of hard-coded rgba,
its z-index comes from theme::layers, its sizes are rem-based, and it
now traps Tab/Shift-Tab within the dialog. The trap helper is shared so
the upcoming drawer can reuse it.

Alerts get per-variant tinted surfaces (dwui-surface-*) instead of one
shared background, and both the alert and modal dismiss buttons swap
the literal × glyph for a stroke icon. Badge Error text uses the
on-accent token and the Outline variant gets a real text color; tooltip
offsets and z-index move onto the shared scales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep: the tab underline becomes a real border-b-2 (the old inline inset
box-shadow silently defeated focus rings), list rows use a spacing
utility instead of an inline 10px pad, buttons drop the forced w-full
and rounded-full for the shared rounded-md scale with an explicit
filter/color transition, and spinner sizes move from hard-coded px to
the rem sizing scale. The transition-all / bg-white / stroke-white
grep-gate over components now comes back clean.

New widgets:
- pagination! — windowed page buttons (unit-tested pure window fn),
  aria-current, disabled edge arrows.
- popover! — controlled anchored overlay primitive with Escape and
  click-outside dismissal.
- dropdown_menu! — WAI-ARIA menu button: aria-haspopup/expanded,
  role=menu items, arrow/Home/End navigation skipping disabled items,
  Escape refocuses the trigger.
- drawer! — modal side panel sharing the modal's scrim, Escape handling
  and focus trap, sliding in via new dwui-slide-in-* keyframes.
- toasts! + Toaster — a cloneable handle pushing into an aria-live host
  above the modal layer; per-toast auto-dismiss timers ride the Dom
  future so they cancel on unmount. Adds gloo-timers and
  wasm-bindgen-futures to dwui.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gallery gains cards for every new component — textarea & number
input, radio group, dropdown menu, pagination, toasts, drawer — plus
first-time cards for card/heading and the list component, which
previously only existed as docs-sidebar chrome. The inputs card now
demos the disabled state.

Buttons no longer default to w-full, so the magnetic CTA wrappers on the
home page opt back in via apply; section cards use CardPadding::Large
instead of stacking their own padding. CHANGELOG documents the whole
0.10.0 overhaul.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA in a real Chrome session surfaced three issues the headless Firefox
suite could not:

- .style("-moz-appearance", ...) on the number input (and the redundant
  -webkit-appearance on select) panics dominator in engines that reject
  the property; plain appearance is universal now, so the prefixed
  declarations are gone.
- position: fixed resolves against the nearest transformed ancestor, so
  the modal, drawer, and toast host all broke inside the gallery's
  route-transition wrapper (an identity transform is still a containing
  block). They now render through utils::body_portal, which appends the
  overlay to document.body for the lifetime of an invisible placeholder
  and tears it down on unmount.
- The example app replaces <body> wholesale via replace_dom, so the
  portal falls back to the document element when document.body is None.

The example app gains console_error_panic_hook so future panics surface
in the console, and the overlay tests query the document instead of the
test container, discarding their DOM handles to clean up the portals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The raised label's line box reached the surface's top edge, colliding
with the inset :focus-within outline. The label now uses a tight
line-height and the field ring draws outside the surface with a 2px
offset — the same geometry as the dwui-focusable ring on buttons — so
the two can never overlap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option popup is native chrome: it ignores utility classes on the
element and takes its light/dark rendering from the element's
color-scheme, so the dark-themed select was opening a light popup with
light-gray text. The select now carries a dwui-select class with
stylesheet rules setting color-scheme (dark by default, light under a
.light ancestor) and explicit option background/text colors for engines
that paint the popup themselves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI failed with "Failed to detect test as having been run" and not one
failing assertion: the components suite never finished loading. Full
debug info had pushed that test binary from 108 MB on main to 149 MB,
and the wasm-bindgen runner only allows the page 20 seconds to fetch and
instantiate the module — enough on a dev machine, not on a two-core
runner.

Line-tables-only debug info takes the binary to 28 MB, well under the
pre-existing baseline, while panics keep pointing at real files and
lines. CI also sets WASM_BINDGEN_TEST_TIMEOUT so future growth surfaces
as a slow run rather than a phantom failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The components suite never leaves "Loading scripts..." on the runner
while passing locally against the runner's exact Chrome and chromedriver
on two cores. Verbose driver logs and the machine's resources should show
what the browser is actually doing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tailing the log only captured the polling loop. The interesting part is
the session start and any page-level event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This is what actually broke CI. wasm-bindgen 0.2.105 derives JS
identifiers for closure shims by sanitizing Rust symbol names, and two
distinct instantiations — invoke::<()> and another whose generics
sanitize away entirely — collapsed onto one name. The glue declared the
same function twice:

    SyntaxError: Identifier
    'wasm_bindgen_8081d5a46af9a2b0___convert__closures_____invoke______'
    has already been declared

The module never parsed, so no test ever started and the harness page sat
at "Loading scripts..." until the runner gave up. Whether the collision
happens depends on the exact symbols the compiler emits: rustc 1.97.1
(CI) collides, 1.96.1 (my machine) does not, which is why every local
run passed. Installing 1.97.1 reproduced it exactly, and 0.2.126
generates a unique identifier per shim.

Bumping the crate requires wasm-bindgen-test 0.3.76 (it pins wasm-bindgen
exactly), which pulls web-sys 0.3.103, and the workflow's CLI pin has to
match the lockfile or the runner rejects the artifacts.

Corrects 35b4cb5, whose message blamed the binary size: at 28 MB with a
120s budget the failure was identical, so size was never the cause. That
commit's profile change is worth keeping on its own merits — a fifth of
the bytes to fetch and instantiate per run — so its comment now says so
instead. The raised WASM_BINDGEN_TEST_TIMEOUT stays as insurance against
a future suite genuinely outgrowing the default.

Verified on rustc 1.97.1 with CI's Chrome 150 and ChromeDriver 150:
Chrome 51+19, Firefox 51+19, clippy clean, native tests, and the example
app builds and loads without console errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.10.0 goes out alongside the dwind 0.8.0 work, and absorbs the dwui
0.9.1 changes documented below it — 0.9.1 was written up yesterday but
never published. Also records the wasm-bindgen floor, since consumers on
0.2.105 can hit the duplicate-identifier collision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JedimEmO
JedimEmO merged commit 88d2998 into main Jul 26, 2026
1 check passed
@JedimEmO
JedimEmO deleted the dwui-visual-overhaul branch July 26, 2026 09:12
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.

1 participant