Skip to content

Releases: jooy2/neba

1.19.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 12:56

Breaking changes

  • neba/a2ui needs @a2ui/react and @a2ui/web_core 0.12. The optional peers were ^0.11.0 and are ^0.12.0, because 0.12 changed the Catalog the adapter builds, so upgrade both together with this release. Two changes in 0.12 reach the code around the adapter. MessageProcessor no longer takes its component type from the catalog, so its surfaces type-check as A2uiSurface's only when it is written new MessageProcessor<ReactComponentImplementation>([createNebaCatalog()]). And it refuses a surface whose messages name another version than the catalog, or none: every message has to carry the catalog's version, "version": "v0.9" unless the protocolVersion below asks for "v1.0", where 0.11 read any. Everything else the catalog allows keeps working, although 0.12 declares two arguments narrower than it does: a numeric check still reads a field's text, openUrl's url may still be bound to the data model, and formatDate still writes a date that has no time in Safari, where 0.12 alone writes an empty string.

  • The A2UI catalog's formatNumber, formatCurrency, formatDate and pluralize write en-US when createNebaCatalog is given no locale, rather than the runtime's language, so a server and every browser write the same text. formatDate also writes the names of months and weekdays in the catalog's locale, where it used to write them in English whatever the language. Both come from @a2ui/web_core 0.12, and the catalog now tells the model so. Pass the locale your NebaProvider has to keep all four in the components' language.

  • A disabled Accordion section's header and a disabled Collapsible's header are out of the tab order. @base-ui/react 1.9.0 gives the trigger the native disabled attribute, where 1.8.0 kept it focusable and marked it aria-disabled, so a keyboard reader no longer stops on a header that cannot open. A test that focused a disabled header should expect the focus to pass it by.

Added

  • createNebaCatalog takes protocolVersion, 'v0.9' by default or 'v1.0', so an agent that writes v1.0 messages can be drawn without rewriting their version. Both draw with the same eighteen components and fourteen functions. A v1.0 component's accessibility.live, accessibility.hidden and metadata are accepted and not read, and a check runs as it does under v0.9. One catalog draws one version, so a host whose agents write both keeps a MessageProcessor for each.

  • An A2UI action's userMessage reaches the host, resolved, beside its name and context. v1.0 gave an action's event the line a host shows as what the reader asked for, and the v0.9 renderer dropped it; under @a2ui/web_core 0.12 it refused the whole button instead. The adapter accepts it, and a userMessage written as a path or a formatString call arrives as text.

Changed

  • @base-ui/react is on 1.9.0, which is the floor dependencies declares. It is the only runtime dependency that moved. A Chip, a LineChart, an Image, a Gallery and an InlineCitation are each 0.2 kB smaller and a DataTable 0.2 kB larger, while a twelve-component app is 0.4 kB larger, a twenty-five-component one 1.5 kB and every export 3.0 kB. Neba's own code weighs what it did.

Fixed

  • A Carousel with autoPlay below the fold no longer turns once before it learns it is out of view. Its timer started straight away and the observer that pauses it answers a frame later, so a short interval could turn the strip once while nobody could see it. The timer now waits for that first answer.

Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.

1.19.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 11:40

Breaking changes

  • A Transfer's lists are narrowed just after a letter reaches the search box, not in the same render. Each keystroke filtered the list and drew the rows it kept before the letter could appear, and every row of both lists, each a full Checkbox, was drawn again on every keystroke and every tick. The box now shows the letter at once and the list follows a moment later, and a tick redraws only the row it changed. A test that checks a row is gone straight after typing should wait for it to leave, with await expect.element(row).not.toBeInTheDocument() rather than row.query().

  • A TreeSelect's tree is narrowed just after a letter reaches its search box, not in the same render. A keystroke filtered the tree and drew what it kept in the same task, so a short query against a tree of a few thousand nodes, which keeps most of them and opens every branch it kept, held the letter back until all of it was drawn. The box now shows the letter at once and the tree follows a moment later. A test that checks a row is gone straight after typing should wait for it to leave, with await expect.element(row).not.toBeInTheDocument() rather than row.query().

  • A Sparkline's name is on its box rather than on its <svg>. role="img" and the label were on the strip's <svg>, which is only drawn once the strip has measured itself, so a server render named nothing and a crawler or a reader without JavaScript got a list of numbers with nothing saying what they were. The box that holds the drawing is in the HTML from the start and carries both now. A test that found the strip by its role and read it as the <svg> should take the <svg> inside it. A Sparkline given width as a number also no longer measures its box, and draws once instead of twice when it mounts.

  • A legend with showValue keeps room for its numbers while no column is active. The number beside each series' name appeared only while the pointer was over the plot, so every row grew as it entered and shrank as it left: a legend under the plot pushed the page below it down and back up, a layout shift of 0.0235 each way on a five-series chart, and one beside the plot took its width from the plot. Each row is now as wide as the widest number its series can show from the start, with the number hidden until a column is active, and the legend no longer changes size. The rows are wider at rest than they were, so a legend under the plot may take another line; a layout that relied on the narrower rows should allow for it.

  • A StreamingText answer goes back to plain text once its stream has ended. Every word that streamed in stayed an element of its own for good, so a long chat session kept one per word of every answer: fifteen thousand for fifty answers. Once streaming is off and the last word has faded in, the .neba-stream-word elements are gone and the answer is one run of text, which looks the same. A test that looked for .neba-stream-word after a stream ended should look while streaming is on, or read the text. A finished word's fade also no longer stays on it as a held animation.

Where the bytes went

