Repository navigation
weft_ui: accessible, styled UI components for Weft, with both demo apps on them - #17
Merged
Merged
Conversation
…ps on them The design for a package of common UI components (layout, text, cards, tables, forms, dialogs, menus, tabs, toasts, a combobox), styled with weft_css and customizable through tokens, variants, className and state attributes, with a few ready-made entrypoints, a gallery app, and weft_notes and weft_shop moved onto it. Phase 0's spikes (JSX spread, a sublayer from a dependency, token overrides, the native dialog and popover, useId across parts, an entrypoint from a dependency) are written up in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gets need For weft_ui's components: DomProps gains the ARIA attributes for forms, popups, composite widgets, ranges and live regions, a fixed set of data attributes for a component's state, inert, open_, the Popover API's attributes, more table and form attributes, and the focusin/focusout, pointer, toggle and dialog events, listened for on the element itself so the ones that don't bubble work. Dom gains contains, matches, closest, document listeners, a dialog's and a popover's methods, and more of Event. A test renders every new prop with Html and the client and compares the DOMs, and another hears each new event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dgets need A variable's initial value was declared on :root, in weft.base, as an app's own value for it would be: which won depended on which sheet loaded last (phase 0 of the weft_ui plan found the library's default winning with the sheets reversed, in every browser). On :where(:root) it has no specificity, so any other value wins. The properties weft_ui's components need are added: logical properties, anchor positioning, and a few for text, tables and form controls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new package, @e-cline/weft_ui (namespace WeftUi): accessible, styled UI components for Weft. Its theme is CSS variables (Tokens) named for what they're for, with a neutral default an app overrides on its outermost element. Every rule is in the layer weft.components.ui, so an app's classes win over them in any order. The static components, which call no hooks, so a server component can render them: Stack, Grid, Container, Separator, Heading, Text, Link, VisuallyHidden, Card, Alert, Badge, EmptyState, Table, List, DescriptionList, Breadcrumbs, Steps, Spinner, Skeleton, Button and IconButton. Each takes a className (added last), the ARIA props it needs and an attrs escape hatch, and the styled ones have recipes (Button.className()) for something else to look like them. Tests: every one rendered by weft_server's Render (which fails on a hook, as the control shows), their ARIA and behaviour in jsdom, and every extracted rule in weft.components.ui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new example app, the gallery the weft_ui plan calls for: a page per group of components (layout, text, surfaces, data, actions and status), each component in its variants, every example captioned in a frame of its own. The pages are server components rendering weft_ui's static components directly; the sidebar is an entrypoint navigating in place in weft_ui's link recipe, which the server hands it as a string; a busy button is an entrypoint of its own. Its harness test opens every page: each renders, hydrates as served, and shows nothing before its stylesheet loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The extraction test pins a fixture's sheet, which follows weft_css declaring variables' defaults on :where(:root) (f699843). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
borderStartStartRadius, borderStartEndRadius, borderEndStartRadius and borderEndEndRadius, for controls joined into one (a number field's buttons and input) the right way round in either direction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking a radio button unchecks the others in its group, with no event of their own, so only the clicked one's controlled checked was put back: a group whose state ignored the click ended with nothing checked. After an event, the other radio buttons with the same name in the same form get theirs back too, as React does. Found by weft_ui's RadioGroup tests; a test here fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Field (a control's label, description and error, told to the control through a context: its id, aria-describedby, aria-invalid, required, disabled; the error's region always there and polite), Fieldset, Form (its page's own errors: noValidate, onSubmit, busy, an error at its top), Input, Textarea, Select (the native one, with a chevron of its own), Checkbox (indeterminate too), RadioGroup (native radios sharing a name, in a fieldset), Switch (a checkbox with the switch role, drawn as a track), NumberField (a spinbutton: arrows, PageUp and PageDown, Home and End, a typed draft committed on blur or Enter, kept in bounds), Toggle (aria-pressed, its label the same either way) and ToggleGroup (single or multiple, one Tab stop, the arrow keys moving between items). State is controlled or uncontrolled, as Base UI's is. Interactive: each keeps state or reads its field's, so renders inside an entrypoint. Tests for each in jsdom (25 more). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sign-up form that checks itself on submit, each field's error shown, then is busy, then done; inputs in each size and state; a select-all over checkboxes, radio buttons with descriptions, switches; number fields; toggles and toggle groups. Each is an entrypoint of the gallery's own. Its harness test fills the form in, after submitting it empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notes' pages and entrypoints are weft_ui's components in the notes' colours, set as weft_ui's tokens on the page's outermost element; the theme's element rules for buttons, inputs, links and headings are gone, and what's left of the notes' own styles is the page's frame and the note's body. The forms are weft_ui's Form and Fields: the comment's name and text have visible labels, the hint describes the text's input, and a refused comment or title says why on its own field (empty, too long: the input is invalid, its error linked to it) or at the form's top (the note's gone, a request the server couldn't read). The tests find those errors as assistive technology does: through the input's aria-invalid and aria-describedby, or the form's alert. The error box is an alert, the related notes a list of chips, the comments a divided list. The harness tests (29) and the browser tests (10) pass; two pinned the title's markup and the comment form's own stylesheet, which are weft_ui's now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The palette was on the outermost element's class, which <body> can't see, so the body's colours were the palette's again. A sheet is sent with the classes it has, so the theme's global sets the tokens on :root (winning over weft_ui's :where(:root)) while its root class brings the sheet. weft_ui's README and Tokens recommend it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attrs was spread into each part's JSX, after which the part's own props were assigned, absent ones too: an unset ?style wrote undefined over attrs.style (phase 3's migration hit it twice, and the compiler can't see it). Host's withAttrs rebuilds the part's host element with attrs under its props, overriding only the props it set, through Weft's Element.classify and fromNode; every part uses it (or Host.element's ~attrs) in place of a spread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dialog (the browser's <dialog>, opened with showModal from a layout effect, its close event closing the state and putting the focus back on the trigger, which WebKit doesn't; a click on its backdrop closes it), AlertDialog (an alertdialog, not dismissed by a click outside, the focus on Cancel), Drawer (a dialog pinned to a side), Popover (an auto popover its trigger opens by popovertarget, so before hydrating too, placed by CSS anchor positioning, flipped when there's no room; the focus in as it opens, back as it closes), Menu (the menu button pattern: ArrowDown and ArrowUp open it on an end, the arrows, Home, End and typeahead in it, disabled items skipped, Escape, Tab and a choice closing it), Tooltip (a manual popover on hover after a delay or on focus, naming an icon trigger or describing a text one) and Toast (a store, the default one or createManager's, read with useSyncExternalStore; a polite region, a danger toast an alert; each closing after its time, paused while the pointer or focus is on them). Ready-made entrypoints, with data props and their own state: DialogEntry, DrawerEntry, PopoverEntry and ToastViewport. Testing.installJsdomShims stands in for <dialog>'s methods and the Popover API in jsdom (which hides every popover, and won't focus in one: the shim shows those it opens), for weft_ui's tests and apps'. Tests for each overlay (17); the browser checks are the gallery's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DialogEntry, DrawerEntry and PopoverEntry rendered by the page's server component, with server-rendered content; an alert dialog, a menu, tooltips and toast buttons as gallery entrypoints; ToastViewport in the frame. Its harness tests add a toast and choose from the menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…testing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tabs (the ARIA tabs pattern: one Tab stop, the arrow keys along the orientation moving and, with automatic activation, selecting, past disabled tabs, Home and End; panels kept mounted, hidden; an underline or pills look; vertical too), and TabsEntry for a server component's tabs with server-rendered panels. Accordion and Collapsible, on the browser's details and summary: static, so a server component renders them, and they open with no JavaScript; an accordion's items sharing a name are exclusive (the name is each item's, as a static component can't hand it down through a context). Combobox (the ARIA editable combobox with a listbox popup): the app owns the text and the suggestions; the combobox has the arrows highlighting them as the input's aria-activedescendant, the focus staying in the input, Enter choosing (or left alone with nothing highlighted), Escape, Tab and leaving closing, the pointer highlighting and choosing; its popup a manual popover under the input, aria-expanded true while the app renders one. The jsdom shim gains a no-op scrollIntoView. Tests for each (11 more). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TabsEntry, an accordion and a collapsible rendered by the page's server component; vertical, manually activated tabs and a fruit combobox as gallery entrypoints. Its harness tests switch the server's tabs and choose from the combobox with the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nent Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store's look is weft_ui's tokens in its colours (Theme.res); its cards, badges, forms, tables, steps, alerts and empty states are weft_ui's, and its own Tabs and Toaster are gone (TabsEntry, Toast). The sign-in dialog and the cart drawer are native dialogs, the account links are a menu, the search suggestions a combobox, and the questions an accordion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each page, and each overlay while it's open, is checked against WCAG 2.1 A and AA once what's animating in has finished. The gallery gets its own browser tests: axe, and its overlays, tabs and combobox with the keyboard, in Chromium, Firefox and WebKit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…board, and its changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… both The switcher puts weft_notes' or weft_shop's palette (a class setting weft_ui's tokens) on the page's outermost element, and keeps it in a cookie for the server to render the next page in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omponents-c99930 # Conflicts: # examples/weft_shop/src/server/Ui.res # examples/weft_shop/src/server/Ui.res.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@e-cline/weft_ui: accessible, styled UI components for Weft, built onweft_cssand the platform's own elements (<dialog>, the Popover API, CSS anchor positioning,<details>, native inputs). Both demo apps now run on it, and a new gallery app shows every component. The design and the notes on each phase are in docs/weft_ui_plan.md.What's in it
packages/weft_uihas 45 components:TabsEntry,DialogEntry,DrawerEntry,PopoverEntryandToastViewport.className, in the cascade layerweft.components.ui, so an app's classes win.attrs, for any other DOM prop.Testing.installJsdomShims()adds enough of<dialog>and the Popover API to jsdom for the overlays to work in tests.weft_dom: the ARIA, data and popup props, events and DOM bindings widgets need;noValidate; controlled radio buttons keeping the one their state checks.weft_css: variable defaults on:where(:root)so an app's values win in any load order; logical and anchor-positioning properties; logical corner radii.weft_notesandweft_shopmigrated onto weft_ui, with some behaviour changes:examples/weft_ui_gallery, a new app showing:Testing
pnpm run testandpnpm run checkpass. Three lint warnings in the Vite plugin'stesting.tspredate this PR.Not in v1
Listed in the plan, dark mode first.
🤖 Generated with Claude Code