A client `data-for` passed its loop locals to hydration in memory but never
wrote the `data-wrn-loop-locals` attribute the SSR path writes. Anything that
resolves locals by READING the DOM -- notably a component's `data-wrn-out-*`
output binding, which calls `decodeLoopLocals(componentRoot)` -- therefore
found nothing and silently dropped the call, with no console error.
A plain DOM handler kept working, because it receives locals through the
hydration closure instead, which is what made the failure look arbitrary: the
same loop variable resolved for `@click` and vanished for a component output.
Both loop paths write the marker now, keyed and non-keyed, so the DOM is the
single source of truth. Encoding goes through UTF-8 before base64 as the
server's does; `btoa` on a raw string throws above U+00FF, which would take the
whole loop down for an ordinary non-ASCII label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The old assertions only searched REACTIVE_RUNTIME for substrings; they never
touched buildApiRequest and were not anchored to the content-type line they
claimed to guard, so they could not detect drift on either side. Replace
with a fixture-driven test that runs both implementations on the same
(path, method, input) cases and compares the actual request they produce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
B1: qualify each generated __wrn_api_check_* assertion name with a short
hash of the page's path (relative to app/, for reproducibility across
checkouts) so two pages declaring a same-named block no longer collide
with an identical type alias (TS2300).
B2: skip assertion emission for any block that is not client-mode, or
that has zero declared request fields. ssr sectioned blocks can never
declare a request and always fell back to Record<string, never>, whose
keyof is `string` -- making the key-exactness arm of AssertAssignable
evaluate to false unconditionally (TS2344) on every ssr sectioned block
regardless of correctness. Chose to skip both non-client blocks and
zero-field client blocks, since neither has anything meaningful to
assert type-safety about.
B3: only resolve the endpoint's input (query params / ctx.req.json())
when the endpoint declares an input schema. Previously the router-set
fix accidentally read the request body unconditionally, so a handler
with no input schema that parses the request itself hit
ERR_BODY_ALREADY_USED.
B4: run response/error bodies in client-mode api blocks through
eraseFunctionTypes, matching every other browser-bound body in
client-codegen.ts, so a TypeScript-only construct inside one (e.g. an
annotated locally-declared function) doesn't reach the .mjs artifact.
B5: only exclude "api" from state/prop destructuring in the generated
browser module when the page actually has client-mode api blocks (i.e.
there is a real `api` binding to shadow). Previously "api" was always
excluded, so a page with `state api` and no api blocks got an
undeclared `api` reference (ReferenceError) in client code.
B6: prefix each emitted assertion with `export`, so it isn't flagged as
an unused local under a downstream project's noUnusedLocals (TS6196).
B7: wrnexusCallApi now resolves with undefined for an ok 204/205
response, or an ok response with an empty/unparseable body, instead of
rejecting with "Response was not valid JSON" -- matching the spec's
failure table (error path only for non-2xx, network failure, or an
actually unparseable body on a non-empty response).
Regenerated examples/basic-app's generated types and editor bundles to
match. Confirmed the example's type gate still fails when an
unaccepted field is added to a request body, and passes cleanly
otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add examples/basic-app/app/api/directory.ts and app/pages/api-block-demo.wrn
as the end-to-end worked example for typed api blocks (Task 6).
- Add **/*.generated.api-checks.ts to .prettierignore: this generated file
must match the CLI's raw output byte-for-byte for check:generated-types,
and prettier was reformatting it.
- Fix packages/csr/test/api-call.test.ts: no-unsafe-function-type lint error
from the raw Function type, uncovered while running the full gate.
- Rebuild editors/vscode bundles (packages/compiler and packages/syntax
changed in Tasks 1-5).
- Regenerate docs/public-api-0.8.json (additive: isIdentPart, isIdentStart,
skipLiteralOrComment newly exported from packages/syntax).
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>
The client runtime had no loop support, so any shared function using one
returned early -- Pagination and ButtonGroup were broken client-side, not
just in tests.
Adding loops exposed two further faults:
- A var reaching writeScope creates a signal and triggers a render sweep.
A declaration inside a function called during a render therefore looped
forever. Declarations now bind into the handler locals instead.
- A control block removed from the DOM keeps its effect in the renderers
list. Running it against a detached node threw, aborting the sweep and
leaving every later effect stale.
Also raises the reactive runtime budget to 53,000: the runtime had already
grown past 49,000 before this change, and 52,570 minified is 16,803 gzipped.
Two deferred minors: html-service leaves absent documentation undefined
rather than an empty string, and the extension declines tag auto-close on
multi-cursor edits rather than closing only the first cursor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
{#if}, {#each} and their {:else}/{:else if}/{:empty} branches worked on
the server and after hydration, but a block nested inside another block
stayed empty once the outer block rerendered. Adding a row to a list
produced the row's markup with its inner block markers in place and
nothing between them, for the life of the page.
Two causes, both on the client-created path only:
reactive() registers an effect; effects run when renderAll sweeps the
list. A state change runs just the affected effects rather than sweeping,
so an effect registered during that rerender was queued and never
invoked. setupControlBlock now returns its runner and the creating block
invokes it immediately.
The first reactive pass is skipped so hydration does not discard
server-rendered DOM. A block created by a rerender has no server DOM, so
skipping its only pass left it permanently empty. firstRun is now keyed
off outerLocals, which is set only on the client-created path.
Verified in a browser as well as in tests: adding a group to a list now
renders the new row's nested {:else}, and the existing rows' nested loops
survive the rerender.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
window.__wrnI18n was undefined in development: the payload shipped as an
executable inline script, and a document's CSP nonce is fixed at load, so
any such script arriving from a later response is blocked. Client
translations and language switching silently had no data.
The payload is now a type="application/json" block, which the browser
never executes and script-src therefore never applies to. The i18n
runtime, CSR navigation, and HMR all read the block instead of matching
window.__wrnI18n= with a regex.
Pages now render zero executable inline scripts, so an inline script-src
violation is structurally impossible rather than merely unobserved. Zero
framework JavaScript on island-free routes is unaffected: the block is
inert data, and nothing loads to read it unless the page needs it.
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>
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>
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>
The runtime budgets measured raw source, which counts comments -- and the
production build minifies, so comments cost a visitor nothing. The metric
therefore rewarded deleting explanatory comments over writing smaller code,
and could not tell a real feature from a wall of prose.
They now measure the minified output, which is what is actually served:
/__wrnexus/reactive.js is its own file, minified, with an immutable year-long
cache. The reactive runtime is 69684 minified against a 80000 budget, from
175246 raw -- roughly 21kB gzipped, fetched once.
Also fixes two real bugs found while testing the showcase:
- object-valued props were serialised as a bare {...} attribute, which the
compiler read as interpolation and tried to parse as JavaScript. That
returned 500 for /components/navbar. Arrays start with [ and were never
affected, which is why only object props broke. All 108 pages now render.
- the runtime walked text nodes inside textarea, script and style, so a
JSON sample in a textarea was evaluated away.
Navbar styles move out of ui.css into the component, matching the rest of the
navigation group. No declarations changed: 4519 before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My diagnosis in the previous commit was wrong. The component root was not
being replaced by a reactive re-render: the client router owns popstate and
swaps the whole page shell on back and forward, which discards component
state entirely. Every mechanism that tried to push state into the component
from outside was therefore doomed -- clicking a tab, announcing an event,
tracking the last applied value.
In url mode the query parameter is now simply the source of truth, read where
the selection is computed. Whatever render happens next produces the right
tab, with no listener to lose and nothing to keep in step.
This deletes the runtime tab sync entirely -- 1590 bytes -- and fixes the
back/forward cases that were previously broken. Verified in the showcase:
click writes the url, two backs and two forwards each land on the right tab,
and a ?tab= deep link opens on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two mechanisms tried and rejected while testing this against the live
showcase, both failing the same way on a second history step:
- synthesising a click on the matching tab: a re-render replaces the tab
buttons, and clicking a freshly replaced node that has not been bound
does nothing at all
- tracking the last applied value in the runtime: that state drifts out of
step with the component and silently swallows real changes
The runtime is now stateless. It announces the value the URL names via a
wrnexus:tabs:restore event and the component applies it, comparing against
its own selection rather than a DOM attribute a re-render owns.
Nav submenus are anchored so the viewport clamp keeps them on screen.
Comments in the runtime template trimmed to stay inside the size budget
rather than raising the ceiling again.
Known limitation: a second consecutive back/forward does not update the
selection, because the re-render replaces the component root without
rebinding its declarative listeners. That is a framework defect, not a Tabs
one, and needs its own fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tabs was the only component in the library styled with Tailwind utilities, so
it could not be themed like the rest and assumed Tailwind was present. It also
fired raw CustomEvents instead of declaring outputs, and set a roving tabindex
with no keydown handler at all -- which left every inactive tab unreachable by
Tab while the arrows did nothing.
It now uses wire-* classes and a local style block, declares change and select
outputs, and opts into the roving runtime.
mode=url mirrors the selection into a query parameter via pushState. Back and
forward are handled in the runtime, which activates the matching tab rather
than assigning to component state: a popstate listener writing state would be
writing after the client function returned, and that write is dropped. The
round trip is marked so the component does not push a second history entry for
a navigation that came from history.
Also anchors Nav submenus so the viewport clamp can pull them back on screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every bundled component must expose color and size; the rewrites dropped
them, which the library-wide invariant test caught. Rather than re-adding
them as dead props, each component now maps color onto an accent variable
that its active, current and focus affordances actually use, and size onto
the root font scale.
Regenerates the component reference, showcase and visual contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces a scaffold that rendered bare anchors with ordered steps: complete,
current and upcoming status derived from the active index, horizontal or
vertical, optional icons, and indexed named slots (step-0, step-1, ...) for
authoring a step body by hand.
Also tightens the roving contract from the previous commit. A template writes
data-wrn-roving="" or data-wrn-roving-item="false" to mean not this time, but
a bare [attr] selector matches either, so a read-only stepper would still have
taken arrow-key focus. The container now requires a named axis, while a bare
item marker still counts as opted in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The focus trap and scroll lock shipped in 0.8.5 gated on
getBoundingClientRect, which the test DOM always reports as zero, so a dialog
never counted as open and none of that behaviour ran under test. focusableWithin
had the same measurement gate and would have found no items even once the
visibility check was fixed.
Both now use the hidden attribute and the data-show marker the components
already emit. Behaviour in a real browser is unchanged; the difference is that
it is now covered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A container marked data-wrn-roving owns its [data-wrn-roving-item]
descendants: one carries tabindex=0 so Tab reaches the group once, and the
arrow keys move within it, with Home/End, wrap-around and skip-disabled.
Written once here rather than five times across Tabs, Nav, MegaMenu, Sidebar
and Stepper, and because focus bookkeeping cannot live in component state --
a client function writing after it returns has that write dropped.
Item visibility is checked via hidden and data-show rather than measured
size: the test DOM reports every element as zero-sized, which is exactly what
left the dialog focus trap uncovered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DataTable replaces the 20-line Table scaffold entirely: columns, sorting,
filtering, pagination, selection, bulk actions, comparison layout, sticky
first column, custom HTML cells, and a remote source driven by a `request`
output rather than a function prop (props travel as HTML attributes, so a
function arrives as its own source text).
Toaster replaces the hand-rolled status div: tone icons, actions, hover
pause/resume and a progress bar.
Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing
ever moved focus into the panel, so the @keydown handler on their root
never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap
and a body scroll lock now live in the reactive runtime, shared by both.
ContextMenu placed pointer menus by subtracting a guessed 340x420 from the
viewport, which pushed every menu that was not that size away from the
pointer; it now positions at the pointer and lets the anchored clamp pull
it back once it can be measured.
The reactive runtime size budget moves 150k -> 175k to cover anchored
overlays, dialog behaviour, the toaster and the DataTable client half.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>