What you import 1.18.1 1.19.0
Button 5.0 kB 5.0 kB
Chip 3.4 kB 5.5 kB
LineChart 17.1 kB 18.1 kB
CodeBlock 5.8 kB 5.9 kB
Image 9.0 kB 9.5 kB
Gallery 12.3 kB 12.8 kB
DataTable 65.4 kB 34.9 kB
CommandPalette 38.7 kB 1.9 kB
InlineCitation 31.7 kB 6.7 kB
a whole page shell 30.5 kB 13.8 kB
12 components — a typical app 74.5 kB 75.6 kB
12 components, with Korean 78.7 kB 79.8 kB
25 components — a large one 122.2 kB 123.8 kB
all exports 304.3 kB 317.9 kB
neba/styles.css 26.4 kB 27.2 kB

Each figure is what a page downloads before it draws, gzipped, with react and react-dom external. The four rows that fell are the parts of Base UI a page used to carry for something closed: a Sidebar's drawer, which a desktop page never opens, a paging DataTable's rows-per-page Select, which a scrolling table never draws, a CommandPalette's whole dialog, and an InlineCitation's preview card. Each is now a chunk of its own, fetched when it can first be needed.

A Chip alone grew 2.1 kB for href and render: the safe rel and href rules and the words a link that opens a new tab ends with, which every link in the library carries, and Base UI's useRender, which a page with a Button on it already has. An Image and a Gallery grew 0.5 kB for the shared observer a Skeleton now holds its sweep still with while it is off screen, and the helper that runs a React 19 ref's cleanup. A LineChart grew 1.0 kB for the hidden table's rows added in runs, the marks laid out once per layout, the transition it draws in after hydration and the room a legend keeps for showValue. The two apps grew 1.1 and 1.6 kB, spread across what they import: useToastActions, a Chip's link support, the sampling of labels that are elements, which a Select and a legend share, a tab bar that binds the wheel only while it overflows, a Select that can be opened from outside and the ref helper.

All exports grew 13.6 kB. About 3.6 kB of that is the code of the new chunks, which a page that imports everything counts as well, and much of the rest is those chunks being compressed as separate files rather than as one. Class strings written as concatenations rather than as an array and a join took 0.1 to 0.3 kB back off every scenario that imports more than one component. The stylesheet grew 0.8 kB for the server-rendered tab indicator, the reduced-transparency surfaces, the text effects' final text without a script, AnimateLighting's composited ring and the rules that hold a waiting entrance in place.

Added

  • autoComplete on Select and Combobox, so a browser's autofill can fill them in with the rest of a form: autoComplete="country" on a country list. It is written on the hidden input that submits the value, and a filled-in value that matches an option's value or label chooses that option. A Combobox's own text input keeps autocomplete="off", and a multiple Combobox takes no autofill.

  • name on Transfer, so a form submits what is on the right. Each chosen value is a hidden input under that name, in the order of value: FormData.getAll(name) reads them all, and a Form's onSubmit is handed them as an array. Nothing is submitted while nothing is chosen or while the Transfer is disabled.

  • href, target and rel on Chip, so a chip that leads somewhere, a tag to its page or a filter that is a URL, is a link a crawler can follow and a reader can open in a new tab. The label becomes an <a> that looks and focuses like the pressable label, and the onDelete button stays beside it rather than inside it. A link that opens in a new tab gets noopener noreferrer merged into its rel and says that it opens a new tab, as every other link in the library does. A selected link chip is marked aria-current="true", and a disabled chip is not a link.

  • NebaProvider warns in a development build when the scheme on <html> at mount is not the one it resolves. The provider writes data-theme only once it has mounted, so a page without colorSchemeScript(), or with one called with another storageKey or defaultColorScheme, is drawn in one scheme and changes to the other as it mounts. Call colorSchemeScript() in <head> with the provider's options. A page with no scheme of its own and a provider on system count as a match.

  • initialWidth on every chart, a GaugeChart and a Sparkline included. A chart is laid out in pixels and a server has nothing to measure, so a server-rendered chart is an empty box of the right height until the page has hydrated. Given a width in pixels, the chart is drawn at it on the server and while the page hydrates, so the drawing is in the first paint and in the HTML a crawler reads, and the measured width replaces it afterwards. It makes the server's HTML larger by the drawing: about 10 kB for a BarChart of a dozen categories, close to 400 kB for a 1,000-point ScatterChart. It is ignored when height is a CSS length, and left out nothing changes.

  • NavigationMenuItem warns in a development build when it has both an href and children. An item with children opens a panel and is drawn as a button, so the href went nowhere and nothing on the screen said so. Put that destination in the panel as a NavigationMenuLink, or drop the children to make the item a link. A production build says nothing.

  • useToastActions(): add, close, update and promise without the list of toasts. useToast also returns toasts, which is a subscription, so every component that raised toasts through it rendered again whenever any toast was added, updated or closed. A component that only raises toasts can call useToastActions instead and is never drawn again for that. useToast is unchanged, and the ToastActions type is exported.

  • CommandPaletteSlot, the type of the keys a CommandPalette's classNames takes. It was declared beside the component but not exported, so a slots object built apart from the element had no type to name. Import it with import type { CommandPaletteSlot } from 'neba'.

  • render on Chip, so a chip with an href can be your router's Link. A link chip drew a plain <a>, which reloads the page; render={<Link to="/tags/react" />} now moves without a reload, as it already does on BreadcrumbItem, TextLink and Button. Keep the href on th...

Read more

1.18.1

Choose a tag to compare

@github-actions github-actions released this 07 Oct 04:13

Added

  • CheckboxGroup: a set of checkboxes that answer one question, any number at once. The set has one value, an array of the ticked checkboxes' values, through value, defaultValue and onValueChange. It is named by its label, and its error is shown under the set and turns every option to danger. Options stand RadioGroup's distance apart, which spacing and orientation change. allValues with a Checkbox that has parent adds a select-all box that draws itself indeterminate while only some options are ticked. size, color, readOnly and name reach every Checkbox that does not set its own, and disabled reaches all of them. A native submit sends one entry per ticked box under name, and a Form's onSubmit gets them as an array.
  • spacing on Form, Fieldset, RadioGroup and List: the gap between what they hold, as an exact length. It is on Tailwind's spacing scale, as on Flex and GridContainer, so spacing={5} is 1.25rem. On a Form or a Fieldset it takes over from the gap size picks, and a Fieldset's legend keeps its type scale. On a RadioGroup it is the gap between the options, on both axes when a horizontal group wraps. On a List it is the gap between the rows, which otherwise touch, and it is ignored with dividers.
  • NebaProvider takes spacing in defaults, for the gap between fields across a product. It reaches every Form and Fieldset, and a spacing written on one of them still wins. RadioGroup and CheckboxGroup options, List rows, Flex and GridContainer do not read it, so a product that spreads its fields apart does not spread every set of options and every layout with them.

