Skip to content
EspañolEnglish

What's new

What arrived in each version, taken from the changelog (CHANGELOG.md) that ships in the 0.17.1 package. An entry marked Breaking changes something your code has to adjust when upgrading: npx amset-ui migrate takes care of almost all of it.

0.17.1, released October 19, 2026

Version 0.17.0 was published from a pre pipeline that predated the Flag fix, and it shipped with that bug: with SSR, the home page and any page with a flag gave error 418 on hydration. If you use Flag with a server, upgrade to this one.
  • Flag no longer breaks hydration when the server already had its region (AMS-1894). A static build renders hundreds of pages in the same process, and from the second one on the flag region was already in the server cache: Flag rendered it without a Suspense boundary, the browser (which starts from scratch) rendered it with one, and React threw error 418 on hydration. It happened on the amset.tech home page and on every page with the language picker. Now the boundary is always there and the browser keeps what the server rendered until the region arrives; a real hydration with a warm server and a cold browser tests it.
  • migrate and doctor see the old stylesheets imported from CSS (AMS-1912). migrate also rewrites the @import '@amset/ui/estilos.css' and estilos/<hoja>.css lines of your .css files, and doctor reports an error if the global CSS imports a stylesheet that no longer exists (before, it said you imported none). doctor now counts the same names migrate changes, including the 0.16 ones, and flags data-tema, which the library no longer reads.
  • npx amset-ui create: a new application with the library already set up (AMS-1786). Two templates in starters/: Next with App Router (the default) and Vite with --template vite. Strict TypeScript and ESLint out of the box, no Tailwind, the shell (AppShell, TopBar, ThemeButton and SideNav marking the current page) and each page in its own file: in Next, server pages; in Vite, React.lazy and loading ahead of time when hovering the link. Then it runs init, so they come with the .npmrc, the theme for the --brand colour and the provider, and doctor has nothing to say. It does not write into a folder that has something in it. npm run consumers creates both with the CLI of the packed package and checks that they install, pass their types and their lint, build, navigate without reloading and fit within their JS limit (measured: 150 KB gzip in Next and 85 KB in Vite).
  • Blocks: seven complete screens that npx amset-ui add copies into the project (AMS-1786). Management dashboard (dashboard), customer list (customer-list), new customer (customer-form, with its customerSchema schema to validate on the server as well), customer record (customer-detail), invoice issuing (invoice-form), settings (settings) and sign-in (sign-in). Built only with library components: they import @amset/ui like your project does and do not copy the code of any component, so they weigh nothing. npx amset-ui list --blocks lists them; add warns about what they need that your project does not have. In Storybook, under Blocks, with their states in light, dark and mobile, and translated into English. scripts/blocks.test.ts checks that they only import the library, that they do not write a single colour or size, and that the seven, once copied, compile in strict mode.
  • useLocale() is exported: the provider's language ({ locale, direction }) to format a date or an amount of your own the same way as the library. The blocks needed it, as does any application that writes its own figures.
  • Table with onRowPress: the row also opens with Enter. The row could already be focused with the arrow keys, but it only responded to clicks: with the keyboard there was no way to open an invoice from the dashboard. Enter on a control inside the row still belongs to that control.

0.17.0, released October 19, 2026

