A parent writing @focus on a component tag never heard that component's
own input or button take focus. The runtime bound every output-named DOM
fallback listener in the bubble phase, and focus and blur do not bubble,
so the event fired on the descendant and stopped there. Nothing errored --
the binding simply did nothing.
That made a whole class of declared outputs undeliverable: button.focus,
button.blur, TextLink.focus, TextLink.blur, WysiwygEditor.focus and
WysiwygEditor.blur all advertised events they could never send.
The ui ratchet for outputs nothing emits excluded natively-named outputs
on the grounds that a native event reaches the root anyway. That holds for
click and change, which bubble, and was wrong for focus and blur. Binding
those two in the capture phase makes the exclusion honest rather than
convenient; the ratchet's comment now says so.
Also documents WysiwygEditor as the chrome shell it is: it emits none of
its four outputs itself, it forwards whatever the slotted control raises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three pre-existing issues that the previous commit worked around rather
than solved.
Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.
Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.
Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.
Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pagination reported the requested page only through its change output, so on a
server-rendered page the controls did nothing: the parent had to own client
state to react, and the emitted page never became a URL. An optional
hrefTemplate now renders the steps and page numbers as anchors, which work
before hydration and without JavaScript and give each page a crawlable URL.
Buttons remain the default for client-owned lists. End steps are clamped and
marked disabled rather than linking past the first or last page.
Output handler errors are no longer swallowed. Nothing awaits invokeOutput, so
a handler that threw became an unhandled rejection that never reached the
console and presented as a control that silently does nothing. Handler errors
are now reported as WRN-DEV-OUTPUT-HANDLER-ERROR.
Regenerates the component reference and the UI visual contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An output only reaches a parent @binding when the component calls
output.<name>(). Two separate faults meant most of the library never got
there, and both failed silently at each end.
HTML lowercases attribute names, so a parent's @sizeChange registered under
"sizechange" while the component emitted "sizeChange". The lookup missed, fell
through to a DOM dispatch, and the binding was never invoked. That made all 17
camelCase outputs undeliverable -- DataTable.pageChange and .rowClick,
Map.markerClick, ChatBubble.messageClick, LayoutSplitter.sizeChange and the
rest. invokeComponentOutput now falls back to a case-insensitive lookup, and a
csr test fails without it.
Separately, 18 components dispatched hand-built CustomEvents rather than
calling output.*. A bubbling event on the component's own root never reaches a
binding, because parent handlers live in a registry only the output proxy
reads. Card, Footer, Breadcrumb, Accordion, alert, Badge, AnnouncementBar,
AvatarGroup, ToggleCount and InputNumber now emit properly; Marquee, Map,
Timeline, List and SearchBox additionally declare the outputs they were
already firing. Dispatches on window are left alone -- that is how Toaster,
Modal and DataTable signal across component boundaries.
Verified in a browser both ways before and after: an AnnouncementBar
dispatching its own bubbling "dismiss" never reached a page-level @dismiss,
and reached it immediately once it called output.dismiss().
This corrects the audit, which called the LayoutSplitter failure "narrow and
unexplained" and read 32 dead outputs as 16 components needing a rebuild.
"Outputs work elsewhere" was an assumption; the components that worked
happened to use lowercase names and output.*. The dead-output ratchet drops
from 32 to 22, and a new test forbids the raw-CustomEvent pattern outright.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A measured pass rather than a bulk rewrite. Numbers come from the source and
from a browser.
The migration entry covers what has accumulated since 0.8.5 and would
otherwise reach upgraders unannounced: the Tabs output contract, the Sidebar
BEM rename, the layout components leaving Tailwind so their rendered class
lists changed, LayoutSplitter and CustomScrollbar changing props and outputs,
the ui.css families that were removed, and the theme tokens that now paint
where they previously resolved to nothing.
The audit records what is still wrong, with counts: 32 outputs across 16
components that nothing emits, 23 components still on the scaffold pattern, 66
without a local style block and therefore dependent on ui.css, and 10 still
using Tailwind. A test pins the dead-output count at 32 as a ceiling that only
moves down, so rebuilding a component tightens it and no new one can be added
quietly.
It also records what is not worth doing. Splitting the runtime saves 3 to 4 kB
gzipped on a first visit to a file cached for a year, and hydration costs
1.5 ms for 21 scopes across 4325 elements, so neither is a real problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section, SectionHeader and PublicPageShell move onto wire-* classes with
local style blocks, variants as data attributes. SectionHeader loses 38
utility lines, and Section stops spending one line per variant and colour
pairing: tinted and solid now select on two attributes. PageHeader was already
on the convention and only needed the audit.
examples/basic-app/app/pages/layout.wrn composes the whole set into one page.
Building it surfaced a library-wide bug. Ten custom properties were referenced
by components and defined by nothing: --wire-color-focus, --wire-color-surface-soft,
--wire-color-on-danger, --wire-color-surface-subtle and the input-* family,
plus hover and contrast for every semantic colour except primary and
secondary. An undefined custom property does not warn, it resolves to nothing,
so focus rings drew with no colour and every soft surface rendered
transparent -- 27 components referenced surface-soft alone. They are derived
in the theme now, and a test checks every token a component references against
the rendered theme CSS rather than the source, since most are generated.
The semantic spread also had to move ahead of the primary and secondary
entries so the palette keeps winning for those two.
Known and unresolved: LayoutSplitter emits its sizeChange output and the
component does fire it, but a parent binding on the tag is not invoked. The
tour page therefore points at the handle aria-valuenow rather than wiring a
handler that would never update. Outputs work elsewhere, so this is narrower
than an outputs-are-broken problem and needs its own investigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Container, Columns, Grid, Divider, Image, Link, Typography and Kbd were built
from utility classes and class: conditionals. That works only where Tailwind
is present, and every variant cost a dozen conditional lines -- Divider spent
eleven of them saying which token to paint the rule.
They now carry wire-* classes with a local style block, and variants are data
attributes the style block selects on. Divider went from eleven conditionals
to five rules, and Typography lost thirteen.
Behaviour is preserved rather than improved on. Container keeps columns and
gap even though a container is not really a grid, because applications depend
on them, and its columns default stays 2: the redesign contract test caught
that changing it would silently reflow every Container already published.
Additive only: Grid gains minItemWidth for an auto-fit track, Divider gains
dashed and dotted variants, Image gains fit, and Link gains underline.
Verified in a browser rather than by eye, since the pane cannot screenshot:
track counts match the declared columns at desktop and collapse correctly
below each breakpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both advertised behaviour they did not have. LayoutSplitter declared
resizeStart, resize and resizeEnd with no pointer handling whatsoever, so a
caller wired up @resize and received nothing, for ever, with no error, and its
props were columns, gap and maxWidth copied from a grid scaffold.
CustomScrollbar was the same shape with a scroll output.
The splitter now resizes. Dragging lives in the reactive runtime behind
data-wrn-splitter, because a pointermove fires far too often to route through
a client function and a state write made in that callback is dropped; the
resolved size is held on the container as a --wrn-split custom property and
the component grids from it. The handle is a real separator: arrow keys step
it, Home and End go to the bounds rather than to nothing, and it carries
aria-valuenow, aria-valuemin and aria-valuemax. minSize fixes both bounds so
neither pane can be dragged away and left unrecoverable.
CustomScrollbar is CSS rather than script -- scrollbar-width and
scrollbar-color with webkit rules for the engines that still need them -- and
its fake scroll output is removed rather than left unimplemented, since a
caller can listen for a plain scroll event.
The test harness needed a fix too: mount did not bind the window CustomEvent,
so the runtime built events from the host global and happy-dom listeners never
matched them, which made anything dispatched look silently lost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scroll lock was mine, and it broke every page carrying a Drawer or Modal.
Making dialog visibility testable, I replaced a size check with a data-show
check -- but a Drawer animates open, so its panel cannot be hidden with
data-show at all: display:none is not transitionable. Every closed Drawer
therefore looked open, took the body scroll lock and never released it, and
the page could not be scrolled. Both components publish data-open, which is
the signal that actually means open, and that is what is read now.
Stepper gains the wizard surface: showPanel renders each step body and shows
only the active one, the same contract Tabs uses, and controls adds Back,
Skip and Next, which becomes Finish on the last step. nextDisabled lets a form
hold the step; the component never validates anything itself, since the page
owns the form.
Stepper also gets a single root. The panels and controls were siblings of the
list, so the component had several roots and anything scoped to
data-ui-component missed most of it.
Tabs panels now slide in the direction of travel rather than fading upward.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>