Changed

  • A RadioGroup's options stand as far apart as a Form's fields, and follow size. A column of options was 0.5rem apart at every size. It is now 0.75rem at md, the gap a Form and a Fieldset put between fields, and 0.375rem, 0.5rem, 0.875rem and 1rem at xs, sm, lg and xl. A horizontal group keeps 1.25rem along the row and puts its wrapped lines on the same steps. Pass spacing={2} to keep the old column gap.

Fixed

  • A toast raised while a modal Dialog or Drawer is open is drawn above its scrim, and its buttons take the click. The toast stack read the same --neba-z-portal as every other portalled surface, and on equal z-index the later portal is on top. The stack mounts with ToastProvider, so a dialog opened after it covered the toast. The stack now reads --neba-z-toast, which is one above --neba-z-portal unless you set it, so the order holds on a page that raises --neba-z-portal. Menus, tooltips and popovers keep their z-index; where one overlaps a toast, the toast is now the one on top.

Documentation

  • Getting started warns against importing neba/styles.css in a project that runs Tailwind v4. Its rules sit outside any cascade layer and Tailwind's utilities sit inside one, so a component's own rule beats a class passed in className for the same property, and only an important class such as px-10! gets through. Such a project imports neba/tailwind.css instead.

Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.

1.18.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 08:56

Breaking changes

  • A date or a number formatted with no locale is written in en-US on the server and while the page hydrates, and in the reader's language right after. It was written in whatever the runtime defaulted to, so a server in one language and a browser in another drew two different strings into the same element, and React threw the server's HTML away and rendered the whole tree again in the browser. This covers every component that writes a date or a number: Calendar and the pickers, the charts and their hidden tables, Statistic, AnimateCounter, Meter, the progress indicators, Slider, NumberField, ContextWindow, ColorPicker and DataTable. A page rendered only in the browser formats exactly as before. On a server-rendered page whose server already spoke the reader's language, the text now changes once, just after it appears; pass locale, or defaults.locale on a NebaProvider, to keep it in one language from the first paint. The library's own words are unaffected: with no locale they are English, as they were.

  • A run's duration is written in the language of the sentence around it. Reasoning, ToolCall and AgentSteps wrote the duration in the runtime's language and their own words in English, so a Korean browser read "Thought for 2.4초". With no locale the duration is English now, like the words around it; pass locale to have both in the reader's language. It is also never different on the server and in the browser.

  • A closed Sources, Reasoning or ToolCall panel stays in the DOM as hidden="until-found". It was left out until it opened, so a folded list of sources had no links in a server render and the page's search could not find a word in a closed thought. It works as Collapsible and Accordion do now: the content is in the HTML, hidden, and find-in-page opens it. A test that told a closed panel by the absence of its text should check aria-expanded instead; hiddenUntilFound={false} leaves a closed panel out as before.

  • AnimateCounter, AnimateScramble, AnimateSplit and AnimateTyping hold their text once. The animated copy was text in the page beside a hidden copy of the answer, so the server's HTML, textContent and a copy read "128K 0", "RESOLVING SIGNAL 7@W5#IBM$ KUUWHI", or a line followed by the same line with its spaces gone — and that is what a crawler indexed. The animated copy is drawn from a data-text attribute now, and the answer is the element's only text. The drawn glyphs can no longer be selected; a selection or a copy gets the answer. A test that read the number on screen through textContent should read data-text.

  • StreamingText draws text it did not watch arrive as plain text. A message loaded from history, text rendered on a server and text already there when a stream starts were cut into one element per word and faded in, which for a history of fifty answers was fifteen thousand animations starting at once. Only the words that arrive while streaming is on fade now. Turn streaming on while the text arrives to keep the fade.

Where the bytes went

What you import 1.17.1 1.18.0
Button 5.0 kB 5.0 kB
Chip 3.4 kB 3.4 kB
LineChart 16.3 kB 17.1 kB
CodeBlock 5.4 kB 5.8 kB
Image 8.8 kB 9.0 kB
Gallery 11.7 kB 12.3 kB
a whole page shell 30.1 kB 30.5 kB
12 components — a typical app 74.2 kB 74.3 kB
12 components, with Korean 78.4 kB 78.5 kB
25 components — a large one 121.7 kB 122.1 kB
all exports 297.7 kB 302.8 kB
neba/styles.css 26.0 kB 26.2 kB

The rows that moved are the ones this release is about, and each one paid for a page that is faster to show or to use.

A LineChart grew 0.8 kB, as every chart did: the hidden table's rows are now added after the first paint for a chart past 500 points, the layout is memoised so the pointer stops rebuilding it, and finding a formatter the library already built no longer writes its options out as JSON. That last part is in internal/format.ts, so every component that writes a number or a date carries its quarter of a kilobyte. In exchange, a LineChart of three 1,000-point series mounts in about a fifth of the time and sends 4.3 kB of server HTML rather than 68.6 kB.

A CodeBlock grew 0.4 kB for the shared observer it now waits on before colouring a block, and an Image 0.2 kB and a Gallery 0.6 kB for telling a server-rendered picture from one mounted in the browser, fetching the viewer on the first touch, and the Gallery's priority, srcSet and sizes and the server-rendered masonry. The page shell moved 0.4 kB for the room PageLayout now reserves for a fixed header and the keepMounted a Sidebar passes to its drawer.