0.15.0 and 0.16.0 were never published: this is the first published version since 0.14.0, so anyone coming from 0.14 gets everything in the three sections at once (0.15.0, 0.16.0 and this one).
  • The AI components, configurable enough to put them into the assistant's chat. What the real AMSET chat does and the pieces did not allow you to configure: PromptInput accepts busyModes (keep typing while it answers -add to the turn, stop and correct, queue-: real radios above the field, and onSubmit receives the chosen mode in { mode }; "Stop" now sits next to "Send"), inputRef, actions on the right, onPasteText (a huge text is attached as a file instead of being pasted), onInputKeyDown for your own shortcuts, inputProps (spellcheck, language, maximum length) and attachDisabled. ChatMessage gains actions (your own actions next to copy, regenerate and edit), actionsVisibility, editor (edit in the same bubble) and highlighted (a search match, with a fixed ring and not an animation). ActivityLog gains group per event (a history of several tasks, with a title per section and without merging identical items from two sections), renderText and loading. And a new component, FindBar: the bar for searching within a long conversation, with a counter, next, previous and close (Enter, Shift+Enter, Escape). The second argument of onSubmit is new but does not break anyone; 36 tests with real keyboard and focus.
  • The AI pieces match the assistant's real chat (AMS-1773). AMSET's chat was read piece by piece and what it did and the library did not was added: PromptInput gains pastedFileName, a guard against the double-send bounce (the same text twice within 0.3 s), busyModeScope, history (up and down arrows, with the caret at the end) and submitOn="auto"; Attachment gains icon, detail, onPreview, compact and the "Uploaded" announcement; Thumbnail gains onPress, loading, error and caption; new ImageViewer (the near-full-screen viewer, a real dialog) and the hooks useBlobUrl and useElapsed; ArtifactCard gains onRetry; ModelSelector, maxFixes; Conversation gains loading, loadingOlder, onReachStart (keeps the reading position when messages arrive at the top), stickToBottom, revealKey, highlighted and empty as a node; Reasoning and ActivityLog count the time live (startedAt, endedAt, label, emptyIcon); Suggestions, allowPartial; UsageBadge, cachedTokens; FindBar, hasMore. Everything is optional: nobody who already uses them changes. Left out on purpose: the PDF preview (an iframe without sandbox would open the door to running foreign HTML; a piece that only accepts PDF is missing) and markdown (no dependencies in the core).
  • Two pages no longer share a title (AMS-1783): the amount field goes from "Amount" to "Amount field" and the card field from "Card" to "Card field", like "Text field" or "Number field". The page ids do not change, and scripts/docs.test.ts fails if two pages repeat a title again.
  • The coverage thresholds rise to what is measured (AMS-1763): from 80/80/75/80 to 95/94/86/92 (lines, functions, branches, statements). New tests for row dragging, the column width handle, the grid keyboard and the interaction states, which were the weakest areas (between 12% and 42%).
  • Breaking Breaking: three root classes take the name of the component that actually uses them (AMS-814). .amset-code (the one-time code field) becomes .amset-code-field with all its children and modifiers, .amset-heading (the page header) becomes .amset-page-header, and .amset-list (the list in the dropdowns' panel) becomes .amset-listbox. The --amset-code-* and --amset-heading-* variables do not change. Anyone who styled those classes from outside has to use the new names.
  • The action menu's type-to-search uses the library's typeahead (AMS-1200): useTypeahead now accepts a function to read the keys from the DOM and the menu no longer carries its own copy. It still finds "Éxito" with "e" and keeps the space as part of what is typed.
  • The tray bell and the open button of the search field and the multi-select use amset-trigger-open (AMS-1293): the two copies of the "trigger with the panel open" rules are deleted.
  • Flag no longer puts all 244 flags into the page's JavaScript (AMS-1893). The drawings are split by region (Europe, America, Africa, Asia, Oceania), each in a file that is requested when one of its flags is painted: what Flag costs goes from about 17.6 KB of compressed JS to about 1.5 KB. While the drawing arrives, the two letters are shown, and a server that waits for suspense (Next, renderToPipeableStream) already includes the flag in the HTML. New preloadFlags() to have it on a client's first paint. The package budget gains the Flag scenario with staticOnly, which only counts what is downloaded through static imports.
  • Table: the row header (rowHeader) is no longer centred (AMS-1884). The <th> inherited the browser's centring; it now aligns to the start and in bold like the rest of the cells, and align="end" / "center" still take precedence.
  • Pagination: a size that is not in the list of sizes now shows in the selector (AMS-1883). Previously the selector came out empty or truncated; now the size is added to the options, without duplicates and sorted.
  • RadarChart: the maximum mark no longer overlaps the name of the top axis (AMS-1895). It hangs below the outer outline.
  • The RelativeTime hydration test no longer depends on the machine's time zone (AMS-1896): it fixes the default timeZone of Intl.DateTimeFormat, because changing process.env.TZ does not work in Vitest threads.
  • All the code is in English: not a single variable, function, parameter, property or type in Spanish (AMS-1887). About 3,400 distinct names were renamed (more than 37,000 uses in 800 files) with the TypeScript LanguageService, not with text substitutions: claveDeFila → rowKey, esClicDeEnrutador → isRouterClick, alPulsarFila → onPressRow, ordenable → sortable. What a consumer may notice: bin/proyecto.mjs becomes bin/project.mjs; dist/fuentes/ (what amset-ui add reads) becomes dist/sources/; the ids of the amset-ui doctor findings are in English (registry, layer-order, legacy-names, provider, router, project, installed, theme-not-imported); the status of each file in add and init is new/equal/overridden/kept/modified; AppShell uses content as the default id of the <main>; the data-photo attribute of Avatar is loading/loaded. amset-ui migrate still looks for the old names (written as strings in bin/migrations.mjs); its list had initialsValues instead of valoresIniciales and did not find that prop, now fixed. It is guarded by scripts/identifiers-english.test.ts, which walks the syntax tree of the whole repository.
  • LocaleProvider no longer drags in the date picker (AMS-1885). It was defined inside date-picker.tsx, so any application that mounted it -which is all of them, on all their pages- pulled in the date.css sheet (4.6 KB gzip, measured with Vite) and, in a Next build with Turbopack, also the calendar, the editable segments and the whole picker: ~45 KB uncompressed on a home page that did not paint a single date. It moves to its own module, components/locale-provider.tsx. The API does not change: LocaleProvider, LocaleProviderProps and DEFAULT_LOCALE come from the same index and the same subpaths. Measured on amset.tech: the initial JavaScript drops by 10 KB gzip and the library's from 144 to 116 KB uncompressed. It is guarded by src/locale-provider-weight.test.ts (the provider's import graph cannot touch dates or floating layers) and a new weight budget scenario, LocaleProvider at 2.2 KB, which only counts what is static: the key catalogue that the provider requests with import() in development is 20 KB that nobody downloads in production and would hide a change of a few KB.
  • RichTextToolbar and ToggleButton: the toolbar for a rich text editor, without putting the editor in the library (AMS-1787). The toolbar talks to the editor through state (what is applied where the cursor is) and onCommand, so it works with Tiptap, with Lexical or with any other, and @amset/ui still has no dependencies. It is a Toolbar (one tab stop, arrow keys, focus memory) with a ToggleButton per format (aria-pressed, shortcut in aria-keyshortcuts and in the tooltip, Control or Command depending on the system), named groups, link and image with panels that are only mounted when opened and that are not a <form> (it works inside the application's form), and pressing with the mouse does not take focus away from the editor. Addresses are checked with normalizeLinkHref and normalizeImageSrc, which are exported: an allowlist of schemes, a javascript: or a data: does not get through. The image requires alternative text or being marked as decorative (alt=""). New sheets toggle-button.css and rich-text.css, with the editor frame (.amset-richtext__frame) and the typography of the written content (.amset-richtext__content). Guide in English in docs/rich-text-editor.md with the two complete recipes (on each editor's core, without @tiptap/react or @lexical/react, which drag in @floating-ui), checked by the compiler and tied to the guide by a test, and a checklist for the manual pass with NVDA and VoiceOver of editable Table, Combobox, InvoiceLinesEditor and the toolbar itself. Tiptap and Lexical come in only as Storybook development dependencies, where two stories actually run them.
  • WeekAgenda no longer breaks hydration in English (AMS-1894). The week header comes from formatRange ("Oct 11 – 17, 2026"), and Node's ICU writes thin spaces (U+2009) around the dash where Chrome puts normal spaces: rendered on the server, the agenda did not match on hydration and React threw away the HTML of the whole page (error 418, measured on amset.tech, only in English because in Spanish that format has no spaces). The calendar, the date segments and the timeline already normalised it, each with its own copy; now all four go through utils/plain-spaces.ts. It is pinned by a test in week-agenda.test.tsx that renders the agenda in en-US (verified to fail without the fix).
  • Decided: onPress remains the press event, and isDateUnavailable and isLeaf keep their "is" (AMS-1762). First decision record in the repository, in docs/adr/ (in English): onPress is the name that all the fronts already write and it unifies mouse, keyboard and finger in a single event; onClick remains only as a migration path in the button family and in TextButton, always alongside onPress and with the native MouseEvent (AMS-735). isDateUnavailable is a predicate, not a state, and isLeaf is the three-state marker for lazy loading. Not a single prop changes. prop-names.test.ts stops tolerating them silently: it lists them as exceptions with a link to the record and fails with any other "is" or with an own onClick without onPress.
  • npx amset-ui migrate also covers what 0.16 moved to English (AMS-1894). It was written for what 0.14 removed and knew nothing about the 0.16 table; migrating amset.tech revealed the gaps. It now rewrites the 133 per-component sheets (@amset/ui/estilos/boton.css → @amset/ui/styles/button.css, also when imported individually), estilos.css, fichas.json and mandos.json, Tooltip para → trigger, PasswordField nueva → newPassword and errorDeImporte → amountError. The sheet table was matched by CONTENT, comparing the rules of each sheet in the two published versions: all 133 have a destination and none maps to the same one as another. Fixed along the way: the codemod only looked at the Table and Form props written in the code, so a new entry in PROP_RENAMES was ignored without warning; now the table rules. What still has to be done by hand: the linea1/linea2 keys of an address (the compiler already says "did you mean line1?") and the data-tema/data-densidad attributes in the HTML, which may belong to the application itself.
  • Storybook: the control descriptions stop using old names and the size guide now shows the "Length" column (AMS-1667). SlotPicker said hora where the prop is time, WeeklySchedule and RangeFilter said { desde, to } where it is { from, to } and closedSchedule() appeared as horarioCerrado(); and the SizeGuide stories declared the largo measurement but gave the values under the long key, so the column came out empty. Documentation and stories only: no API changes.
  • BarChart: reference markers and a second figure per bar (AMS-1818). markers draws lines that cross all the bars at their value (the group average, a target), in columns and in rows: they are included in the axis ceiling, appear in the table with their name and value and do not count in the chart summary; the layer measures the same as the area where the bars grow, so the line at 62% falls exactly where a bar of 62 would end (measured at 0 px). DataBar.detail puts a second visible figure after the bar's number ("+12 pts"), only in rows, where there is room; in columns it stays in the table, next to the note. New variables --amset-bars-name and --amset-bars-row-gap. What is still pending from that ticket (stacked correct/blank/wrong bar and the per-point detail in LineChart) is new API and is handled separately.
  • MonthAgenda: events can be pressed (AMS-1817). AgendaEvent accepts href (the event in the day's list is a link, which goes through the RouterProvider just like Link) and MonthAgenda accepts onEventPress(key): without href, the event becomes a button that reports its key; with href, it also reports when the link is followed. The card looks the same and the whole of it can be pressed (2 px focus, border in the event's colour on mouse hover). With neither of the two, the event remains text. What is still pending from that ticket (month grid with titles inside each day and agenda view by range) is new API and is handled separately.
  • Table: the rows-per-page selector is no longer truncated, and the rowHeader column aligns to the start (AMS-1819). The selector measured a fixed 4.5 rem and with a wider figure it showed "5…": now that is a minimum and it grows with its value. A th scope="row" is centred by the browser, so a rowHeader column ended up centred under a left-aligned header; the library now says text-align: start for all th cells that are not end or center. And the toolbar's free bar (.amset-table__bar-free) no longer shrinks without a floor until it truncates a custom toolbar at 390 px: it has a minimum and wraps onto a new line.
  • PhoneField: the number fits with long prefixes in a narrow field (AMS-869). With +351 in an lg field of 224 px the last digit overflowed by 7.4 px; the prefix trigger now takes 12 px less (less space between the flag, the prefix and the arrow: 97.2 → 85.2 px measured) and the number has min-width: 0.
  • Own page for 14 components that only appeared inside their group's page (AMS-1898). AreaChart, StackedBarChart, Grid, Container, Center, ScrollArea, AspectRatio, Kbd, Mark, Highlight, Blockquote, ColorPanel, ToggleChipGroup and Uploader had a public API and had no page, no index entry and no search by their name: now each one has its own in Storybook and in docs.json (202 pages), with its "Interactive" story, its English text and its keyboard and accessibility measured in a browser. Uploader was not described anywhere. The pages for Stack, Code (now "Inline code"), LineChart, ColorPicker, ToggleChip and FileUpload stop describing the others and link to them. Along the way: Blockquote with cite lost the attributes passed to it (they now go to the <figure>); in Uploader the attributes, className and style landed on the inner area and not on the box, and when the last file was removed focus went to the body and the announcement was lost (it now returns to the choose button and is announced); the colour area handle was positioned relative to the popover and not the area (52 px off in ColorPicker, outside the panel with a standalone ColorPanel), and now it lands where its value says; in the hue strip, End (or dragging to the edge) took the handle to the start because 360° is 0°, and it now ends at 359°; ToggleChipGroup and Uploader introduce --amset-chip-group-gap and --amset-uploader-gap; and Storybook builds again, as it imported LocaleProvider from its old location.
  • Floating layers reposition when the typography loads (AMS-1176). A reflow that moves the trigger without changing its size (the web font that has just arrived) triggered neither scroll, nor resize, nor the trigger's observer, and the layer kept the old measurement until the first resize. It is now repositioned when document.fonts.ready resolves and when the size of the body changes.
  • Hydrating a screen with DatePicker costs a third: the closed panel no longer mounts its calendar (AMS-1886). FloatingDialog -the panel of the date picker, the range, the colour, the menus, the Dropdown and the table filters- rendered its content even when closed, so each DatePicker mounted a whole month that nobody saw, and each day created its Intl formatters (calendarOf, the long date, the day number). Now the content is mounted on open and unmounted when the exit finishes, and the formatters are created once per language (utils/intl-cache.ts). Measured with the apps benchmark (CPU ×4, median of 15): the orders screen hydrates in 150 ms instead of 264, on a par with Mantine (149) and ahead of MUI (242) and antd (352); the closed date picker goes from adding ~250 ms to ~40. Warning: anyone who looked in a test for something inside a closed panel will no longer find it until it is opened.
  • Breaking Breaking: ALL the API that is touched from outside moves to English. AMS-550 translated components and props, but functions, attributes, files, subpaths and the CLI remained in Spanish, and the examples in the reference still used the old API (<Boton variante="solido">). What changes when upgrading:
    BeforeNow
    @amset/ui/estilos.css, @amset/ui/estilos/<hoja>.css@amset/ui/styles.css, @amset/ui/styles/<sheet>.css (the sheets, also in English: boton.css → button.css)
    @amset/ui/formulario, @amset/ui/esquemas@amset/ui/form, @amset/ui/schemas
    fichas.json, mandos.json, ejemplos.json, idiomas/*.jsondocs.json, knobs.json, examples.json, locales/*.json (and their keys: component, module, title, family, description, stories, name, knobs, story, code)
    npx amset-ui tema, npx amset-ui mandos, npx amset-ui migrar, amset-tema.cssnpx amset-ui theme, npx amset-ui knobs, npx amset-ui migrate, amset-theme.css; add writes styles/ and styles.css
    data-tema, data-densidaddata-theme, data-density
    data-cargando, data-seleccionada, data-deshabilitada (Card) and the rest of the data-* in Spanishdata-loading, data-selected, data-disabled… (data-actual → data-current, data-sin-leer → data-unread, data-nivel → data-level…)
    @layer amset-ui.componentes@layer amset-ui.components
    Classes amset-no-imprimir, amset-only-impresion, amset-for-imprimir, amset-card-pago*, amset-motion-esencial, amset-order-state__hitoamset-print-hidden, amset-print-only, amset-printable, amset-payment-card*, amset-motion-essential, amset-order-state__milestone (y --amset-order-state-milestone-gap)
    Animations amset-*-entra, -entrar, -salir, -aparece, amset-progress-va-y-viene, amset-rating-marcar-enter, -exit, -appear, amset-progress-back-and-forth, amset-rating-mark
    csvDeTabla, descargarCsv, separadorPorDefecto, filtrarFilas, ordenarFilas, paginarFilas, opcionesDeColumna, valorDeCeldatableToCsv, downloadCsv, defaultSeparator, filterRows, sortRows, paginateRows, columnOptions, cellValue
    calcularFactura, errorDeImportecalculateInvoice, amountError; and rejectionReason / FileLimits from FileUpload, which the README presented as exported and were not
    Tooltip para, AddressField linea1 / linea2, parseHotkey(...).teclaTooltip trigger, line1 / line2, .key
    PasswordField nuevanewPassword
    Avatar state="inline" (it was "en línea" mistranslated)state="online"
    TaxZone: peninsula, baleares, canariasmainland, balearic-islands, canary-islands (ceuta and melilla stay)
    TransferList moveText(..., 'elegidos' | 'disponibles'), ROOT_FIELD = 'raiz''chosen' | 'available', 'root'
    Text keys CabeceraDeAgenda.*, FilaDeFranja.*, TablaDeDias.*, NumerosDePagina.*, TiraDeDias.*, HorasDelDia.*AgendaHeader.*, SlotRow.*, DayTable.*, PageNumbers.*, DayStrip.*, DayHours.*
    Storybook ids componente-boton--jugar, componentes-*, sistema-*, fundamentos-*component-button--playground, review-*, system-*, foundations-*; the story exports also in English (Cargando → Loading) and the visible name in Spanish

    The repository's files and folders also move to English (src/components, src/utils, src/styles, src/form, src/schemas, src/table, src/messages/texts, boton.tsx → button.tsx, DISENO.md → DESIGN.md...). What is noticeable from outside: npx amset-ui add copies to components/, utils/, messages/texts/ and styles/ instead of the previous names; DESIGN.md and ACCESSIBILITY.md ship in the package with their new name; and the files in dist/ hang from the same folders (dist/components/button.js), although from outside they can only be reached through the index and the exports subpaths.

    The package.json scripts too: build, build:css, build:css-components, build:docs, build:examples, build:sources, build:locales, build:storybook, typecheck, verify, test:watch, e2e:engines, e2e:engines:gate, consumers, budget, audit and export:figma (previously construir*, tipos, verificar, test:mirar, e2e:motores*, consumidores, presupuesto, auditar and exportar:figma). Anyone with an npm run construir in a branch's CI should change it.

    The parameters of the exported functions are also named in English, and scripts/docs-api.test.ts checks every example in README, REFERENCE and Getting started with the compiler: imports, components, props and values.

  • Storybook without accessibility noise and with the code in English. axe was run over the 1,036 pages (stories and documentation, light and dark): the PAGE rules (region, landmark-one-main, page-has-heading-one) fired on almost every story by construction and are turned off in the bench (src/bench/a11y.ts); the heading-order and repeated-landmark rules are turned off only on the review and system pages, which stack loose pieces. What did belong to the component: the header of the calendar and the agenda was a <header> that axe counted as banner, it is now a <div>; the empty column of Board and an empty SortableList had no valid row inside the grid (aria-required-children); the accessible name of each Board column had "tarjetas" written in Spanish and now comes from the text bundle (Board.column, with plural, also in English); a disabled RangeFilter carries aria-disabled and its dimmed parts are no longer measured for contrast; and the neutral bar of Timeline did not reach 4.5:1 in the dark theme. Separately, 504 stories had Spanish names (Cargando, ConError, SoloLectura…): now the export and the Storybook id are in English, the visible name stays in Spanish, and the identifiers of the stories, the bench and .storybook/ too (fotoDeMuestra → samplePhoto).
  • The side panel and the dialog once again stop the page behind them and say on their trigger whether they are open (AMS-1284). When both moved to <dialog>, two things that React Aria did were lost without a single error: the page behind kept scrolling with the modal open (measured in the bench: the wheel over the backdrop took it from 200 to 1,000 px) and the trigger stopped carrying aria-expanded and aria-controls, so a screen reader did not say "expanded" and it did not stay pressed with its layer open. Now, while a Dialog, a Drawer or a CommandPalette is open, the root has overflow: hidden and, if the scrollbar takes up space, scrollbar-gutter: stable, so that nothing jumps sideways; whatever the page had inline is restored on close, and a dialog opened from a panel does not release the page until the panel is closed as well. The trigger passed through trigger receives aria-expanded, aria-controls (only with the layer open, pointing at the id of the <dialog>, yours if you give it one) and amset-trigger-open. It costs 0.34 KB compressed in each; the weight budget adds a Button, Drawer scenario.
  • benchmark/: MUI and Mantine in the individual pieces, and three equivalent apps measured in a real browser (AMS-1784). The pieces (Button; Button + Select + DatePicker; Table) now compare seven libraries with pinned versions; Mantine with the minimal CSS, only the sheets for what remains in the bundle, checked at build time. In addition, the same screen (form with validation, table with sorting and pagination, confirmation dialog) built with @amset/ui, MUI, Mantine and antd, rendered on the server and hydrated in Chromium: it measures what the page downloads, the JS that is parsed and the hydration, and each app goes through the same walkthrough by roles and axe without failures. npm run benchmark builds, measures and rewrites benchmark/RESULTADOS.md with the versions, the date and the caveats. Measured today: the screen loads 127 KB gzip with @amset/ui, 176 with Mantine, 220 with MUI and 391 with antd.
  • The button keeps focus when pressed with the mouse in Safari as well (AMS-1881). Safari does not focus a native <button> when it is pressed, neither with the mouse nor with a finger, so when opening a Dialog, a Drawer or a menu with a click and closing it, the browser had nowhere to return focus to and it fell on the body (anyone continuing with the keyboard went back to the start of the page). Button and IconButton now take focus on click -not on mousedown, where Safari undoes it and Firefox shows the focus ring with the mouse- and before onPress, which is when the layer checks who has focus. Nothing changes with the keyboard. It costs 70 bytes compressed.

0.16.0, released October 18, 2026

  • npx amset-ui migrar: rewrites the code that uses the names that 0.14 removed (AMS-1831). The Table props and column keys (enServidor, paginacion, ordenable, numerica...), the sort's sentido, Formulario with its props (esquema, onEnviar...), the *Form fields, BotonEnviar, resolverDeEsquema, PropsCalendarRange, TableSortInput and the call to ariaGranularity. It does so with the syntax tree from the project's own TypeScript, not with regular expressions: it tells a prop apart from the same word in a comment, a string or another component, respects your indentation and your comments, follows imports with aliases and with namespaces, and columns declared in a variable. What it cannot decide on its own (a ...props, a .sentido read in a handler, PropsForm, ERROR_NETWORK_COUPON) comes out as a warning with file and line. --dry-run counts beforehand, --check exits with 1 if anything remains (for CI), and it is idempotent. doctor warns (error) when those names remain. Covered by 19 tests, including one that compiles the old code (fails) and the migrated code (compiles) against the library's real types, and run dry against four in-house fronts without a single false positive.

0.15.0, released October 17, 2026

  • InvoiceLinesEditor: the row no longer overflows its box (AMS-1812). With the withholding column (the default) the single-line row adds up to about 65 rem of columns, but the threshold for switching from card to row was 52 rem, the same with and without withholding: between 832 and 1040 px of container it overflowed by up to 210 px and the amount and the remove button were left outside, clipped (without withholding, 38 px at 832). There are now two thresholds, 55 rem without withholding and 66 with it; below them, each line is the two-column card. e2e/factura-ancho.spec.ts sweeps twelve widths and fails if the row overflows (verified that it bites). It was uncovered by an axe sweep over all the Storybook stories, which counted the clipped amount as a 1.12:1 contrast.
  • Breaking BREAKING (fixes a bug): NumberField, QuantityPicker and MoneyField submit the number in a form, not the text that is shown. The name was on the visible field, which holds the number in the language's format: in a native form, 10,5 arrived as "10,5" (a server reads it as NaN or as 10), 12.000 as "12.000" (a 12) and an amount as "1.234,56 €". Now the name goes on an <input type="hidden"> with the canonical number —10.5, 0.21, 12000, 1234.56—, always with a point, without thousands separator or symbol, already clamped to minimum, maximum and step as the field leaves it on blur, and empty if there is no figure or it cannot be understood. A disabled field is not submitted, like an <input disabled>. Warning: the visible field no longer has a name; if a test or a script looked it up by [name=...], it now finds the hidden one. New canonicalNumber() in utilidades/numero. 12 new tests (numero-formulario.test.tsx); 9 fail against the previous code.
  • Equivalence surcharge on invoices (AMS-1785). calcularFactura and InvoiceLinesEditor add the surcharge to the VAT according to its rate -5.2% with 21%, 1.4% with 10%, 0.5% with 4%, art. 161 of Law 37/1992- with its own row in the breakdown (surchargeBreakdown) and in the total. It is requested for the whole invoice (equivalenceSurcharge) or line by line (surchargePerLine adds a checkbox per line and the line's one takes precedence). It is rounded once per rate, like the VAT, not line by line (ten lines of 0.10 give 0.05 and not 0.10). surchargeRates changes the rates; the transitional 0.62% for 5% is not included by default. Without requesting it, the usual totals do not change. Exported from the index: SPANISH_SURCHARGE_RATES and the new types.
  • Item, ItemGroup, InputGroup and RadarChart. The generic row (with href or onPress the TITLE is the link or the button and its area is stretched with CSS to cover the row: a single interactive element, and the actions on top as separate buttons); the field with segments joined inside a single box, with the focus ring, the help text and the error of TextField; and the radar chart (scale from zero, a null is a gap and not a zero, a summary for screen readers with the highest and lowest of each series, and the whole table). radarPoint, ringPath and seriesPath come from utilidades/grafico-radar.ts.
  • TextButton, the text action (AMS-1792). A <button> with the look and tones of Link for an action that sits inside a sentence (retry, dismiss): the screen reader says "button", it responds to Enter and Space, it is type="button" by default and it does not submit a form twice while loading. Without the box or padding of Button: it inherits size and line height, and on touch the tap area grows to 44 px with a pseudo-element, without moving the line (a <button> is always inline-block: padding would stretch it). It replaces the hand-written button that every front built for this.
  • HoverCard, Menubar and NavigationMenu. The card that appears when hovering over a link (its content can be interacted with: closeDelay lets you reach it, Tab enters because it comes right after and Escape dismisses it, WCAG 1.4.13); the desktop menu bar (a single tab stop, left and right arrows with wrap-around and reversed in RTL, and with a menu open the mouse opens the neighbouring one, which a modal ActionMenu cannot do); and the main navigation with ARIA disclosures (real links, the panel right after its button, aria-current, Escape and tabbing out close it, and pressing a button that the mouse has just opened PINS it instead of closing it). useMenuList is extracted from MenuPanel to share the list keyboard with the menu bar.
  • The phone prefix appears in full in the list (AMS-1703). Dominica, the Bahamas, Jamaica and the rest of +1 all showed as "+1"; now the list and the accessible name say "Dominica (+1 767)", and with several ranges "+1 809 · 829 · 849" in the accessible name and the title, and "+1 809…" in the row (the full one split the country name over two lines: 43 px for the Dominican Republic row versus 36). The trigger stays at "+1" (the area code digits are already in the typed number, and repeating them would give "+1 767 767 555…"); the United States, Canada (too many ranges) and area codes of one or two digits stay at the dialling code. The E.164 does not change. dialLabel() is exported from the index.
  • ModelSelector and ArtifactCard (+ ArtifactFrame), the two that were missing to migrate the assistant's chat. The engine and model selector is purely presentational (the catalogue and the choice are supplied by whoever uses it): two rows of native radios —the arrow keys skip disabled engines—, a disabled engine states its reason in text, and if the selected one has been disabled the button is flagged and the panel warns; fixLabel + onFix add the link to fix it. The artifact card shows the thumbnail or the icon according to the type, open and download as SIBLING buttons of the header, and the preview is only mounted when open; onVisible requests the content of what comes into view. ArtifactFrame isolates the HTML with sandbox="allow-scripts" and never allow-same-origin, and it cannot be removed from outside. selectionLabel and artifactKind are exported separately.
  • npx amset-ui init and npx amset-ui doctor (AMS-1778). init leaves a project with the library set up properly: the @amset:registry line in .npmrc (never a token), @amset/ui/estilos/base.css at the top of the global CSS —which fixes the layer order before Tailwind 4 without paying for the CSS twice—, your theme generated with the tema command (contrast checked), AmsetProvider and the Next router mounted in the layout, _app or <App />. It is idempotent, it does not overwrite a file of yours (it only adds lines to it), --dry-run reports what it would do and --install installs with your package manager. doctor diagnoses a project that already uses it and exits with code 1 if there are errors: Tailwind 3 without layer order, data-tema with other values, a token as ${VARIABLE} in .npmrc, Pages Router without transpilePackages, and warnings for an ungenerated theme, links that reload the page, Jest without a CSS mapping. Tested with unconfigured Next and Vite projects that go through init, build, open in Chromium and carry the requested brand colour (npm run consumidores, scripts/init-doctor.test.ts).
  • ref works with React 18 (AMS-1782). In React 18 a function component does not receive ref as a prop and discards it: <Button ref={nodo}> left it as null and Dropdown did not open its panel from a Button (measured with real React 18). Button, IconButton and FloatingButton -the only three that expose ref in their public type- are now wrapped with withRef, a forwardRef that passes the ref to the render; in React 19 everything stays the same. New npm run test:react18 (and its CI step): installs react@18 separately and renders every page and the buttons with ref without a single exception or React warning, so "React 18 and 19" stops being an unchecked promise.

0.14.0, released October 17, 2026

  • Breaking Breaking: the old names that remained as aliases are gone (AMS-1761). A deprecated alias is dead code: out go those of Table (enServidor, seleccionadas, alSeleccionar, paginacion, columnasConfigurables, alCambiarColumnas, redimensionables, alPulsarFila, barraSuperior; in the column numerica, ordenable, filtrable, ocultaPorDefecto, pegada, cabeceraDeFila; and sentido in the sort, with its type TableSortInput), the Spanish names of the form (Formulario, CampoTextoForm, CampoAreaForm, SelectorForm, SelectorFechaForm, CasillaForm, InterruptorForm, GrupoRadioForm, BuscadorForm, BotonEnviar, resolverDeEsquema, PropsForm, EsquemaEstandar, IncidenciaEstandar, ShippingAnswer), PropsCalendarRange, ariaGranularity and ERROR_NETWORK_COUPON. The table of equivalents is in the README. amset-ui-web and amset-assistant move to this version at the same time.
  • The DatePicker calendar no longer reappears when it closes (AMS-1766). Closing the layers waited for ALL the animations and transitions of the panel, including those set by the CSS of whoever uses the library: in amset-assistant, a 200 ms transition on scrollbar-color meant that the 100 ms exit finished, the panel became fully visible again for about 80 ms and only then closed. Now it waits for @keyframes animations and for the transitions that move the layer (opacity, transform, translate, scale, display, overlay), and the exit of the floating layers and of the Tooltip stays on its last frame (forwards) until close().
  • README in English (AMS-1716). README.md becomes the English front page —what npm and GitLab show—: installation, usage, styles, themes, Next.js, on-demand languages, forms, measured weight, tests, tools for AI assistants and licence. The full per-component reference remains in Spanish in README.es.md, which now ships in the package. src/readme-ingles.test.ts ties the front page to reality: every npm run and every @amset/ui/... subpath it cites has to exist, and the documentation test also checks README.es.md.
  • Reference in English (AMS-1774). REFERENCE.md translates the full per-component reference (README.es.md, 2,674 lines) into English and ships in the package, linked from the front page. src/referencia-ingles.test.ts prevents them from drifting apart: same headings in the same order, same code blocks and same tables, and the same --amset-* variables and components cited in both.
  • Components for AI interfaces: PromptInput, ChatMessage, Reasoning, ToolCall, ActivityLog, Attachment, Suggestions and UsageBadge. They come from what the AMSET assistant's chat already did, without losing anything: the box grows with the text up to a limit, sends with Enter and adds a new line with Shift+Enter (without sending while composing with an IME), recalls the last message with the up arrow, accepts pasted, dragged or chosen files and switches from "Send" to "Stop" while it answers; the log follows the end only if it was already at the bottom (useStickToBottom, exported) and groups repeated events; a tool call that fails opens by itself. Everything is conveyed with text as well as with colour, with no dependencies and with one CSS file per component. formatDuration, formatBytes, collapseRuns and isNearBottom are exported separately. A new @keyframes, amset-caret-blink, shared by the three cursors.
  • FormTreeSelect, the connected field that was missing (AMS-1675). @amset/ui/formulario now has a connected field for every field with a value: FormTreeSelect stores the key of the chosen node, or the list of keys with selection="multiple" (in tree order, not click order), and shows the schema error in the field. FormCascader and FormTreeSelect are also added to the reference's tables of connected fields, from which the former was missing.
  • CSS order no longer decides how a field looks (AMS-1775). The box and the input of all fields (.amset-field__control and .amset-field__entry) had the specificity of one class, the same as their variants (the text area, the number field, the search field...), and whichever the bundler left last won: in the library on its own the order is fixed by index.css, but in a consumer it is not. Measured in a browser with the sheets in the wrong order: the TextArea came out with a 36 px box around a 56 px textarea (in Next 16 with Turbopack, 46 around 67), and with line height at normal instead of 21 px. Now both bases go in :where() and have zero specificity: any variant, and any CSS from whoever uses the library, always beats them. In both orders the box measures the same (80.3 px) and does not overflow. A new test (orden-del-css.test.ts) prevents the base from being written with a bare class again. No API changes.
  • Firefox and WebKit in the e2e, and what they found (AMS-1716). npm run e2e:motores runs the behaviour e2e in both engines (opt-in with AMSET_MOTORES=1, without axe or screenshots), and CI has a fast gate, e2e:motores. Two real bugs that Chromium did not show: in Safari, when closing a dropdown or a confirmation, focus was lost on the <body> (Safari does not give focus to a button when it is clicked with the mouse, and the browser returns focus to "whatever had it"); now the layer returns it to its trigger if it has been lost. And in Firefox and Safari the dialog, the side panel and the command palette disappeared abruptly when closing (only Chromium animates display and overlay); now the exit goes through data-exiting and close() waits for it to finish, the same in all three engines. No API changes.
  • The brand now supports a second colour: secondary. Until now only brand (the accent) existed, so a design with two brand colours could not be expressed. New family of tokens --amset-secondary, -hover, -active, -text, -soft and -ink (violet by default, in light and dark, with their AA contrast pairs measured), key palette.secondary in amset-ui.config.js, and tone="secondary" on everything that already accepted accent (buttons, tags, alerts, cards, avatars, charts, text, links…). No changes for anyone who does not use it.
  • And a third one: tertiary. Same recipe as secondary: --amset-tertiary* tokens (teal by default, light and dark, AA contrast measured), palette.tertiary and tone="tertiary" on everything that accepted accent.
  • The second and third colours reach ALL components with data-tone. Checkboxes, switches, sliders, tabs, steps, the selected day in the calendar, the focus ring… always painted accent and had no tone. Now data-tone="secondary" or data-tone="tertiary" on the component itself or on any container repaints everything inside (it remaps --amset-accent* and --amset-focus), without touching each component.
  • The documentation covers the three brand colours. Foundations → Colour lists the secondary and tertiary families (53 roles) and introduces "The three brand colours", a live demo of tone and data-tone; Getting started explains the new palette keys and the pages that spoke of "six tones" now name all eight.
  • npx amset-ui add: copy a component's code into your project (AMS-1771). For anyone who does not want to depend on a published version: add button iban-field (or --all) copies the code with everything it needs, its stylesheets and a text catalogue generated only with what was copied; list shows the 190 available. It removes the .js from imports (--keep-js keeps it), does not overwrite a file you had already changed (--overwrite does) and records what was copied in amset-ui.json. The package publishes the code in dist/sources/ (npm run construir:fuentes). Measured: the copied code compiles in strict mode and so does add --all.
  • @amset/ui/esquemas: the validations of the Spanish fields, on their own (AMS-1771). documentSchema, ibanSchema, cnaeSchema, socialSecuritySchema, plateSchema, cardNumberSchema and postalCodeSchema: the letter of the tax ID (NIF), the IBAN mod 97, the CNAE division... as Standard Schema schemas that also include check and message to hook them into Zod or Valibot without the library depending on either. Same messages as the field, in es and en. Server subpath: no 'use client'.
  • ExportButton and exportElement: charts and tables to PNG, SVG and PDF (AMS-1771). In the browser and without libraries: the element is copied with its already computed styles, placed into an SVG with foreignObject and the three formats come from there (elementToPng, elementToSvg, elementToPdf, downloadBlob). scale up to 4 for printing; it covers what overflows the box; it embeds fonts and images. The PDF contains the image, not text. A hidden element fails with ExportError. Measured in Chromium with LineChart, BarChart and DonutChart. New texts ExportButton.*.
  • Table with 100,000 rows, measured (AMS-1771). With virtualized and maxHeight: first paint in 121 ms with 16 rows in the DOM, 61 frames per second when scrolling and 42 ms to sort. No code changes: it is only documented with the measurement.
  • ACCESIBILIDAD.md: what is measured (axe with WCAG 2.0, 2.1 and 2.2 AA), what is checked separately and what has not been measured yet, without claiming a level that has not been verified (AMS-1771).

0.12.0, released October 16, 2026

  • The library is tested in real projects (AMS-1716). npm run consumidores packs the package, installs it in a Next project (App Router) and in a Vite one, builds them, serves them and opens them in Chromium: typing of a third-party project, translated server HTML, hydration without errors, interaction, lazy languages that are not downloaded unless someone requests them and a first-load JS limit. consumidores job in CI. No changes for anyone using the library.
  • The developer warnings no longer say or load anything in production (AMS-1716). With process.env.NODE_ENV behind a typeof process guard, in the browser production was taken for development: with catalogs the provider downloaded the library's entire catalogue (18 KB gzip, measured in a real Vite app) to check the keys, and the table's deprecated-prop warnings appeared in the console. Now isDevelopment() decides, which leaves the expression that the bundler replaces without a guard.
  • Choosing a day in a DatePicker (or any layer) inside a Dialog no longer closes the modal. The close and cancel of the child <dialog> bubbled up the React tree to the onClose/onCancel of the containing modal and closed it (also with Escape inside the calendar). Dialog, Drawer, CommandPalette and FloatingDialog only handle the close that is their own.
  • Text on a coloured fill is light in both themes (AMS-1737). In the dark theme the solid fills (accent, error-solid, success-solid, warning-solid, info-solid) were light and carried almost black text: with a red or green brand you got black lettering on red or green. Now the fill has the same dark lightness in both themes and its ink (*-ink) is almost white, tinted with the family's hue, not pure white or black. Measured: with any brand hue and chroma the ink reaches 4.5:1 on its fill in light and dark (new test in paleta.test.ts). highlight becomes black in dark as well, so hovering always darkens. Warning: in dark mode the primary and status buttons look deeper than before; if you forced colors with a light fill, the generator will refuse to write the theme.
  • The text of an active ToggleChip is readable with any colour (AMS-1737). It used 75% of the chip's colour and the rest from the theme's ink: an intense pink in light or the accent in dark fell below 4.5:1. It now uses 30% (--amset-chip-ink-share), and a test sweeps the colour space in both themes.
  • Mark stops painting dark text on black. It used the highlight role (the direction in which a fill darkens) as its background; it now sits on gold-soft with the main ink, and the pair is in the contrast tests.
  • The completed part inside a timeline bar darkens the bar instead of lightening it, so that the dates remain readable on it.
  • Block components fill the width of their slot without being asked. Fields and Select, table, accordion, cards, alerts, tabs, pagination, charts, lists and the rest of the block components have inline-size: 100% from base.css: placed in a flex row they shrank to their content (on the website, 80 components; the Select changed width when an option was chosen). Those that size to their content (buttons, tags, avatars, switches, SegmentedGroup, text) do not change, and a test forces every new component to be classified. Inside other components, the phone prefix, the hours of the weekly schedule and the pagination's rows selector stay compact.
  • A standalone Calendar is measured against its box and is centred: the days shrink with the slot instead of with the window (in a 238 px card the month measured 274 and overflowed) and the calendar fills its slot up to the size of the month.
  • Grid equalises the height of what goes in each cell (equalHeight, true by default): a card inside a wrapper fills the row just as if it were a direct child.
  • Link is underlined by default (underline="always"): inside a paragraph colour is not enough (axe link-in-text-block, 1.12:1 measured). underline="none" removes it for menus and bars.
  • SegmentedGroup: the fill no longer flies off after scrolling the page. The starting point was measured when the previous option was chosen; with the page scrolled in between it started thousands of pixels away and the frame showed the scrollbar.
  • benchmark/: weight compared with react-aria-components, shadcn/ui, HeroUI and antd (P1.6). npm run benchmark bundles the same thing in each one with Vite (Button; Button, Select and DatePicker; Table) and counts JS and CSS in gzip, with the versions pinned and the caveats written in benchmark/README.md.
  • Requesting one component no longer brings the texts of all of them (AMS-1718). The entire es/en catalogue (65 KB uncompressed) travelled with any component that said anything: a Button weighed 23.4 KB gzip and now 5.9; Table goes from 54 to 38 KB and the week agenda from 35 to 18. Each component brings only its own bundle (src/mensajes/textos/); ES and EN are still the entire catalogue. translatorFor(locale, paquetes, mensajes?) and useMessages(paquetes) now receive the component's bundles, and catalogFor is replaced by catalogEntry. A text requested from a bundle that does not contain it is reported in the console and makes the tests fail.
  • Security: Link and ButtonLink with target="_blank" carry rel="noopener noreferrer" (AMS-1716). Previously they had no rel and the destination page received the originating window and the Referer header. The rel passed by the user is respected and completed. The tracking address of OrderStatus also carries noopener.
  • SECURITY.md, audit and weight budget in CI (AMS-1716). SECURITY.md with how to report and what the library does and does not do; npm run auditar (high and critical issues in what ships) and npm run presupuesto (gzip weight of what is imported, measured with Vite) in the verificar job; src/seguridad-urls.test.tsx ensures that javascript: stays blocked.
  • axe also runs wcag22aa (AMS-1716): in axe-core 4.13 it is the target-size rule; 568 cases without violations.
  • All prop descriptions, in English too (AMS-1687). The 300 that were missing, from old components and new ones, now have their version in src/mensajes/props.en.json; a new test in scripts/api.test.ts fails if a new prop is left with its description in Spanish only.
  • LineChart, AreaChart and StackedBarChart (AMS-1698). Multi-series charts with gaps (null), an axis with round numbers, legend, values on hover, summary and table for screen readers. Sheet series.css, texts SeriesChart.*.
  • The Select panel is capped at 20 rem. With many options the panel could take up the whole window, with an extremely long scrollbar. It now measures at most 20 rem (about eight and a half options) and the rest is reached by scrolling; it can be changed with --amset-select-panel-max-height. Any floating layer can request its own cap with --amset-floating-cap; if the window is shorter, the panel keeps shrinking.
  • WorkCalendarView, createWorkCalendar and holiday readers (AMS-1700). Working calendar without built-in holidays: they are loaded from the official source with parseIcsHolidays / parseCsvHolidays. Summary of working days per month, isDateUnavailable for the date pickers. Sheet calendario-laboral.css, texts WorkCalendar.*.
  • WeekAgenda on mobile: on a narrow screen it switches to the list of days with the week strip at the top, like Teams (layout auto | full | compact). On touch devices appointments are tapped and not dragged.
  • WeekAgenda (AMS-1700). Week and day agenda with appointments that can be dragged and stretched (pointer and keyboard), layout of overlapping ones, a "now" line and announcements to screen readers. Its own sheet, agenda-semana.css; texts WeekAgenda.* in Spanish and English; placeBlocks, moveBlock and resizeBlock are exported separately.
  • Breaking BREAKING: a single name for each prop, the HTML one and without "is" (P0.1, AMS-1727). The library had two dialects: 44 controls said isDisabled, isReadOnly, isRequired, isInvalid and isSelected and 24 pieces disabled, readOnly, selected and open, so anyone building a form had to remember which one went with each. Now they are disabled, readOnly, required, invalid, selected, open, indeterminate, dismissable and keyboardDismissDisabled on all of them, and no alias of the old names remains: they have to be changed when updating (in JSX, isDisabled becomes disabled and so on for each one). MultiSelect keeps disabled, readOnly and required like all the others, and its "is" aliases disappear. isDateUnavailable (it is a function, not a state), onPress and the native onClick remain, and useDisclosure still returns isOpen alongside its verbs open() and close(). A test (nombres-de-props.test.ts) prevents a name with "is" from coming back.
  • InvoiceLinesEditor (AMS-1700). Editor for the lines of an invoice with VAT and personal income tax (IRPF) withholding per line and the breakdown of totals; calcularFactura and lineAmount are exported separately. Its own sheet, factura.css. Texts InvoiceLinesEditor.* in Spanish and English.
  • Advanced Table (AMS-1699). Grouping by column, filters with several conditions, CSV export, columns saved between visits, in-cell editing and row virtualisation. All optional: without the new props the table behaves as before. New texts Table.* in Spanish and English; csvDeTabla and descargarCsv are exported separately.
  • ContextMenu and submenus in ActionMenu (AMS-1698). The right-click menu, with keyboard support, and MenuAction.children for nested lists.
  • CodeField: filling or clearing a box in the middle no longer shifts the digits on the right (AMS-1747). In controlled mode the gap did not survive the parent returning the value without gaps, and typing in box 5 with "123" entered left the 7 in box 4. Now the boxes keep their gaps as long as the parent returns what onChange emitted, and onChange/onComplete always deliver the code without gaps (previously onComplete could arrive with a space inside). Typing over a box that already has a digit replaces it instead of spreading the two, and an empty box is no longer painted with a space.
  • Firefox and WebKit in the e2e, and what they found (AMS-1716). npm run e2e:motores runs the behaviour e2e in both engines (opt-in with AMSET_MOTORES=1, without axe or screenshots), and CI has a fast gate, e2e:motores. Two real bugs that Chromium did not show: in Safari, when closing a dropdown or a confirmation, focus was lost on the <body> (Safari does not give focus to a button when it is clicked with the mouse, and the browser returns focus to "whatever had it"); now the layer returns it to its trigger if it has been lost. And in Firefox and Safari the dialog, the side panel and the command palette disappeared abruptly when closing (only Chromium animates display and overlay); now the exit goes through data-exiting and close() waits for it to finish, the same in all three engines. No API changes.
  • Fields without a box: variant="bare" and --amset-field-touch-height. A field that sits inside another box (a chat bar, a search field inside a toolbar) had to remove its own with custom CSS, and when removing it it also lost the padding and the height, so the text ended up stuck to the edge and the field was taller or shorter than the buttons next to it. bare on TextField, TextArea and the rest of the fields paints no border, background or underline, keeps the height and padding of the size and marks focus with a ring (and the error with the bottom line). And --amset-field-touch-height sets the height of the box on touch (48 px by default, as always) for the front that uses shorter controls on mobile. Additive.

0.11.0, released October 16, 2026

  • Links prefetch their destination (like next/link). RouterProvider accepts prefetch (Next's router.prefetch): Link and ButtonLink call it once per destination on mouse hover or when receiving focus. They do not do so with an external destination, with their own target or when disabled. Optional: without prefetch nothing changes. Next without wiring anything: @amset/ui/next exports NextRouterProvider (App Router) and @amset/ui/next/pages exports NextPagesRouterProvider (Pages Router); they are placed once at the top of the tree and all links navigate and prefetch through the Next router. next is an optional peer: anyone who does not import those subpaths does not need it. New Routing page in Storybook (and, with it, on the website) that explains the generic provider and the two for Next.
  • PageHeader, ButtonGroup and Combobox no longer carry 'use client'. They use no state, effects or handlers of their own: in Next they render as server components and their code does not ship to the browser. Measured in a Next 16 app with a server page that uses them: PageHeader and ButtonGroup disappear from the client JS (-1.4 kB, -469 B gzip). The rest of the components with the directive do use hooks, context or handlers.
  • Texts that the library was still writing by hand in Spanish (AMS-1720). With the language set to English they came out in Spanish: the instalment calculator ("al mes durante", "La última…", the minimum and "TIN / TAE"), the shipping selector ("Te faltan… para que salga gratis"), the board's announcement when moving a card, the "de 5" in the rating, the guided tour's counter, the follow-up question of the NPS survey (its default function was called without a translator), the address and card errors and the colour and channel names of the colour picker. They now come from the message catalogue. addressErrors, postalCodeError, cardNumberError, expiryError, cvvError, cardErrors, moveAnnouncement and colorName accept the translator as an optional last argument and, without it, return the same as before; new channelName. The catalogue test no longer settles for looking for accents: it walks the JSX and the templates of each component.
  • Storybook: the demos, the documentation and the controls, truly in the chosen language (AMS-1720).
    • The texts in the demos (buttons, tables, alerts, amounts) come out in English: the args reach the component already translated and the rest is changed in the preview with src/banco/demos.en.json (3,400 texts; proper names and codes stay as they are). The amounts and dates that the stories formatted with a fixed 'es-ES' use src/banco/formatos.ts. In Spanish everything stays as before, including what the reference screenshots capture.
    • Twenty-five Review and System stories wrapped the demo in a LocaleProvider without a language that forced Spanish: removed.
    • The pages whose stories run in their own frame (inline: false) came out in Spanish: each frame receives the language and the page reads it from its own state.
    • "Jugar" is renamed "Interactivo" (Playground in English). The ids do not change.
    • Clicking a link in a demo no longer navigates the whole window (Storybook sets <base target="_parent">): an anchor scrolls, an external one opens another tab.
    • The controls that the component only reads on mount (default…, initial…, the story's state) now work: the demo is remounted when they change, except in Layers, where changing the title of an open dialog does not close it. currency and locale are chosen from a list: typed by hand they crashed the story.
    • The documentation of 198 files used the API names from before AMS-550 (alCambiar, valorInicial, plana, fantasma, clave…): it now uses the real ones (onChange, initialValue, flat, ghost, key…), checked against the props and types of each component.
    • Seven stories requested icons by their old name ("Subir", "Carrito") and painted the word: fixed, with a test.
  • Server rendering with lazy languages (AMS-1721). loadMessages(locale, catalogs) resolves the language's catalogue before rendering (server component, loader, getServerSideProps) and its result travels as messages in AmsetProvider/LocaleProvider: the HTML comes out translated and hydrates without warnings. It shares the preloadCatalog cache, does not carry 'use client' and rejects if loading fails. An AmsetProvider now also receives messages. No changes for anyone who does not use it.
  • The remaining gaps for migrating a front (AMS-1730). Select gains native (a real <select>: the system picker on mobile, with onChangeEvent) and group on the option, which groups under a heading both in the <select> (optgroup) and in the custom panel. Button gains loadingText, which replaces the content while submitting, without a spinner. IconButton gains fullWidth. NumberField gains onChangeEvent. And --amset-field-required-display: none hides the "required" asterisk (the screen reader still announces it). Everything is additive.
  • The catalogs languages are loaded on demand (AMS-1721). Each tag can be the JSON or a function (fr: () => import('./fr.json')): only the language of the active locale is requested, only once, without repeating the load even if the function is written inline, with StrictMode or when returning to a language already seen. While it arrives, the library's texts are shown; if it fails, a console warning is issued, onCatalogError is called and it is retried when requested again; if the language is changed with another on its way, the late one does not overwrite the new one. preloadCatalog(cargador) preloads (language selector, server). New types CatalogLoader, CatalogSource and CatalogErrorHandler. Compatible: an object still appears on the first paint.
  • API for migrating an existing front (AMS-735). Three things that previously forced rewriting hundreds of handlers: onClick on Button, IconButton and ButtonLink now delivers the native MouseEvent (with preventDefault, stopPropagation and currentTarget; previously it received the unified press); onChangeEvent delivers the <input> event in TextField, PasswordField, TextArea, Checkbox and Switch (e.target.value, e.target.checked), without removing onChange; and inputRef reaches NumberField and FileUpload, so inputRef.current?.click() opens the file picker from a button of your own. Warning: if you passed onClick reading pointerType, use onPress, which still gives the unified press.
  • Entire languages from the provider. AmsetProvider and LocaleProvider accept catalogs ({ fr, 'pt-BR': ptBR }, a JSON of Componente.clave -> text per BCP-47 tag) and AmsetProvider also accepts messages. The catalogue of the active locale is used (exact, language or regional) and anything missing falls back to English key by key; messages takes precedence over catalogs. The template with all the keys is published in @amset/ui/idiomas/es.json and en.json. In development the provider warns once about a language without a catalogue and about keys that do not exist, and validateCatalog(json) (with the types MessageCatalogs, MessageOverrides, MessageKey and CatalogReport) checks it in a test. No changes for anyone who does not pass catalogs.
  • The colour stripe of each agenda event measures 3 px again (AMS-1665). --amset-agenda-slot was declared twice in the same rule and the second one (the gap between the month and the list, 20 px) overrode the stripe's thickness. The thickness moves to its own variable, --amset-agenda-stripe; --amset-agenda-slot is left only for the gap.
  • Grouped fields say that they are disabled (AMS-1268). NumberField, QuantityPicker, CodeField and SignatureField set aria-disabled on their role="group" when they are disabled: previously only the buttons inside were disabled and a screen reader did not say that the whole field could not be used.
  • MIT licence (AMS-1688). The package declares "license": "MIT" and includes the LICENSE file (previously UNLICENSED). No code changes.
  • Texts that the library wrote in Spanish even when the language was English (AMS-1714). They now come from the message catalogue: "Antes" and "Ahorras un" in the amount, "sube" and "baja" in the data change, "de 5" in the ratings summary, the gallery counter, "con subniveles" in the cascading selector, "sin leer" in the tray, "Eventos del..." in the agenda, "Trabajando" in confirm action and "(se abre en otra pestaña)" in the order status. 12 new keys; nothing changes in Spanish.
  • The Select announces that it is required and that it is invalid (AMS-1155). The trigger goes from role="button" to role="combobox" (the APG "select-only combobox" pattern) and carries aria-required and aria-invalid when appropriate. A screen reader now says "combo box" instead of "button". It affects every field built on it: phone, language and currency selector, VAT rate, month and rows per page. If one of your tests looks for that control with getByRole('button'), change it to getByRole('combobox'). Visually nothing changes.
  • The whole Storybook follows the language in the toolbar (AMS-1720). Previously it was only halfway there: with English chosen, the sidebar, the explanation of each story, *Getting started* and the labels in Review and Foundations stayed in Spanish, and the Storybook interface (tabs, settings, props table, shortcuts) came out in English even when Spanish was chosen. Now: the sidebar uses .storybook/menus.en.json (with the names that Storybook derives from the export without accents corrected in menus.es.json); the 672 story explanations are in .storybook/historias.en.json; *Getting started* is two markdown files; the bench labels (Canvas, Matrix and the Foundations tables) are in src/banco/banco.en.json and Canvas now follows the chosen language (without a toolbar it is still Spanish, so the reference screenshots do not change); and what Storybook writes on its own is put into Spanish with .storybook/traductor-de-interfaz.ts. A test (scripts/historias.test.ts) requires every name, explanation and label to have its English version and none to be left over. Storybook's welcome guide and what's-new notice are turned off, as they are its own advertising and cannot be translated. The descriptions in the *Controls* tab, which Storybook fixes when preparing the story and does not repaint, are translated in the DOM itself when English is chosen (only if the translation has the same code in the same order).
  • SiteFooter and @amset/ui/seo (AMS-1750). SiteFooter is the semantic page footer: a <footer> (contentinfo inside AppShell) with brand, link columns —each one a <nav> with its title as its name—, icon-only social links, a legal strip and controls; external links open in another tab with rel="noopener noreferrer" and announce it to screen readers. Sheet pie-sitio.css, texts SiteFooter.*. The new @amset/ui/seo subpath (server, without dependencies or CSS) provides pageMetadata (compatible with Next's Metadata API and with validation that throws SeoError), checkSeo, the JSON-LD builders (organization, webSite, webPage, breadcrumbList, article, product, faqPage, localBusiness), <JsonLd> with safe escaping inside <script>, sitemapEntries/sitemapXml and robotsRules/robotsTxt (blockAll for pre). Additive.
  • Tabs supports tabs without a panel (AMS-1634). A tab without content does not render a panel or carry aria-controls: it is a filter or a mode of the same screen. It is still a tab (arrow keys, focus, selected) and can be mixed with those that do have a panel. Tab.content becomes optional; it is additive. The Dialog already covered the rest of the gaps that the ticket measured in 0.8.0 (header icon and colour, actions in the header, embedded, top-aligned, numeric width, no padding and in a column).
  • A real usage example for every Storybook story, published in @amset/ui/ejemplos.json. Taken from the code itself: if the story has render, it is its body as is; if not, the JSX tag is synthesised with the real args from meta and from the story. An example that is missing a required prop (because its value depended on something outside the file, or on the mapping of a Storybook control such as iconControl) is discarded instead of being published incomplete. scripts/ejemplos.test.ts compiles each synthesised example against the component's real types.

0.10.2, released October 15, 2026

  • ToggleChip, ToggleChipGroup and OptionCard (AMS-1534). The two components that were missing to migrate the assistant without losing design. ToggleChip is a chip that is switched on and off (<button aria-pressed>) with its own colour per chip (color or --amset-chip-color), count, icon, three sizes and a check mark when active; ToggleChipGroup groups them with a name and line wrapping. OptionCard is a RadioGroup option painted as a card, with media, description and the selected mark: inside the group they are laid out in a grid (--amset-option-card-min). Keyboard and focus are those of a native radio. Own sheets chip-alternable.css and tarjeta-opcion.css, a Storybook page in Spanish and English and behaviour tests.

0.10.1, released October 15, 2026

  • The time field now hydrates the same way in English (AMS-1687). With a 12-hour clock, the server (Node's ICU) wrote a thin space before the "AM"/"PM" and Chrome a normal space: React discarded the HTML and threw error 418 on the TimeField and DateTimePicker pages of amset.tech/en/. The field's separators are normalised to the normal space, and a test pins this down.

0.10.0, released October 15, 2026

  • The documentation in English too (AMS-1687). @amset/ui/fichas.json carries en: { titulo, descripcion } on each component page, and there is a new export @amset/ui/props.en.json (the description of each prop in English, keyed by the Spanish text). The extractor breaks the build if a page has no version in src/mensajes/fichas.en.json, and a test watches the coverage and that the Spanish has not changed since it was translated (each translation stores the fingerprint of its source). In Storybook, the documentation pages follow the language selector: with English, the title, text and prop descriptions appear in English (1,758 descriptions, all translated). The standalone stories (their names and comments) and the Getting started pages remain in Spanish.
  • TagsInput and Combobox (AMS-1698). A field of tags that are created as you type (Enter, comma, paste or on leaving the field), without duplicates, with max, validate, transform and name; and Combobox, the search field that allows free text by default. TagsInput.* strings in es and en.
  • DateTimePicker and MonthPicker (AMS-1698). Date and time in a single field (2026-10-15T09:30, with min, max, isDateUnavailable and name) and the month and year picker (2026-10, with the months in the active language and minYear / maxYear). They only notify with the complete value. Their own stylesheet, fecha-hora.css, with --amset-datetime-gap and --amset-monthpicker-gap.
  • Five business fields and five hooks (AMS-1677). New: MoneyField (money that understands a decimal comma, thousands separator and symbol according to the language, and returns a number), SocialSecurityField (Social Security number (NUSS) with both check digits verified), LicensePlateField (current, provincial and moped number plates), CnaeField (CNAE-2009, with an optional catalogue) and VatRateSelect (21, 10, 4 and 0%). The validators are exported on their own (validateSocialSecurity, validatePlate, validateCnae...), as are the money functions (parseMoney, formatMoney, formatMoneyForEdit). Hooks: useMediaQuery, useDisclosure, useClipboard, useHotkeys and useAnnouncer. Strings in Spanish and English, a Storybook page and behaviour tests (keyboard and errors on leaving the field) for each one. See the roadmap in docs/roadmap-amset-ui.md.
  • The library's strings, in Spanish and English (AMS-1687). Every string that a component produced on its own (accessible labels, notices, errors, announcements, summaries, plurals...) now comes from a per-language catalogue (about 560 keys, each in es and en), according to the LocaleProvider. A language without a catalogue falls back to English; without a provider it stays in Spanish, so nothing changes for anyone already using the library. New: the provider's messages prop, translatorFor, useMessages, and an optional last parameter msg in the functions that write a string. Strings with quantities use Intl.PluralRules. Storybook includes a language selector (es, en, ar). Three tests check that there are no unused keys, that the English carries no accents and that no component goes back to hard-coding a Spanish string.
  • CSS comes in per component, not whole (AMS-1677). Each module in dist/ now imports ITS stylesheet (and base.css, with the tokens and the reset), so the bundler takes only the CSS of what you use: a Button in Vite goes from 392 kB to 23 kB of CSS (4 kB gzip), and a screen with 20 components from 395 to 133 kB, with the same pixel result as the single stylesheet. New exports: @amset/ui/estilos/base.css and @amset/ui/estilos/<hoja>.css. What changes when you upgrade: (1) if you still import @amset/ui/estilos.css you pay for the CSS twice: remove it; (2) with the Next Pages Router you need transpilePackages: ['@amset/ui'] (without it, ERR_UNKNOWN_FILE_EXTENSION when building); (3) in Jest, a moduleNameMapper for .css$. Tested on Vite 8, webpack 5, and Next 16 with App Router and Pages Router. The imports go on the same line as "use client" so as not to move the sourcemaps.
  • Eleven components can be used from a Server Component (AMS-1677). Previously ALL components carried "use client", so in Next a server-only layout still shipped the JS of Text, Title, Badge, Separator, EmptyState, VisuallyHidden, CompactGroup, DataList, List, Stack/Grid/Center/Container and Blockquote. They no longer carry it: they use no hooks, event handlers or browser APIs. Verified by importing and calling them with Node's react-server condition, and a new test (componentes-servidor.test.ts) prevents a file without the directive from starting to need the browser. No API changes. The components that depend on a provider, on per-language text or on another client component (PageHeader, ButtonGroup, Result) remain marked, on purpose.

0.9.6, released October 15, 2026

  • A light theme inside a dark page now looks light. The light values only existed in :root: a <div data-tema="light"> inside a page in dark mode stayed dark, without any error (the theme sample on amset.tech came out dark with "Light" selected). Now [data-tema='light'] works on any element, just like the dark one, and where they are nested the innermost one wins. For the text and background of the area to change, the area also needs the amset-canvas class (or its own colour and background).
  • Links are no longer underlined. Link is rendered without underline, in the accent colour and in bold (the weight is the cue that does not depend on colour: axe stays clean with the link inside a paragraph). underline="always" always underlines it and underline="hover" on mouse hover. Nor are the following underlined on hover any more: the titles of Card, the breadcrumbs, the FileList download, the Stat link, the filters link, the ProductCard link or those in GlobalAlert. bare still exists and now only removes the weight.

0.9.5, released October 15, 2026

Quality review of twelve components that Aitor flagged on amset.tech (AMS-1701). Everything measured in the browser before and after, with a test that fails without the fix.

  • DateRangePicker now lets you pick the second end in the other month. With two months in view, the first month followed the focused day, and a day receives focus when the mouse button is pressed, before it is released: the grids shifted one month under the pointer and the click landed somewhere else. Now the view only moves when focus leaves it. Along the way: each month has its name above it, "Next month" looks at the last visible month and DatePicker/DateRangePicker follow their value (a QuickRange shortcut now writes to the field).
  • Countdown with variants: variant="roll" (the default, the digits that change roll), "ring" (a ring that empties continuously) and "text" (the previous look). The screen reader announcements do not change.
  • Square ActivityMap: the table caption was being inserted as a cell of the first column; now the days column is as wide as its content, the cells are square, the months go above and the legend lines up with the grid.
  • LocalePicker with flags (withFlags, LocaleOption.flag, localeFlag), and the placeholder of an empty DatePicker shows each language's letters (yyyy, jj/mm/aaaa, tt.mm.jjjj).
  • PhoneField with the 244 ITU countries and territories, each with its flag (our own SVGs, real geometry) and its name in the active language; you search by typing the name ("Fra"), space keeps searching, Page Up/Page Down jump ten and the mouse wheel now scrolls the list. New prop initialCountry. The options are announced as "Spain (+34)".
    • The wheel did not scroll in ANY long floating list: the panel also listened to the scrolling of its own list and, on re-measuring itself, sent it back to the top. Fixed in flotante.tsx, for all of them.
    • ListOption.textValue (search text separate from the label), CountryPrefix.areaCodes.
  • Amount aligned: its own line height, "/month" next to the price, the discount badge centred with the digits; three sizes in a row share a baseline.
  • Table: sort and filter are two identical 22 px boxes; the active filter is marked with the accent colour and the filled funnel, with no block background. The sticky row header no longer picks up the header grey.
  • Tabs without a scrollbar: one pixel of negative margin overflowed the list and Windows showed a scrollbar with arrows. If the tabs do not fit, the wheel moves them sideways without a bar. orientation="vertical" is now rendered as a column.
  • SideNav with the design of the AMSET menu (the assistant's): icon tile, name and description, current page marker, collapsible groups with a count, collapsing to a rail with a tooltip. New backwards-compatible API: description, groups (NavGroup, NavItem), onCollapsedChange, header/footer as a function of { collapsed }. Widths 13/16/19 rem. AppShell no longer stacks the menu above the content on a narrow screen: a rail remains at the side.
  • Closed Collapsible and Accordion without a ghost gap: the closed panel still had a box and took up the gap.

0.9.4, released October 15, 2026

  • Two more hydrations that broke on a real website (AMS-1666). They came up when sweeping amset.tech in production, where the HTML is rendered by a CI runner in UTC and the browser is in another time zone and on another day:
    • QuickRange with a period set marked the matching shortcut ("this month") according to the day it was when rendering: the server said one and the browser another. Now it marks none until the day is known.
    • RelativeTime with withFullDate wrote the full date in the server's time zone ("15:45") and the browser in its own ("17:45"). Now the server's version is accepted the first time and, as soon as the node hydrates, it is remounted with the viewer's time (useInBrowser): suppressHydrationWarning silences the warning but leaves the server's text.
    • hidratacion.test.tsx now renders the server in UTC and the browser in Europe/Madrid, and checks that the text ends up correct.
  • The Table API moves to English, with the old names still working (AMS-1677). It was the only component that mixed languages (onSort alongside alSeleccionar, paginacion, enServidor...). Now: serverSide, selectedKeys, onSelectionChange, pagination, configurableColumns, onVisibleColumnsChange, resizableColumns, onRowPress, toolbar; in the columns sortable, filterable, numeric, hiddenByDefault, sticky, rowHeader; and the sort order is { column, direction }. The old names keep working with a console warning (once per name, never in production) and will be removed in the next major version. onSort delivers direction and also sentido in the meantime, so as not to break anyone reading it. Equivalence table in the README, which also had the Table section written against an earlier API that no longer existed.
  • The library's API, readable by AI agents (AMS-1677). The build now writes dist/api.json with the real props of each component (type, required or not, comment), taken from the compiler and not from a hand-written list; native HTML attributes are not enumerated, it is noted that they are accepted. From it come dist/AGENTS.md, dist/llms.txt and dist/llms-full.txt, and the package ships a skill, skills/amset-ui/SKILL.md, for anyone building with the library from Claude Code, OpenCode or another agent. New exports: @amset/ui/api.json, /AGENTS.md, /llms.txt and /llms-full.txt. No changes to the components' API.
  • The verificar job builds before testing. The 8 tests of the themes CLI were always skipped in CI because dist/ did not exist yet; now they run.

0.9.3, released October 14, 2026

  • Timeline no longer breaks hydration because of the spaces in the date (AMS-1666). The span of a bar ("24 Oct – 4 Nov") is written by Intl.DateTimeFormat.formatRange, and with ICU 78 (the one in Node 22.23) it puts thin spaces (U+2009) on either side of the dash, whereas Chrome puts normal spaces. HTML rendered in Node and hydrated in the browser did not match and React threw error 418. Now they are normalised to the normal space. It came to light when removing from the amset.tech website the patch that worked around the timeline: the library's hydration test did not catch it because it runs on a single ICU.

0.9.2, released October 14, 2026

  • useTabs is exported from the index (AMS-1670). It is the tabs logic without its clothes: for a screen that already renders its own panel and only needs the list with its keyboard behaviour (the arrows wrap around, Home and End, only the active tab is in the tab order, disabled ones are skipped, automatic or manual activation). TabSlot, TabsSetup and TabsPlumbing are also exported. Previously only Tabs and the gallery used it. Filtering the same list is not a job for tabs: that is SegmentedGroup.
  • Hydration no longer breaks on a website with SSR or exported as static (AMS-1666). A component that reads "today" or "now" while rendering comes out different on the server and in the browser, and React discards the HTML and re-renders (error 418 in the console, measured in production on amset.tech). It happened in Calendar, DatePicker, DateRangePicker, QuickRange, MonthAgenda, ShippingPicker and Timeline. Now the first render (the server's and the hydration's) does not know the day, and the browser learns it right after, without an extra frame in an app that only renders on the client:
    • Calendar and MonthAgenda without a day to anchor them (no value, no defaultFocusedValue, no initialDay) render only the space they occupy, with aria-busy, until "today" arrives; with a given day they are rendered in full from the server.
    • The today marker (data-today) and the today line of Timeline appear once in the browser.
    • ShippingPicker writes the delivery time once in the browser, unless it is given now.
    • There is a test that watches this on ALL the component pages (hidratacion.test.tsx): it renders one day on the server, hydrates on another and counts the mismatches. A new page is included automatically.

0.9.1, released October 14, 2026

  • The text of the 149 component pages, up to date with the code (AMS-1659). The pages in the workbench (and from now on those on amset.tech) still talked about React Aria, which is no longer used, and used the Spanish names of the old API (etiqueta, tamano, alNavegar, ProveedorIdioma…). Reviewed one by one against the code: real props and values, keyboard and accessibility as they work today, and out with what no longer exists.
  • @amset/ui/fichas.json: each component's page, as data (AMS-1659). One entry per component with its import name, its module, its Spanish title, its family, the text of its page (what it is, when to use it and when not to, keyboard, accessibility, fine details of its appearance; in markdown) and the ids of its stories in the workbench. It is generated from the Storybook stories at build time, so the amset.tech website no longer carries a hand-made copy that went stale.
  • The description of Dialog accepts a node (a status tag next to the sentence) and is still announced as a description. Without a description it no longer points with aria-describedby to an element that does not exist.
  • Dialog now covers what a modal with a branded header needed (AMS-1534, at the request of amset-assistant, which had 42 modals of its own). Everything is optional and what already existed does not change:
    • title can be a node; icon sets a custom icon instead of the tone's and accent gives it colour (--amset-dialog-accent); headerActions holds buttons between the title and the close cross.
    • width (pixels if it is a number, a CSS measure if it is a string) takes precedence over size; placement="top" sticks it to the top, for a palette that grows downwards; hideHeader does not render the header but the title is still the accessible name.
    • flush removes the body padding and bodyColumn makes it a flex column, for content with its own scrolling.
    • inline: the same box IN the page, as a region with its title. No <dialog>, backdrop, Escape or focus trap, no close cross and no invented "Close": it lets you reuse as a screen panel what elsewhere is a dialog.

0.9.0, released October 14, 2026

  • QRCode (AMS-1638). A QR code in SVG with no dependencies: <QRCode value label level size margin onError />. Behind it is our own encoder (encodeQr, ISO 18004, byte/UTF-8 mode, versions 1–40, levels L/M/Q/H), checked module by module against the qrcode package (2,032 identical matrices with a forced mask), and the rendered SVG is decoded with an independent reader. It is always black on white, also in the dark theme: that is what a scanner reads. Mentions, Watermark, Masonry and antd's image viewer are not built: no measured demand.
  • TreeSelect: a dropdown with the tree inside (AMS-1636). The field is the Select's and the panel is the Tree: choosing a category or a department from a hierarchy without having the tree always in view. One (selection="single", closes on selection) or several ("multiple", stays open and the selection comes out in tree order); any node can be chosen, branch or leaf, except disabled ones. The branches leading to the selection open by themselves and focus goes to the selected row. displayRender, name (one hidden field per key). The keyboard is the Tree's plus Down/Up to open and Escape to close. No search inside the panel (the Tree does not have it) and no connected version of its own: it goes with FormField.
  • Tree: lazy loading and cascading selection (AMS-1637).
    • loadData(clave) requests the children of an isLeaf: false branch the first time it is opened; the chevron gives way to a ring (aria-busy) and "Loading…" is announced. If it fails, the branch stays as it was and the chevron retries.
    • cascade (with selection="multiple"): checking a branch checks everything inside it, and a branch with only part of it checked appears "half" checked (and the reader says "partly selected"). The value comes out in tree order; disabled items are not touched. With lazy loading, an already checked branch passes the check on to whatever arrives.
    • Measured against the ticket: the Carousel already had autoplay (automatic, with a pause button, pausing on mouse hover or focus and respecting "reduced motion") and external control (index + onChange); it was not touched. Dragging nodes to move them is left out (it needs a keyboard alternative designed separately).
  • Cascader: cascading selector (AMS-1635). Country > province > city in a single field, with one column per level, instead of several chained Select components that share neither state nor keyboard. The value is the path (['es', 'madrid', 'alcala']), controlled or not. changeOnSelect lets you choose a branch without going down to a leaf; loadData(ruta) requests the children of an isLeaf: false option the first time it is opened (the column says "Loading…"); displayRender, levelLabels (the name of each level for the screen reader), name (the leaf's key is submitted). Keyboard: up/down arrows through the column, right enters, left goes back, Enter selects, Escape closes. Branches are announced "with sublevels". FormCascader connects it to the form. No search or multiple selection yet.
  • Actually uploading files (AMS-1555). FileUpload chose and validated, but uploaded nothing: progress, failure and retry were rewritten by every project. Now:
    • Uploader: the drop zone, a bar for each file being uploaded, the reason and "Retry" if it fails, and removing one that is uploading cancels the request. It uploads over HTTP with http={{ action, method, headers, data, fieldName, withCredentials }} or wherever you say with customRequest({ file, onProgress, signal }) (a signed URL, an SDK). How many upload at once: concurrency (3). It notifies with onChange, onUploaded and onFailed, and announces "factura.pdf uploaded" or the failure.
    • useFileUploads: the engine without rendering anything (queue, progress, failure, retry, cancel, pending, and autoStart: false + start() to upload with a "Send" button), to build a different screen.
    • httpUpload, UploadError, isAbort: the default request, with XMLHttpRequest (it is the only one that gives upload progress; fetch does not).
    • FileUpload gains maxCount (files that do not fit are rejected with their reason), directory (choose a folder; dropping a folder onto it does not traverse it), pasting a file or a screenshot with Ctrl+V, and hideList.
    • Still true: the type and size filter is a convenience, the server has to check it again.
  • Fields consistent with one another and a clearable NumberField (AMS-1625, AMS-1626).
    • NumberField can be cleared: deleting the text and leaving the field notifies with onChange(NaN) —the "no value" that consumers were already writing— and leaves the field blank. Previously the previous number came back and was submitted. What is not a number (abc) is still discarded. QuantityPicker does not change: a quantity cannot be empty. FormNumberField translates it to undefined.
    • DocumentField and IbanField accept onBlur.
    • PhoneField accepts name (it submits the full number in E.164, not the visible part), QuantityPicker accepts name and Slider accepts name (each thumb carries it); they are now submitted in a native form.
    • The MultiSelect error carries the warning icon, like that of the other fields.
    • Slider already accepted onBlur (it is an attribute of its <div>); the ticket said it did not.
  • Shell, sticky on scroll and attached controls (AMS-1558).
    • AppShell: header, side column, content and footer in a CSS grid (antd's Layout, Header, Sider and Content in one). The content is a <main id="contenido" tabindex="-1">, the target of the TopBar's "skip to content"; the header stays fixed and the column stays fixed just below it (it measures its height) with its own scrolling. Below 48 rem the column moves above the content and stops being fixed. siderWidth, mainId, stickyHeader.
    • Affix: a block that stays stuck when scrolling, at the top (offsetTop) or at the bottom (offsetBottom), with onChange(pegado). It is position: sticky: the container has to be taller than the block and no ancestor can have overflow: hidden or auto.
    • TableOfContents gains offset: with a 64 px fixed header, clicking an entry leaves the section below it and the reading line is counted from its bottom edge. Without offset it behaves as before.
    • CompactGroup: a field with its button (or several fields) joined with no gaps and with square corners where they meet; antd's Space.Compact. The fields inside carry hiddenLabel. It works with any field (text, number, select...) and with Button, IconButton and ButtonLink.
    • Not done, with their ticket: Cascader (AMS-1635), TreeSelect (AMS-1636), lazy loading and dragging in the Tree and autoplay in the Carousel (AMS-1637), and the minor pieces (AMS-1638).
  • @amset/ui/formulario: all fields connected and the API in English (AMS-1557). Previously it connected 8 fields and had the names in Spanish (Formulario, CampoTextoForm, BotonEnviar, resolverDeEsquema), which left two languages in the same import.
    • Names in English: Form (props schema, initialValues, onSubmit, validateOn), SubmitButton, schemaResolver and the types FormProps, SubmitResponse, StandardSchema, StandardIssue. The usual names keep working with the same props —they are deprecated aliases and will be removed in a major version—: there is nothing to migrate today.
    • New fields: FormPasswordField, FormPhoneField, FormDocumentField, FormIbanField, FormCodeField, FormNumberField, FormQuantityPicker, FormSlider (one value), FormRating, FormMultiSelect, FormSegmentedGroup (one option), FormDateRangePicker, FormTimeField, FormColorPicker, FormCardField, FormAddressField, FormSignatureField and FormFileUpload. Those with a compound value (card, address) distribute the schema error to each subfield: tarjeta.number fails on the number, not on the block.
    • FormField: connects ANY other field with a function that receives value, onChange, onBlur and error. It is the equivalent of antd's Form.Item, and covers anything without its own version, or a component of your own.
    • The old connected fields are now called FormTextField, FormTextArea, FormSelect, FormSearchField, FormDatePicker, FormCheckbox, FormSwitch and FormRadioGroup.
    • Note: ROOT_FIELD is still 'raiz' and not 'root'; it is the key that projects already read in formState.errors.
  • Bar tone, button shape, group size and broken-photo notice (AMS-1556).
    • Progress gains tone (success, error, warning, info, neutral; without a tone, accent): a failed upload is painted red and a finished one, green. Colour is not the only thing that says so: put it in the text as well with valueLabel.
    • Button, ButtonLink and IconButton gain shape: 'default' or 'pill' (fully rounded corners) and, only on IconButton, 'circle'. Inside a ButtonGroup only the outer corners of the group are rounded. The Shape vocabulary and SHAPES are declared in variantes.
    • ButtonGroup gains size: it is written once and inherited by the Button, IconButton and ButtonLink inside that do not state their own (one with its own size keeps it).
    • Avatar gains onError(src): it reports that the photo has not loaded. The avatar already fell back to initials by itself; this is for retrying with another address or keeping a record.
    • Not needed: ButtonGroup already had orientation="vertical".
  • Table: expandable rows, per-row class, maximum height and more (AMS-1554).
    • renderExpandedRow(row) makes each row expandable: it gains a button with aria-expanded (which points to its detail with aria-controls when open) and the detail appears below, at full width. rowExpandable(row) leaves rows that have no detail without a button. It is reachable with the keyboard (right arrow from the row, Enter) and expanding does not count as pressing the row (alPulsarFila). Controllable with expandedKeys / onExpandedChange, or initialExpandedKeys.
    • rowClassName(row, index) adds a class to a specific row (an overdue invoice in red).
    • maxHeight ('24rem', '60vh'): the body scrolls and the header and footer stay fixed. It was the --amset-table-height variable; now it is a prop.
    • TableColumn.pegada accepts 'end' (the actions column stays at the end when scrolling horizontally); true and 'start' still stick to the start.
    • TableColumn.ellipsis truncates long text with "…" on one line and leaves the full text in the title.
    • Fixed: with the table scrolled, the sticky row header (th) was painted over the "Number" column header, and the fixed footer ended up below the sticky columns.
    • Not needed: the footer totals (TableColumn.footer) and the sticky header already existed.
    • Still not available: colSpan per cell, bordered, tableLayout, showHeader, caption.
  • Render functions (AMS-1550). Until now an option, a cell or a day was text plus an icon: as soon as an avatar, two lines or a price was needed, the component was discarded. Now the piece itself can be rendered. The rule is that of Chip: label is what is read and the function renders what is seen, so the accessible name does not change and the piece remains selectable with mouse and keyboard.
    • renderOption(option, { selected, disabled }) in Select, MultiSelect, SearchField, SelectableList and VirtualList (the latter only calls it for the rendered rows).
    • renderValue(option) in Select: the selection inside the closed field.
    • renderTag(option) in MultiSelect: each tag of the selection.
    • renderItem(item, { selected, disabled }) in TransferList.
    • renderDay(date, number) in Calendar: what goes inside each day. The day's name does not change.
    • renderPage(page, { current }) in Pagination: the content of each page's button (only with shape="full").
    • renderCrumb(crumb, { current }) in Breadcrumbs.
    • TableColumn.render now also receives index, the position of the row in the visible page.
    • The OptionRender and OptionRenderState types are exported.
    • Everything is additive. Still missing is what antd calls expandedRowRender and summary in the table: they belong to AMS-1554.

0.8.0, released October 13, 2026

  • Who closes a layer and what opens it (AMS-1553).
    • Dialog gains isDismissable (clicking outside closes it; with false, it only goes away through its buttons, Escape or open) and isKeyboardDismissDisabled, the same two that Drawer already had. It is antd's maskClosable. kind="alert" still does not close when clicking outside.
    • Tooltip gains openOn ('hover' | 'focus' | 'press', by default ['hover', 'focus']). With 'press' it toggles on press and closes when pressing outside: that is what makes it visible on a touch screen. The element's own onClick is respected.
    • Dropdown gains openOn ('press' | 'contextmenu'). With 'contextmenu' it opens with a right click and the browser's menu does not appear. There is no 'hover', on purpose: the panel is modal.
    • There is no zIndex or portal container, and it is not an oversight: layers go in the browser's top layer, which sits above any z-index (measured with 2147483647) and works inside a shadow DOM. A panel opened from inside another library's modal lives in that modal, so it remains clickable.
  • Loading state (AMS-1552). loading (and loadingText, for what it says) reaches the components that did not have it: Switch, Select, MultiSelect, SearchField, SelectableList, VirtualList, Dialog and Drawer.
    • Switch: while waiting for confirmation it does NOT change value or notify, but stays in the tab order (disabling it would take the focus away), with a spinner in the thumb.
    • Select, MultiSelect, SearchField: the field is marked busy (aria-busy), shows a ring and the panel shows a Cargando… row. Any options already present stay.
    • SelectableList, VirtualList: busy, with the row underneath (infinite scroll).
    • Dialog, Drawer: the content is replaced by a skeleton and announced; the title and the buttons stay.
  • Spinner gains delay (it does not appear until that time has passed, so that a 100 ms load does not flicker) and can wrap a block with children: it leaves it greyed out, inert and with the spinner on top.
  • inputRef on TextField, PasswordField and TextArea: the node of the actual <input> or <textarea>, not that of the box. To focus a field when opening inline editing, select a token that is about to be copied or return focus to a search box. It accepts an object and a function, and coexists with autoFocus (the attribute still reaches the DOM). It was what prevented migrating the amset-assistant fields that are focused by hand.
  • A field's id reaches its control (AMS-1488). TextArea, TimeField, DatePicker and DateRangePicker accepted it and threw it away; MultiSelect and SearchField left it on the outer <div>. Now, in a field with an <input> the for of its label points to it, and in one whose control is a row of segments (date, time, range) it lands on the group itself.
  • PhoneField, QuantityPicker, TimeField, DatePicker and DateRangePicker accept id in the type: at runtime they already forwarded it, but the compiler did not allow it to be written.

0.7.0, released October 13, 2026

All components pass through the attributes of their element (AMS-1549). Previously 14 out of 158 did; now all those that render DOM do. It is an additive change: measured against 0.6.2, no prop has been removed or had its type changed, so upgrading should not break anyone's build.

### What you can do now

<Tag id="estado-pago" title="Pagada el 12/10" data-testid="etiqueta" style={{ marginInlineStart: 8 }}>
  Pagada
</Tag>

Any attribute of the element that the component renders at its root: id, title, lang, data-*, aria-*, style and the mouse and keyboard handlers. Two rules worth knowing:

  • className is added to the library's classes and style is merged with the component's. Neither of them replaces it.
  • In a field (TextField, Select, NumberField…) the attributes go to the control —that is where anyone writing a data-testid or an autoComplete expects them—, and className and style go to the box, which is what is seen.

DomProps<'div'> is also exported, to type a wrapper of your own without listing attributes by hand.

### What is NOT passed through, and why

There are element props that a component sets itself, and one from outside would be accepted and silently discarded. In those cases the type does not accept them, so you find out when compiling instead of having to check that nothing happens:

ComponentDoes not acceptUse
Dialog, Drawer, IdleWarning, CommandPaletteonCancel, onClose, onClick, role, aria-labelledby, aria-describedbyonOpenChange, title, description
Dropdown, ConfirmAction, ShareButton, NotificationTray, ActionMenuid, role, tabIndex, onKeyDown, onClick, aria-label, aria-labelledby, aria-describedbytitle, onOpenChange
Tooltipid, role—
OverlayCloseButtononClickonPress
Checkbox, SwitchonKeyDown, onKeyUp, onPointerEnter, onPointerLeave, onPointerDownwrap it in your own element
CarouselonFocus, onBlur—
Tree, SelectableList, VirtualList, SortableListonKeyDown, onFocus, onBlur, role, tabIndex—
DocumentField, IbanFieldonBlur—

### Other changes

  • In a text field, an external aria-describedby is added to the help text and the error instead of overwriting them (TextField, TextArea, PasswordField).
  • Card separates in the type what governs each form (static, pressable, link); what it accepted does not change.
  • FieldRootPassThrough and FieldInputPassThrough now accept style.
  • Three tests check the result in the library itself: that the attribute arrives, that it lands on the root and not on an internal piece, and that an external handler is called.

Before 0.7.0

There was no changelog. Earlier versions are in the GitLab history.