The rest of the 5.1 kB on every export is spread across the components below: the text cut the same way on every engine for the animated text, StreamingText's blocks, DataTable's memoised rows and Select's sampler. The stylesheet's 0.2 kB is the sweeps' right-to-left rules and the classes that keep content hidden while keepMounted holds it in the DOM.

Added

  • keepMounted on Tabs, TreeView, Breadcrumb, Drawer, Sidebar, Menu and ContextMenu keeps hidden content in the DOM. It is off by default, so nothing changes until it is set. On, the panels behind every tab, the rows of a shut branch, the steps a maxItems fold hides and a closed drawer's contents stay in the document, hidden, so a crawler that does not click can follow the links in them. Those of Tabs, TreeView, Breadcrumb and an inline Drawer are in the server's HTML; a Menu, an overlay Drawer and a collapsed Sidebar render into a portal, which mounts only in the browser, so theirs reach only a crawler that runs JavaScript. A TabPanel's own keepMounted still wins over its set's.

  • autoClose on Reasoning. Turned off, the panel stays open when the stream ends instead of folding away and moving the answer below it up the page.

  • headingLevel on Drawer, so an inline drawer's title can sit at the right level of the page outline. It is 2 by default, as before.

  • rel on a BreadcrumbItem and on a ChatBubble's preview, merged with the noopener noreferrer a link that opens a new tab gets, so a card for a URL somebody posted can carry ugc nofollow.

  • priority on Gallery, and srcSet and sizes on a Gallery item. The first priority tiles are fetched eagerly at a high fetch priority and drawn from the first paint, as an Image with priority is. srcSet and sizes reach the tile's <img>, so a small tile is not sent the full-size original; with no full, the viewer chooses from the same srcSet.

  • imageWidth, imageHeight and imageLoading on a HowToSteps step. The dimensions are written on the step's <img>, so the browser reserves the picture's proportion and the panel no longer grows as pictures arrive. imageLoading: 'eager' is for a guide at the top of a page.

  • getPageHref on DataTable, handed to the footer's Pagination, so with paging="pages" the page numbers are links a crawler can follow.

  • DataTable warns in a development build when it draws more than 500 rows with virtual scrolling off by accident: virtual is on but there is no height or maxHeight for it to work with, or there is a groupBy. virtual={false} says it was meant, and does not warn.

  • NebaProvider warns in a development build when its direction disagrees with the dir on <html> at mount. A right-to-left page whose server HTML has no dir="rtl" is drawn left to right until the provider sets it.

Changed

  • A Slider's labels at min and max line up inward from their ticks. Centred, the label at either end hung half its width past the track, and in a narrow column past the edge of the column. Every other mark stays centred, and a vertical slider does the same at its top and bottom.

  • A chart with more than 500 data points adds its hidden table's rows just after it first draws, in small idle batches, rather than in the server's HTML and the first render. Four 1,000-point line charts and a 5,000-point scatter came to 40,000 table cells and 680 kB of server HTML; a LineChart of three 1,000-point series now mounts in about a fifth of the time, and its server HTML is 4.3 kB rather than 68.6 kB. The caption and the header row are there at once, and the table is aria-busy until it is whole. With JavaScript the table ends up exactly as it was, and a chart of 500 points or fewer is unchanged.

  • Charts redraw less while the pointer moves. A ScatterChart redraws only the marks the pointer left and the ones it reached, and pointing at its legend fades each series of dots as one group instead of starting a transition on every mark: 3,335 animations became 5. A LineChart or an AreaChart no longer retraces its lines as the crosshair moves, and the category axis no longer draws an empty group for each unlabelled category. Where two dots of a dimmed series overlap, the overlap now fades as one sheet, as a dimmed line does.

  • A DataTable sorts once instead of on every keystroke in its search. The sort runs over every row and the search filters what it returns, which is the same rows in the same order; a caller's filter is now called in sorted order. Only the rows that changed are drawn again when the active row moves, the body scrolls past a row, a selection the table keeps itself changes or a column is resized, and a render of the table by the caller still draws every row. A dragged selection stops auto-scrolling once it reaches the end instead of...

Read more

1.17.1

Choose a tag to compare

@github-actions github-actions released this 01 Oct 07:57

Fixed

  • A TextLink that opens a new tab leaves no space after itself. The space between the label and the "(opens in a new tab)" note for a screen reader was a text node of the link's own, so it was drawn after the glyph and the next word of the sentence stood a space away: "Terms ↗ ." in English, and a Korean particle pushed off its word. The space is inside the hidden note now, and the link's accessible name is unchanged. The same held with icon={false} and with a target="_blank" written by hand.

Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.

1.17.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 06:15

The release the second audit of every public component produced. It went through all of them again, 187 findings in all, and looked hardest at what had landed since the first audit: the agent components, labelPlacement and the notch, the chart brush, the second value axis, references and CSV export, and the A2UI catalog and its adapter.

Most of what it found is accessibility, and most of that is the keyboard and the screen reader. Controls lost the focus when a press disabled them; a DataTable's column widths, column order, counts and multiple sort were out of the keyboard's or the screen reader's reach; links that open a new tab did not say so; live regions read their buttons out or said nothing at all; and small targets were pressed below a finger's size. The pickers are fields of a Form now, a resting float label stops short of what a field draws at its end, a two-axis chart reads each axis in its own units and puts its far ticks on the gridlines, and a server-rendered Calendar hydrates cleanly.

The A2UI half is mostly the catalog telling the truth. The functions declare the arguments the protocol's implementations take, a check may leave out its message, a field that is not bound can still be changed, openUrl waits for a press, a component's accessibility is read, the published catalog names the minor version it belongs to, and the adapter carries a copy of the catalog without the prose, which is about 6.5 kB less on every page that draws a surface.

Eight changes are breaking and come first. A Switch's labelPlacement is deprecated in favour of labelSide, and engines.node is >=18.20.0. Each change below says what it was and what it does now.

Breaking changes

  • ScatterChart and TimelineChart take no brush or secondaryAxis. Both type-checked and neither could be honoured: a brush windows a series by index, which on a cloud of points is an arbitrary cut rather than a range of x, and on a timeline is a cut by row while the spans on it run past the plot; a scatter's second axis wrote its numbers in the first axis' format everywhere but its ticks. They are out of both types, and ignored when passed from JavaScript. Remove them from a ScatterChart or a TimelineChart that sets them.

  • A Rating a reader sets draws its empty stars at 3:1 against the page. They were the muted ink at 40%, about 1.7:1 on white, which is under what WCAG 1.4.11 asks of a control's state. They are at 80% now, which clears 3:1 in both themes, so an interactive Rating's empty stars are visibly darker. A readOnly Rating keeps the fainter ghost.

  • AnimateCounter, AnimateScramble and AnimateSplit render a <span> by default, as AnimateTyping, AnimateHeadline and AnimateMarquee do. A <div> is not allowed inside the <span> a Statistic's value is, which is the pairing the AnimateCounter page recommends, and inside a <p> it broke hydration. The root is inline now rather than a block; pass render={<div />}, or a block class, where the block was wanted. Their ref is typed HTMLElement.

  • An Alert's live region is its title and message rather than the whole alert. role="alert" or role="status" was on the root, and a live region is read out whole, so every announcement ended with the action's and the dismiss button's names. The role, and a role you pass, go on the column that holds the title and the message now; the ref, className and every other attribute stay on the root. A test that found the alert with getByRole('alert') or getByRole('status') to read a class or a slot should find the root instead.

  • A HowToSteps with no title writes its steps' titles at headingLevel. They stayed a level below a title that was not drawn, so the default guide under a page's <h2> put its steps at <h4> and skipped a level in the outline. A guide with a title is unchanged.

  • A ContextWindow's root is a plain <div>, and role="meter" is on the ring, the label and the count. The whole gauge was the meter, and a meter's children are presentational, so the split was not read as a list and a link passed as children was not a link. The ref, className, style and every other attribute still go to the root. A test that found the gauge with getByRole('meter') to read a class or an --n-* slot should find the root instead.

  • A Gallery viewer's Previous and Next keep the focus at the ends. Pressing Next onto the last picture, or Previous onto the first, made the button disabled, and the focus went with it. Both stay in place with aria-disabled, as a Carousel's arrows do; a test that asserted toBeDisabled() on them should assert aria-disabled="true".

  • A Transfer's move buttons keep the focus when the press runs them out. Moving every ticked row made the button disabled, and the focus fell to the document, so a keyboard reader moving rows one at a time started again from the top after each. Both stay in place with aria-disabled, as a Pagination stepper does; a disabled Transfer still disables them outright. A test that asserted toBeDisabled() on either should assert aria-disabled="true".

Added

  • The A2UI adapter reads a component's accessibility. Every component accepted it and none used it, so an agent that named a group of controls named nothing. On a Flex and a Card, label makes the root a group by that name, and on a Button with no text it names the button; description describes a Flex, a Card, a Button and an Image. The components that already name themselves from a prop they draw ignore it.

  • A ScrollArea takes a label, and its viewport always has a name. The viewport is a tab stop whenever it overflows, and it had no name, so the focus landed on something that said nothing; an aria-label passed to the component named the root instead. With label the viewport is a region by that name, and without one it is a group named "Scrollable content" in the locale's words, as a ScrollZone's strip is.

  • A TreeView moves to a row by its first letter. Typing a character moves the focus to the next visible row whose label starts with it, wrapping at the end, so pressing it again steps through the rows it starts. Accents and case are ignored, as a DataTable search ignores them.

  • A DataTable's columns resize and move from the keyboard. A resize handle was pointer-only and hidden, so a column cut short was text a keyboard reader could not get back. Each handle is now a focusable separator named "Resize" and the column's label (table.resizeColumn, in every registered language), and ← → move it 16px at a time. On a reorderable table, Alt + ← → on a heading moves its column one place, and a heading with no sort button takes the focus for it. Under the pointer, a cut cell without render shows its whole text as a tooltip.

  • A Transfer says what a move did. A polite status reads out how many rows moved and to which list, "2 moved to Selected", in the locale's words. It is transfer.moved, in every registered language.

  • createNebaCatalog takes a locale. The A2UI functions that write numbers — formatNumber, formatCurrency and pluralize — followed the runtime's language whatever the components around them were set to, so a Korean surface could say "1,234.5" in a Korean sentence and pick the English plural. createNebaCatalog({ locale }) builds them in the language given, which is the one a NebaProvider should be given too.

Changed

  • A Switch takes labelSide for the side its label sits on, and labelPlacement is deprecated there. On every field labelPlacement says where the label is drawn — above the field, on its edge, inside it — and a NebaProvider fills it in for all of them; on a Switch the same name meant which side, start or end. labelSide takes those two values now. labelPlacement is still read when labelSide is left out, and is removed in the next minor release; SwitchLabelPlacement is an alias of the new SwitchLabelSide for as long.

  • engines.node is >=18.20.0. The package said >=18.0.0, and neba/a2ui imports its catalog with an import attribute that Node has only from 18.20 and 20.10. Nothing else in the package needs more.

  • The A2UI catalog names the minor version it belongs to. Its catalogId was the bare https://neba.cdget.com/a2ui/catalog.json, served from the docs, which deploy from main — so the file behind it could describe components the adapter a host had installed did not have yet. A published catalog's catalogId is …/a2ui/<major>.<minor>/catalog.json now, served at that address, and the adapter registers the same id; the bare URL is always the latest. A host that hard-coded the old id in a createSurface should use nebaCatalogId.

  • A pressable Pill lights and presses its button rather than its whole shell. The pointer light, the hover fill and the press were on the shell, so pressing a control in endIcon or inside the open details pressed the whole Pill, and the light washed across the details. They are on the pressable middle now; an outline Pill's edge no longer changes under the pointer, since the edge belongs to the shell.

  • useShortcut runs once for a held key, and leaves a key a control already answered. Every auto-repeat of a held combination ran the handler again, and a key a focused field had handled with preventDefault ran it as well, so a field's own Mod+K also opened a CommandPalette bound to the same key. Repeats are skipped unless repeat: true is passed, and still have their default prevented; a key another handler prevented is left to it. A CommandPalette that is already open no longer asks to open again.

  • An outline Toggle that is on keeps its plate under the pointer. Hover and press swapped the dyed plate for a paler wa...

Read more

1.16.2

Choose a tag to compare

@github-actions github-actions released this 29 Sep 01:48

Added

  • labelPlacement on every field: the label above it, on its top edge, or in place of the placeholder. top is the default and what every field drew before. notch stands the label in a gap cut out of the field's top edge, and float rests it where the placeholder would be until the field takes the focus or holds a value, then moves it up into that gap. TextField, NumberField, Select, Combobox, DatePicker, TimePicker, DateTimePicker, DateRangePicker, TreeSelect and ColorPicker all take it, typed as the shared NebaLabelPlacement. A floating label travels on top and resizes on font-size rather than on a transform, so it is set again at each size instead of scaled. A startIcon keeps a float label in the notch, because the icon is where it would rest. The gap is sized by the layout rather than measured, so it is right in the first frame of a server render, and a field is never narrower than the label on its edge. NebaProvider takes it in defaults as well, so a product sets it once.

Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.

1.16.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 01:48

Fixed

  • A ContextMenu can sit inside another trigger, which is what a Tooltip on the same element needs. 1.16.0 stopped the trigger wrapping children and merged it onto them instead, which is right — and it meant a ContextMenu around a Tooltip no longer reached anything: the props were merged onto the Tooltip, which describes its own popup with what it is handed and passed none of it on. A tab with a card on hover and a menu on right-click lost the menu. The ContextMenu now passes what it is handed to the area rather than dropping it, so the two compose with the tooltip written around the menu. The other way about still does not work, and cannot while a tooltip's extra props are the popup's.

Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.

1.16.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 01:48

Breaking changes

  • A ContextMenu merges onto the element it is given rather than wrapping it. The trigger put a <div> of Base UI's around children, and nothing could reach that box — className goes to the popup — so a scroll container handed to a menu became a scroll container inside a box with no height, and its content spilled instead of scrolling. children is one element now, which the trigger merges onto exactly as Tooltip's does, and the menu costs the layout nothing. An area written as several elements, a fragment or bare text has to be wrapped in one element of the caller's own.

Where the bytes went

What you import 1.15.0 1.16.0
Button 5.1 kB 5.1 kB
Chip 3.4 kB 3.4 kB
LineChart 12.6 kB 15.7 kB
CodeBlock 5.5 kB 5.5 kB
Image 8.8 kB 8.8 kB
Gallery 11.7 kB 11.7 kB
a whole page shell 30.0 kB 30.0 kB
12 components — a typical app 71.6 kB 71.7 kB
12 components, with Korean 75.6 kB 75.7 kB
25 components — a large one 118.7 kB 118.8 kB
all exports 286.6 kB 290.4 kB

One row moved, and it is the one the release is about. A LineChart grew 3.1 kB and so did every other cartesian chart: the reference lines, the export button, the second value axis and the turn arithmetic all live in chart-frame.tsx, which is in all five of their bundles.

What is not in them is the brush. It is 1.5 kB behind a React.lazy of its own, which a chart with no window never fetches, and the CSV writer is another 0.5 kB fetched on the press. Both show in the unbudgeted async column npm run size prints beside every scenario, which is where they can be watched rather than quietly become the entry's problem.

The two app rows moved a tenth each, and neither list holds a chart: that is TextField and Select carrying fieldLight beside trackPointer, and Tabs carrying its alignment map. The page shell has neither and did not move.

Registering a language still costs 4.0 kB. The one word this release added is the export button's, in the chart namespace, and none of those twelve components reads it.

neba/styles.css is 24.6 kB gzipped against 24.5 kB: the rule that puts a field's pointer light out while the reader is typing, and the utilities the brush strip and the export button are drawn with.

Added

  • nulls on a LineChart and an AreaChart, with a third answer to a gap. connectNulls had two: break at a null, or bridge it. The one it could not say is the one a caller with an event count wants, which is that a missing row means none happened — so nulls takes 'gap', 'connect' or 'zero'. zero rewrites the data rather than the drawing, so the axis takes the nought into its range and the tooltip and the table say 0 as well; a line pulled to the baseline over a table that still says nothing is a chart disagreeing with itself. connectNulls is now the old spelling of 'connect', still read when nulls is left out.

  • nulls on a Sparkline too. The same three answers, and they matter more on a strip than on a chart: there is no tooltip and no axis to notice a hole with, so one that quietly closes over three missing weeks is a trend nobody can tell was interpolated. zero rewrites the values, so the range the strip scales itself to takes the nought in and the numbers read out to a screen reader change with it.

  • tickAngle on NebaChartAxis, which turns the category labels. Between -90 and 90 degrees. Flat, a long category name has one slot to fit in, so the axis cuts it to an ellipsis and then drops every other one; turned, each label only has to clear its neighbour across its own height, and the room a name needs stops depending on how long the name is. Seven onboarding stages that showed two labels show all seven, whole. The band under the plot grows to hold them, capped at the smaller of 140 pixels and two fifths of the box — past that they are still cut, because a chart that is mostly axis is not a chart. Read on the category axis only, and only where that axis runs along the bottom.

  • brush on every cartesian chart — a window on a series a plot cannot hold. Two thousand points is a chart with no points on it: every column is a fraction of a pixel and the shape is a smear. The strip under the plot draws all of them small, the window on it says which part the chart draws, and dragging either handle or the window itself moves it. The handles are role="slider" buttons rather than shapes inside the picture, so the arrow keys move them and a screen reader is told which category each one sits on — the strip is a sibling of the plot for that reason, since role="img" makes everything under it presentational. It is drawn inside the chart's own height, like the axis labels. The window narrows the picture and nothing else: the hidden table and the exported file still hold every point.

  • secondaryAxis, and axis: 'secondary' on a series — two value scales on one plot. For the series whose units are not the others': revenue beside a conversion rate, a count beside a duration. The second axis is drawn on the far edge — the right of a vertical chart, the top of one turned on its side — and its own tickFormat writes its series' numbers everywhere they appear, the ticks, the tooltip and the table, because the alternative is a percentage printed with the other axis' currency in the one place the two meet. It casts no gridlines: two grids on one plot is graph paper drawn twice, so its ticks land at the same count as the first axis' and the rules already there serve both. Not read on a stacked chart — a stack is a total, and a total across two units is not a number. Worth reaching for rarely, which is why the axis is drawn and labelled rather than merely implied: two scales let a caller make any two series look like they move together, and the reader cannot see that it was done.

  • exportable on every chart that has a table — the numbers as a CSV file. A picture is the one form of a number nobody can paste anywhere, and the table a screen reader gets is not reachable with a pointer; this is the pointer's way to the same thing. exportFileName names the file and onExport takes the string instead of downloading it. The module that writes the CSV is fetched on the press rather than imported with the chart, which is highlight.ts' arrangement in miniature: a page that never turns this on downloads none of it, and no bundle moved. A GaugeChart does not take the three props at all — one reading is not a sheet — and the chart's own words gained an exportCsv entry in all nineteen languages.

  • references on every cartesian chart — lines and bands across the plot. A target, an SLA, a budget, the window a forecast covers: the one thing on a chart that is neither a mark nor chrome. value places one and to turns it into a band; axis: 'category' reads the numbers against the other axis, for a rule that says when rather than how much. The scale widens to hold them, so a target above everything measured is still on the chart, and one that names itself is read out with the data — the picture is aria-hidden, so a reader who gets the table instead would otherwise never learn the target exists. Drawn dashed and neutral unless told otherwise, because a reference in danger says the line is the bad thing when usually the bad thing is the data crossing it.

  • xAxis and yAxis on a HeatmapChart. It had neither, so its column names could only be thinned by the stride — a grid of long stage names showed two of them and there was no prop to say otherwise. Both are category axes here, since the magnitude is on the colour ramp the scale legend describes, so min, max, tickCount and grid are not read; label, tickFormat, hidden and thickness are, and xAxis.tickAngle turns the column names. The arithmetic behind the turn moved into chart.ts as turnedAxis, so a heatmap and a cartesian chart on one dashboard cannot disagree about how deep a band of turned names runs.

  • hole and gap on a PieChart. shape already sets how much of the middle is cut out and hole is the dial behind it, from 0 to 0.92 of the radius; gap is the surface showing between two touching slices, in pixels rather than degrees, because the gap is a constant on screen and one that looked right on a 300px chart is a wedge out of a 60px one. hole on a pie is also what gives that shape somewhere to put a center.

  • align on Tabs. Where a tab's label sits once the tab is wider than the label is. A vertical bar is the case it exists for: every tab in a column is as wide as the longest one, and centred labels down a column read as a ragged edge on both sides. It moves the label and the icons and nothing else — the tab keeps its size, its padding and its indicator.

Changed

  • A field's pointer light is half what a control's is, and goes out while the reader types. A control is the thing the pointer acts on, so the bloom under the cursor is the control answering; a field is on the way to somewhere else, and what the reader looks at a moment later is their own text over the exact patch the bloom is brightest on. So a TextField, NumberField, Combobox, Select, PromptInput and the date and time pickers take fieldSpotlightSlot, which is the same light at 50%. And because a pointer resting on a field is where the hand left it — over the text, since that is where the reader clicked to get the caret — the one moment the bloom is least wanted is the one moment it cannot get out of the way on its own: a keystroke puts it out and the next point...
Read more

1.15.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 01:48

Breaking changes

  • A NumberField answers the wheel by default, through a prop now called wheel. allowWheelScrub was off, so the one gesture a spinner exists for did nothing until a caller went looking for the prop — and it was a second spelling of a question ScrollZone and Tabs already ask as wheel. It is wheel, and it is on. Both halves of what it asks still have to be true before anything happens: the field holds the focus and the pointer is over the number, so a reader who has not put the caret in the field scrolls the page over it exactly as before. Rename allowWheelScrub to wheel where it was passed, and pass wheel={false} for a field inside something that has to keep scrolling under a pointer resting on it.

Where the bytes went

What you import 1.14.0 1.15.0
Button 5.2 kB 5.1 kB
Chip 3.5 kB 3.4 kB
LineChart 12.7 kB 12.6 kB
CodeBlock 5.6 kB 5.5 kB
Image 8.9 kB 8.8 kB
Gallery 11.8 kB 11.7 kB
a whole page shell 30.0 kB 30.0 kB
12 components — a typical app 71.6 kB 71.6 kB
12 components, with Korean 75.0 kB 75.6 kB
25 components — a large one 118.5 kB 118.7 kB
all exports 279.8 kB 286.6 kB

Six rows went down, and all six by the same tenth of a kilobyte: three shared class strings in internal/styles.ts were […].join(' '), and a bundler cannot drop a const whose value is a call. Every component carried all three whether it read them or not. Written with + they fold at build time and drop, which is why a single-component bundle is smaller in a release that added nine components.

All exports grew 6.8 kB, and no runtime dependency was added. That is the agent group: PromptInput at 5.2 kB of its own code down to StreamingText at 1.2 kB, plus internal/run.tsx and internal/glow.ts, less the fold above. InlineCitation is the one to know about — its preview is a HoverCard, so a citation in a paragraph costs about 35 kB with Base UI's floating machinery, which is why the number is the same whether a page draws one or forty.

Registering a language costs 3.4 kB still; the extra 0.6 kB on that row is the six new message namespaces the agent components read, in Korean.

neba/styles.css is 24.5 kB gzipped against 24.1 kB. It carries the two keyframes a StreamingText word and its caret arrive on, and the classes the nine components use.

neba/a2ui and neba/a2ui/catalog.json are in none of these rows, and that is the point: neither is re-exported from neba, so a bundler walking the package never reaches them. The catalog is 58 kB of the tarball and 0 kB of any bundle.

Added

  • An A2UI renderer to go with the catalog, as neba/a2ui. createNebaCatalog() returns a Catalog for @a2ui/react's v0.9 surface with the eighteen components implemented against this library and the protocol's own fourteen functions registered, so an agent's JSON comes out as Neba components with nothing in between to write.

    The Zod schemas the renderer binds against are derived from catalog.json at load time, rather than written a second time. A hand-written mirror is a mirror that drifts, and the copy that drifts is the one the agent was never told about: a model writing exactly what the JSON allows and a renderer rejecting it. The converter handles the subset the catalog uses and throws on anything else, rather than quietly returning z.any() for a construct it has not met.

    The functions are @a2ui/web_core's own. required, formatCurrency and the rest are the protocol's semantics rather than this library's, and a second implementation of them would be a second chance to disagree with the agent about the one thing both sides have to read the same way.

    @a2ui/react, @a2ui/web_core and zod are optional peer dependencies, which is safe here because neba/a2ui is not re-exported from neba — a bundler walking the package never reaches it, so a project that imports Button and has never heard of A2UI resolves nothing new. Nothing about any bundle moved.

  • An A2UI catalog, as neba/a2ui/catalog.json. A2UI is the protocol for an agent describing an interface as JSON that the host renders with the host's own design system, and a catalog is how the two sides agree on the vocabulary. This one is written against v1.0 and is also served from the URL it names itself by, https://neba.cdget.com/a2ui/catalog.json.

    Eighteen components and the specification's fourteen functions. Eighteen is the Basic Catalog's count and the number is the decision rather than an accident: a model chooses badly from a list of a hundred and thirty-eight, and what is left out is everything whose useful props are functions, ReactNodes or render props. The names are the library's own, which makes a host's mapping a lookup rather than a translation, and the instructions field carries the four things a screen gets wrong without being told — that nothing has a margin, that colour is semantic, that size is one ladder, and that a field draws its own label and its own error.

    The catalog is JSON, so it never enters a bundle and npm run size does not move; what it costs is about 57 kB of the tarball. Take it alone and the mapping to your components is yours — or take neba/a2ui below and it is already written.

  • ToolCall — one tool invocation in an agent transcript: the name, what it was called with, what came back and how long it took, with everything but the header folded away behind it. status is the new shared NebaRunStatus — pending, running, success, error — and each of the four gets its own shape as well as its own colour, so a reader who cannot tell the two reds apart still has a dashed ring, a turning ring, a tick and a cross. color decides only what a running call looks like: a failed one is danger and a finished one is success on every ToolCall on the page, because a red row that meant the product's accent colour would have spent the one signal it had.

    A string args or result goes into a <pre> with its own line breaks kept, since indented JSON, a stack trace and a diff all mean something by where their lines break; anything else is rendered as the node it is. Leave duration out and a running call counts its own seconds, saying nothing for the first one and stopping the moment a real figure arrives. And a call that fails opens itself, unless open is holding it — a reader should not have to go looking for the reason something did not work.

  • Approval — the agent asking permission, and the record of what was answered. A Confirm and a Popconfirm are opened by the reader, offer two answers, and are gone the moment one is taken; this is opened by the agent, offers as many as the agent has, and stays, because a transcript in which the question disappears once it is answered cannot be read back to find out what was agreed to. risk is low, medium or high, written out as a word on a chip and never as a colour alone, and it takes over the card's family.

    No option is emphasised by default. Every button is outline in the card's family and an option's own variant is how one is singled out — a permission request whose loudest button is "Allow" is a request answered by the shape of the buttons rather than by the reader.

  • Reasoning — a thinking panel that the stream opens and closes. On a Spoiler and a Collapsible the reader owns the open state; here the stream does, which is the whole of what the component is for: the panel fills while the model is working, folds itself away when the working stops, and leaves one line saying that it happened and how long it took. It follows the edges of streaming rather than its value, so a reader who folds it away mid-stream is not overruled on the next token, and one that mounts mid-stream starts open because no edge is coming to open it.

    variant defaults to text here and nowhere else in the library: thinking is an aside, and a bordered box around every aside in a conversation is a conversation made of boxes. The panel is deliberately not a live region — thinking is long and is revised as it arrives, and a screen reader reading every revision aloud would bury the answer it is on the way to.

  • AgentSteps and AgentStep — a chain of steps that grows as it runs. A Timeline and a HowToSteps draw a list that is known in advance; this one does not know how many items it has, so a step's place is not a prop and appending or inserting one renumbers nothing. running draws one more marker under the last step for the case where the next step has no name yet, with no visible label, because the turning ring has already said what it is and the status is read out either way.

    A step takes the same NebaRunStatus a ToolCall does, and the same four marks; success is its default, since a step that is already in the list has usually already run. Its children hold whatever it did — a query, a file, a whole ToolCall.

  • ContextWindow — how much of the context window has gone, and what went into it. A Meter draws the same reading as a bar and would do most of it; what it cannot do is the part that makes this a component: token counts are five and six digits long and have to be written compactly in the reader's own language, the split into input, output, reasoning and cached only means anything with the four next to each other, and the money underneath is a third unit again. used defaults to input plus output plus reasoning; cached is drawn as a row and never added, because every model that reports it means "this much of the ...

Read more