Skip to content
EspañolEnglish

What's new

What arrived in each version, taken from the changelog (CHANGELOG.md) that ships in the 0.35.0 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.35.0, released November 3, 2026

  • Table: several loose cell ranges at once (AMS-2351). With cellSelection, Ctrl/Cmd+click adds another range without dropping the existing ones, and on a cell already selected it removes it (its range is split into what surrounded it); Shift+F8 starts another range from the focused cell, and Shift+arrows or Shift+click stretch the last one. Ranges that touch look like one, with the outline on the outside. Ctrl/Cmd+C copies several ranges only if they share rows (side by side) or columns (one below the other), as in Excel; if they do not line up, the clipboard is left alone and the reason is announced. A single pasted value fills every cell of every range, and several cells are not pasted over several ranges. onCellRangeChange keeps notifying the active range (the last one) and onCellRangesChange gives the whole list. Without Ctrl or F8 everything stays as before. And the range outline finally shows: the zero border of the general rule beat the 2 px of each side on specificity and only the background was painted (measured in Chromium; a browser test pins it). And it also looks right in dark: the fill is now --amset-accent-soft, the same as a selected row (before, a 12 % accent mix that in dark was barely distinguishable from the row), and the outline --amset-accent-text (the plain accent gave 2.9:1 against the row in dark; now 7.1:1 in light and 8.6:1 in dark).
  • Table: leaving an edit that cannot be saved, range without folded groups, row count when paginated, load error, select everything filtered, batch paste and fit the width to the content (AMS-2351). Leaving a cell being edited with something invalid (or a failed save) discards the edit and the table says why; before, the field stayed open with the error and the focus somewhere else, where neither Enter nor Escape reached it. With the focus inside, the error is still shown to fix it. Shift+arrows no longer lands on the rows of a folded group. When paginated, the grid carries aria-rowcount with the total and each row its real aria-rowindex (with pagination.total, the server one). error (with true, the default text) replaces the rows with a notice and onRetry adds “Retry”. With the whole page checked and more rows on other pages, “Select all N rows” appears, as in email. onCellsEdit receives everything pasted in a single call. Double-clicking a column handle fits it to its widest content.
  • DataImportWizard: importing in batches with progress and cancel, UTF-16 CSV, hidden sheets, merged cells and formula errors (AMS-2352). With batchSize, onImport is called once per batch, with a progress bar and “Cancel the import”; its second argument brings batch, batches and a signal that fires on cancel. If a batch other than the first fails or is cancelled, the result says how many rows went in and how many are left. Change: onImport always receives that second argument, also without batches ({ batch: 1, batches: 1, signal }); a test that compares the call with a single argument has to add it. UTF-16 CSV files (Excel’s “Unicode text”), with or without a mark, are read correctly. A hidden sheet of the workbook is marked “(hidden)” and is not the one opened. The value of a merged cell is copied to the rows with data it covers. A cell with #DIV/0!, #N/A and the other Excel errors is a row error, not data.
  • FileManager: uploading one by one with cancel and retry, per-item result when deleting or moving, maximum size and repeated names (AMS-2353). onUploadFile uploads each file separately: it shows in a list with its bar, can be cancelled (the signal fires) and, if it fails, stays with the reason to retry it or remove it. onDelete and onMove can return { failed: [{ id, message }] }: what failed is named and stays selected, and the rest is taken as done. maxSize (in bytes) and rejectDuplicates leave out what exceeds the size or has the same name as something in the folder, also with the button, and say how many and why. screenUploads does that filtering outside the component.
  • SchemaForm: draft-07 fixed-position lists, objects without fields, values that arrive late, “no changes” after saving, keys with a dot or numeric keys, and a list’s error and focus (AMS-2354). items: [a, b] with additionalItems (draft-07, OpenAPI 3.0) is read as prefixItems and one field per position is painted. An object with nothing to ask no longer paints an empty group. If initialValues changes (data arriving from a request), the form takes it without overwriting what was already changed by hand. A successful submit counts as saved: onChange goes back to dirty: false. A property called version.mayor or 2024 is edited and submitted in its place; before, react-hook-form read it as a path. A list’s error describes its group (aria-describedby) and removing a row moves the focus to the one taking its place, or to “Add” if none is left.
  • DataImportWizard: each error shows the file’s own row, a decompression cap and notices that say why (AMS-2352). Errors cite the real CSV line (counting blank lines and the ones inside a quoted field) and the Excel row, and a correction is written to the right row even with gaps before it. An .xlsx with a cell in Excel’s last row no longer creates a million empty rows in memory, the ZIP has a 256 MB uncompressed cap, and a file of more than 64 MB inside the workbook stops the read instead of being skipped silently (a huge sharedStrings used to leave the cells empty). If onImport fails, the notice gives its reason; the review shows more rows, 50 at a time; it warns when one column feeds two fields; with two files chosen in a row the last one wins, and when it finishes focus moves to the result.
  • FileManager and MediaManager: dropped files go through accept, a bar for several uploads, shortcuts and “Load more” (AMS-2353). What is dropped on them goes through the same filter as what is picked with the button, and it says how many files were left out. With two uploads at once, the first one to finish no longer hides the other’s bar: the bar shows the average of the ones still running. nameProblem rejects Windows reserved names (CON, NUL, COM1…), names ending in a dot and names longer than 255 characters. In FileManager, Ctrl+A selects what is shown, Delete asks to delete with its confirmation, F2 renames the focused entry and Shift+click selects a range; the confirmation says what is deleted, not just how many, and hasMore with onLoadMore adds “Load more”. An onKeyDown from outside is still called.
  • Table: empty values last in both directions, page-only selection without hidden rows, tree search and local dates on export (AMS-2351). Empty values (and a NaN) go last in descending order too, in the table and in the tree. The header checkbox checks or unchecks only the page, without clearing what was picked on other pages, and whatever the filter, the search or the data hides stops counting as selected: actions never get rows that are not shown (with server data nothing is trimmed). searchable also searches the rows of a tree, and with serverSide or server pagination the box only shows with onSearchChange, which reports the text. A column that arrives after mounting is shown (unless hiddenByDefault) and pagination.size also applies after mounting. A cell save that rejects with something that is not an Error no longer leaves the editor busy. Format change: the CSV exports dates with a time in local time with its offset (2026-02-01T11:30:00+01:00), as they are shown on screen, instead of in UTC.
  • SchemaForm and its validator: patterns that hang, multipleOf with amounts, keywords that were ignored and submits that failed silently (AMS-2354). A pattern with a nested repetition and no separator ((a+)+, (\w+\s?)*) is rejected as a SchemaFormError: with 26 letters it took 27 seconds on every keystroke. multipleOf 0.01 now works for large amounts. An inherited property (constructor) does not count as present. patternProperties (and additionalProperties: false no longer rejects what matches), propertyNames, minProperties, maxProperties, prefixItems, contains, minContains, maxContains and nullable are validated. A $ref with properties or required next to it adds both. Key change: general schema errors go to root (ROOT_FIELD), the same as the server ones; they used to go to raiz. A submit that throws is reported in the form, switching the branch of a oneOf keeps the shared fields and a new row of a list of objects comes with its default values.

0.34.0, released November 2, 2026

  • MediaManager: drop files on it, “Load more” and a progress bar; FileManager with a progress bar (AMS-2328). onUpload now gets a second argument, onProgress: call it with a value from 0 to 100 and the manager shows a bar with the progress until the upload finishes or fails (functions that do not use it keep working as before). MediaManager accepts files dropped on the library, with the border highlighted while dragging, and with hasMore and onLoadMore it puts a “Load more” button at the end of the grid to ask the server for the next batch.
  • SchemaForm: pick the branch of a oneOf or anyOf of objects that have no field telling them apart (AMS-2327). When the branches are objects with their own fields and none is recognised by a const value, the form shows a “Type” dropdown (with each branch's title) and shows only the fields of the chosen one; changing branch clears the previous branch's fields so they neither clash when validating nor get sent.

0.33.0, released November 2, 2026

  • Selectors with thousands of options: only the visible ones are painted, and the scroll bar follows the active option with the keyboard (AMS-2326). Select, MultiSelect and ComboBox share the list, which with more than 300 single-line options (no groups, no second line and no renderOption) paints about twenty rows and keeps the room of the rest with the list's own padding. The arrow keys, Page Up, Page Down, Home, End and type-ahead take the scroll bar to the active option even when it is not painted, and on reopening the chosen one is in view. Every row carries aria-posinset and aria-setsize so a screen reader says "1,234 of 5,000". With groups, descriptions or renderOption nothing is trimmed, because the room assumes rows of the same height. New "Long list" story and e2e/select-long-list.spec.ts, measured in Chromium with 5,000 options: 15 to 22 rows painted and the active one always in view.

0.32.0, released November 1, 2026

  • Calendar: jump to another month and year from the title with monthJump (AMS-2325). With monthJump the month title is a button that opens a view with the twelve months of the year; pressing the year shows a block of twelve years. The side buttons change the year or the block, months outside minValue and maxValue are disabled, and choosing one returns the grid at that month (and reports it with onFocusChange). Only with one month in view and in the Gregorian calendar; in any other, or with several months, the title stays plain text.
  • MultiSelect: checklist mode and a cap on painted options; TreeSelect: search (AMS-2326). checklist keeps the list open while ticking, shows what is chosen with its mark (aria-multiselectable), removes a chosen one when pressed and, on reaching max, disables the remaining ones instead of hiding them. maxVisibleOptions paints at most that many options in lists of thousands and says how many are shown. TreeSelect with searchable adds a search box that keeps what matches with the path to it open. Free text in ComboBox already existed (allowsCustomValue). VirtualList has not been put inside the drop-down lists: the optional cut of maxVisibleOptions is what there is.
  • Table: date, checkbox and custom cell editors, and server-side validation (AMS-2324). editor.type adds date (a date picker that saves YYYY-MM-DD), checkbox (saves a boolean) and custom, with a render that receives commit, cancel, error and saving. validateAsync checks what was typed against the server after validate, with the cell busy and “Checking…”, and if it returns a reason the edit stays open with it; it also applies to each pasted cell. CellEdit.value can now be a boolean (before string | number | null): whoever assigns it to a variable of that type has to widen it. Pasting understands “yes”, “no”, “true” and “false” in a checkbox, YYYY-MM-DD in a date and parse in a custom editor.
  • Touch Board, editable Timeline and DockLayout with a split centre (AMS-2333). In Board, the grip of each card is dragged with a finger (pointer events, touch-action: none, 44 px on touch screens) and dropped on another card or on the gap of a column; mouse dragging and the keyboard do not change. Timeline with onTaskChange makes the bars editable: drag the body to move the task, its ends to change the start or the end, and with the keyboard the arrows move one day (Shift, the end; Alt, the start), announcing what it became; it hands over the new dates and whoever uses it decides whether to accept them. DockLayout adds an optional split area next to the centre (with its separator, collapse and a “Split the centre” option in the panel menu); a layout written without it stays valid. A DockLayout inside a panel already worked and is now tested.
  • Button with preventFocusOnPress and Breadcrumbs with disabled (AMS-1401). preventFocusOnPress makes pressing with the mouse not take the focus away from the field or selection that had it, for the buttons of a formatting bar; the keyboard does not change. Breadcrumbs with disabled turns off the whole path: the crumbs stop being links, leave the tab order and are read as unavailable.

0.31.0, released November 1, 2026

  • WeekAgenda: all-day, multi-day and past-midnight appointments, and picking a stretch by dragging (AMS-2333). An appointment with allDay shows in a fixed strip above the grid (not in the hour grid) and, with endDate, spans several columns; overlapping ones stack in rows, and in the phone list they read "All day". An appointment with times and endDate (for example an on-call shift from 22:00 to 06:00 the next day) is drawn split into one piece per day, locked, and repeating ones keep their length. onRangeSelect delivers the day and the start and end times of a stretch picked by dragging across the empty slots of a column (with mouse or pen; with a finger, dragging still scrolls the page) or with the keyboard: Shift and the up and down arrows stretch the slot cursor, Enter delivers and Escape lets go. A single click still goes to onSlotSelect. Everything is optional and, without the new props, the DOM is the same as before.
  • Charts: legend that hides series, pressable values, grouped bars and reference lines (AMS-2330). On LineChart, AreaChart and StackedBarChart: toggleSeries turns the legend into buttons that hide or restore each series (the axis and totals are recalculated; the screen-reader table keeps all of them), with hiddenSeries and onHiddenSeriesChange to keep the state outside; onPointPress makes every value in a category tooltip pressable, by keyboard and touch, and hands over series, category and value; referenceLines and referenceBands draw targets, averages and acceptable ranges (the axis includes them and the screen-reader summary states them); and StackedBarChart grouped puts the bars side by side from zero. Everything is optional and, without the new props, the drawing and the DOM are the same as before.
  • Menus and tabs: checkbox and single-choice options, and panels that keep their state (AMS-2332). MenuAction adds type (checkbox or radio, with role menuitemcheckbox or menuitemradio), checked and closeOnSelect (with false, choosing it closes neither the menu, the submenu nor the outer one); they work in ActionMenu, Menubar, ContextMenu and the SplitButton dropdown, and the state is kept by whoever uses them (for "Show columns" and "Sort by"). Tabs adds keepMounted: the panels of the tabs that are not selected stay mounted and hidden (hidden), so what was typed in a form is not lost when switching tabs; useTabs().panelProps(key, keepMounted) then returns the attributes of the hidden panel. By default nothing changes. Tests in menu-checkable.test.tsx and tabs-keep-mounted.test.tsx. Left out: NavigationMenu still has no mobile version.
  • Notices and command palette: promise-bound notice, fuzzy search, first result highlighted and server-side search (AMS-2331). useNotifications() adds promise(promise, { loading, success, error }), which shows "loading" and turns it into success or error (the error interrupts like any other) and returns the same promise, and update(id, changes) to change a notice that is still visible. CommandPalette adds searchMode (contains by default, fuzzy with skipped letters and ordering by similarity, none to skip filtering), onSearchChange (the typed text on every keystroke and '' on close) and loading (busy list and "Searching…"). Behaviour change: when typing, the first result is highlighted, so Enter runs it without pressing an arrow first; with an empty field nothing is highlighted. Tests in command-palette-search.test.tsx and notifications-promise.test.tsx.
  • ImageCropper and FileManager: zoom, rotation, cropped image, folder tree and details panel (AMS-2334). ImageCropper with onCrop adds "Crop", which draws the rectangle on a <canvas> and hands over the cropped image (a Blob, with outputType and quality; crossOrigin for an image from another origin, and if the canvas cannot be read it says so instead of failing silently); rotatable turns the image a quarter turn either way and crops on the turned image (it also reports the rotation); zoomable zooms in up to four times with a scrollable viewport and dragging still counts in pixels of the image. FileManager with tree draws the folder tree on the left (choosing one navigates to it; loadFolders loads it on demand) and with details the details panel on the right with name, type, size, date and thumbnail of what is chosen (renderPreview puts in your own viewer); in a narrow box they stack. Without these props, both are as before.
  • QueryBuilder and fields: value by type, export to SQL, MongoDB and text, pagination without a total, cents and password requirements (AMS-2329). In QueryBuilder, a field with options picks its value from a list with "is" and "is not" and a date field uses the date picker; exports shows the query written out, and queryToSql (parameterised, ? or $1, with column names that only come from fields and are rejected unless they are plain identifiers), queryToMongo and queryToText give it to use on the server, ignoring what matchesQuery ignores. Pagination with hasNext serves a cursor API that does not know how many pages there are: only "Page N" between the arrows. MoneyField with unit="cents" takes and gives whole cents (according to the decimals of the currency). PasswordField with rules shows the list of requirements with their state written out ("met" / "pending"); common brings the usual ones. Multi-selecting values in QueryBuilder is not included yet.
  • Upload and import: PUT to a signed URL, thumbnails, already uploaded files, fixing cells while importing and value lists (AMS-2328). httpUpload with PUT and without data or fieldName now sends the file itself as the body (it used to send a multipart form, which an S3 signed URL rejects); body sets multipart or raw by hand. Uploader gains previews (a thumbnail of each chosen image, released when it is removed) and existingFiles with onRemoveExisting to show the files that were already uploaded, with their download; FileList accepts preview and removable per row. In DataImportWizard, cells with an error are fixed in the review itself and the row is validated again, the rows with errors are downloaded as a CSV with their line and the reason, and the new list type with options translates the file text ("Alta", "activo") into the application value. Not included yet: chunked upload with progress in FileManager and MediaManager, dropping files and pagination in MediaManager.
  • SchemaForm: custom widgets, onChange with change state, own and asynchronous per-field validation, and reorderable lists (AMS-2327). widgets registers your own fields by name (ui={{ color: { widget: 'swatch' } }}); the widget receives its value, its error, the schema and the data of the whole form, with setValue to write to another field. onChange(values, { dirty }) reports every change (not on mount) and says whether there are unsaved changes, comparing against the starting values. validate adds an own rule, asynchronous too (checking on the server that a code is free), which is only called once the schema already accepts the field and which supports lines.*.sku for each item of a list; if it throws, the error shows on the field. In a list, reorderable adds "Move up" and "Move down" and duplicable adds "Duplicate" (respecting maxItems). Everything is optional: without these props the form is the same as before. Choosing the branch of a oneOf/anyOf without a discriminator is not included yet.

0.30.0, released October 30, 2026

  • Four new blocks: sign-up, invoice-list, team-members and pricing (AMS-2301). The account sign-up (name, email, a password typed twice with its strength, optional terms of use and the server error), the invoice list (what is pending and overdue on top, search, filter by status, sort, paginate, select several and export), the team (roles, removing access with confirmation and inviting) and the plan comparison (pay monthly or yearly, saving, recommended plan and current plan). They are copied with npx amset-ui add. With them there are eleven; the limit in the blocks test goes from ten to fourteen.
  • MultiSelect and PeoplePicker now mark the field as invalid with invalid alone (AMS-2301). They computed invalid but then looked only at error, so invalid without a message set neither data-invalid nor aria-invalid. invalid={false} now also wins over an error, as in Select and SearchField.
  • The outside style now reaches ImageCompare, ReadingProgress, Thumbnail, WeekAgenda, LineChart, AreaChart and StackedBarChart (AMS-2301). The types accepted it, but each one set its own style after the pass-through and wiped it. It is merged with the component's own; in WeekAgenda and the series charts the component's own variables win, because without them the drawing breaks.
  • A read-only Calendar announces itself as such (AMS-2301). The grid carries aria-readonly when readOnly is set; before, it only stopped allowing a choice and a screen reader could not tell.
  • src/components/props-contract.test.tsx: the props contract over the 200-odd stories (AMS-2301). It enumerates with the compiler every closed prop of every component (about 1,100 props and 3,100 values), paints them one by one and requires that none crashes the component; that those of the common vocabulary (size, tone, variant, invalid, loading...) change something that is painted or are justified with their reason; and that className joins the component's classes and style reaches the DOM. Checked that it bites: with the invalid fix reverted, it fails pointing at multi-select.invalid.

0.29.0, released October 29, 2026

  • Table: choose the columns on export and export in server batches (AMS-2274). On exportCsv and exportXlsx, columns sets which columns come out and in what order, and chooseColumns opens a panel with one checkbox per column before the file downloads. With loadRows the table asks the server for rows batch by batch (starting at 0, until hasMore is false), shows how many it has loaded with a cancel button, and downloads them all together; if the server fails or the export is cancelled, it says so and nothing downloads. The types ExportPage and LoadExportPage are exported.

0.28.0, released October 29, 2026

  • DockLayout: workspace with dockable panels (AMS-2259). Four areas with tabs; each panel moves to another area from its menu or by dragging its tab and is closed from the same menu; the sides and the bottom area resize with a separator that also moves with the arrows, and collapse. The layout is created with createDockLayout, changed with pure functions and can be controlled to save it.
  • MediaManager and ImageCropper: media library and image cropping (AMS-2259). The library searches, filters by type, uploads, chooses one or several, marks the images without alternative text and, for the chosen one, edits title, alternative text, caption and tags, crops and deletes with confirmation. The cropper moves and resizes the box with mouse or keyboard, with a free or fixed ratio, and hands out the crop in pixels of the original image.
  • FileManager: file explorer (AMS-2258). Breadcrumb path, accent-insensitive search, sortable list or grid with folders first, multiple selection, new folder and rename with the name checked, move with "Move to" or by dragging onto a folder, delete with confirmation and upload with the button or by dropping files on it. Each action is an application function; if it fails, it says so and the selection is kept.
  • DashboardBuilder: dashboard each person arranges (AMS-2258). Widgets in a 12-column grid that, in edit mode, move by dragging their handle or with the arrows, resize from the corner or with Shift and the arrows, get removed and added from the catalogue; when one moves, the others make room and everything moves up. Each move announces where the widget ends up and how big it is.
  • WorkflowBuilder and AgentWorkflowEditor: node and connection workflow editor (AMS-2258, AMS-2259). A canvas with panning and zoom, nodes that are dragged or moved with the arrows, validated connections (data types, occupied inputs, loops), the workflow's problems written out, "Arrange" by layers, the state of each node in a run and a panel to connect and remove connections without a mouse. AgentWorkflowEditor brings the steps of an AI agent (message, model, tool, condition, human approval and reply) translated.
  • Chunked, resumable uploads with retries and pause (AMS-2273). chunkedUpload uploads a file in parts (5 MiB by default), asks the server where it was to resume after a cut or a pause and retries each part with increasing wait; retrying adds automatic retries to any upload. useFileUploads gains pause, resume, pauseAll, resumeAll, the paused status, retries and the total progress (progress), and Uploader has pausable with the bar of all of them together and the pause and resume buttons.
  • NotificationCenter, useNotificationCenter and onShow in NotificationProvider (AMS-2275). The notification center with the history grouped by day or type, unread filter, mark as read or unread, delete one or all with confirmation and each notification's own actions; the hook that saves it in the browser; and onShow so ephemeral notifications stay in the history.
  • AddressField proposes addresses while typing (AMS-2276). With suggest (the search of your map provider or your server), the street asks for proposals from three letters and after 300 ms without typing, cancels the old request when you keep typing and, on choosing one, fills all the fields. If the search fails, it says so and you carry on by hand. Without suggest, the street is the usual one.
  • RecentSearchList and useRecentSearches: recent searches and scopes (AMS-2278). The latest searches saved in the browser and shared between components, a list to repeat, remove or delete them under an empty search box, and parseScopedQuery to search in a scope with #pedidos 1042.
  • ShortcutsProvider, useShortcut and KeyCombo: keyboard shortcuts for the whole application (AMS-2277). A single listener with a shortcut registry, scopes that win over global ones, conflict detection, help with Shift+? and shortcuts each person changes from the help by pressing the new combination; KeyCombo paints a shortcut as Mac or PC keys.
  • AgentApprovalDialog, AIUsageDashboard and Excel in Table (AMS-2279, AMS-2280, AMS-2274). The dialog to approve or reject an AI agent's action (tool, data, changes, risk, comment on rejecting, editable data, "always allow" and expiry); the AI usage dashboard (cost, input and output tokens, cost per day and model, breakdown by model and limits with warnings); and exportXlsx in Table, which downloads a real .xlsx with numbers and dates with their type, plus selectedOnly to export only the selected rows in CSV or Excel.
  • PromptTemplateEditor, DragSelectArea, UserRoleEditor, OrganizationTree and DataMappingEditor (AMS-2259). Template editor with {{name}} variables (insert, test values, preview and token estimate); selecting several items by dragging a rectangle, with Control+A and Escape; people and roles (change, search, remove with confirmation); org chart as boxes or as a list with tree keyboard; and field matching between a source and a target with transformations, warnings and applyMapping.
  • SmartFilterBar, SavedViewsManager and BulkEditPanel (AMS-2259). The filter bar of a listing (search, quick filters, advanced query with the QueryBuilder in a panel and clear all), with smartFilter to apply it on the client; saved views (choose, save new or over the active one, rename, default and delete with confirmation); and bulk editing of the selected records (keep, set a value or clear, with a summary before applying).
  • SLAIndicator, useAutosave and ApprovalFlow (AMS-2259). SLAIndicator says how much is left of a service deadline with the bar of what is spent and the phase written out (on time, due soon, overdue, paused, met, missed). useAutosave saves a value when it stops changing, one at a time and with no retry loops, and AutosaveIndicator tells where it stands. ApprovalFlow paints an approval chain with its steps, its approvers and what they decided, and collects approve or reject with a reason from whoever's turn it is.
  • DataImportWizard: import a CSV or an Excel file (AMS-2256). A three-step wizard (file, columns and review) that reads the file in the browser, without uploading it, and, matched with the application's fields, hands out the rows already converted and validated. It detects the separator, the encoding and Excel dates; it reads .xlsx with a reader of its own (a ZIP of XML) that is only loaded when a workbook is chosen. It validates type, required fields, duplicates (unique) and own rules (validate); rows with errors are skipped or prevent the import (allowPartial), and the result counts the imported, the skipped and the ones the server rejected. The validation is of format: the server validates again.
  • QueryBuilder: queries with nested groups (AMS-2255). "Field, operator, value" conditions joined by "and" or "or", with groups inside groups (up to five levels) and the option to negate a group. It hands out a data tree (QueryGroup) and filterByQuery / matchesQuery apply it to a list of records; a half-built condition filters nothing. The logic of each condition (operators per type, accent-free text comparison, numbers and dates) was moved out of the advanced filters of Table into a shared module, so both behave the same.
  • DiffViewer and AuditLogViewer: compare and audit (AMS-2254). DiffViewer compares two texts or two JSON documents, one after the other or side by side, line by line and word by word; equal lines far from a change fold into a button and each changed line carries a sign and a hidden text, so the change does not depend on colour. AuditLogViewer is the change history of something: who did what, on what and when, grouped by day, with search, loading in batches and the change of each field unfolding with its comparison. With onRevert each entry carries a revert button with confirmation; the library undoes nothing, the server has to check it again.
  • PermissionGate and PermissionMatrix: permissions in the interface (AMS-2253). PermissionProvider hands out what is granted (a list with wildcards such as invoices.*, or a function with your policy) and PermissionGate shows, hides or switches off a piece of interface accordingly; switching off is a real <fieldset disabled>, so the browser disables every control inside, and the reason is read. usePermission and usePermissions give the same answer in code. While the permissions load it decides nothing, and without a provider it grants nothing and says so in the console. PermissionMatrix is the matrix of a role: modules in rows, actions in columns, row, column and corner checkboxes with an intermediate state, locked cells that do not change even when ticking their row, a single tab stop with arrows and every change announced. Hiding something on screen does not protect it: the server still checks.

0.27.0, released October 28, 2026

  • Five components the house fronts had had to write themselves (AMS-736). AnimatedNumber, a number that travels from the previous value to the new one (the reader receives only the final one; with prefers-reduced-motion it changes at once). LiveClock, the live time of the requested zone, ticking on the dot by reading the system clock. PeoplePicker, choosing several people with avatar, presence and a line underneath; those online first and without offering whoever is already in (exclude). VideoPlayer, a video with the audio player's own controls plus sound, WebVTT subtitles, speed, full screen and keyboard (Space, arrows, M, C, F). And EmojiPicker, with search, categories, recents and skin tone: the catalogue (almost two thousand emoji with name and keywords in Spanish and English) does not travel with the component, it is fetched on opening (about 37 KB compressed) or beforehand with preloadEmoji(); EmojiPanel comes on its own for a panel of your own.
  • SideNav inside a Drawer keeps its labels on a narrow screen (AMS-2022). Below 40 rem the menu folded into icon tiles even inside a drawer, where there is room to spare, and the mobile menu was left without names. Inside a Drawer it no longer folds, and responsive={false} asks for it by hand anywhere else.
  • The SignIn block can open with an error (AMS-2217). New error prop: the reason the screen starts with (the session expired, the recovery link was already used), above the form, which goes away when sending again. The "Wrong credentials" story uses it and now shows the error in its capture.
  • amset-ui theme repairs the contrast a forced colour breaks (AMS-2154). A token written as is with colors (the assistant's exact red as error in dark) left its neighbouring derived token (error-soft) computed for the library value, and the text stayed at 3.48:1 with just a warning. Now the derived token of the same family moves until the pair reaches its minimum and the CLI says from where to where; what cannot be fixed without touching another family still warns. New repairContrast function in @amset/ui/styles/palette.
  • The audio player now enters the editor with a sound that exists. Its starting value pointed to an example file (ejemplo.mp3) that exists nowhere and returned 404, so the player came in broken. It now points to ejemplo.wav, a short chime with no third-party rights, hosted by the library website. A data: address does not work here: the editor discards any src starting with data: (except in images). scripts/audio-default.test.ts pins the address.
  • A controlled DateTimePicker no longer drops the date when it starts empty. With value={null} (or "", as a react-hook-form form starts), picking the day joined it with a time that did not exist yet, gave no value and, since the field was controlled, it was stored nowhere: the date was lost before the time was set and it never reported, so the form stayed empty. Now the half already picked is kept inside while the other is missing, and it reports as soon as both are there. If it is cleared from outside after being completed (a reset), it stays empty. src/components/date-time-picker.test.tsx pins it.

0.26.0, released October 27, 2026

  • The bar that appears over selected text no longer covers the formatting toolbar. With the first line selected it appeared above the selection, because it fitted inside the editor frame and the frame includes the formatting toolbar: on a phone it covered bold exactly and it could not be tapped. Now it only goes above if it fits inside the text area; otherwise, below. e2e/rich-text-mobile.spec.ts pins it.
  • The editor no longer gives up when typing fast. Typing more than 50 keys in a row at once (a text expander, dictation, a held key) made React throw "Maximum update depth exceeded": what was left to do for that key, including placing the cursor, was not done and the rest of the text came out at the start of the paragraph. It came from two effects that scheduled a setState on every document change (the cleanup of custom blocks and the closing of the spelling menu) even when there was nothing to change; now they only schedule it when there is something. Measured: 1,000 letters at once, no errors and all in place. e2e/rich-text-ux.spec.ts pins it.
  • @amset/ui/rich-text: components with content (card, alert, grid, stack…) now look like the real thing inside the editor. Until now they were a dashed frame with their name and the text inside: the title, tone, icon or border set in the panel did not show until the document was read, and it looked as if the component ignored them. Now the editor paints the real component, with its look, and its content is document text written inside it, with paragraphs, lists, the "/" menu, deleting and undoing like anywhere else. Changing a property shows at once. A click on its look (not on the text) selects it whole to move it, delete it or open its properties, and a double click or the right button open the panel. In read-only mode the content cannot be written. A grid or a stack spread what is written into columns while editing just as when reading, and if the component has just finished downloading while someone was already typing inside it, the cursor stays where it was. What is written in the content is still a document, as before: the only thing that changes is how it looks while editing. e2e/rich-text-components.spec.ts and src/rich-text/view/framed-component.test.ts pin it.
  • The properties panel of a component no longer has English-only fields or fields that only make sense when programming. Measured over the 157 components: 81 properties and 146 values of closed lists showed up named in English ("Close label", "Raised", "Min height", "Secondary"), and so did the Intl formatting options, functions and form field names. Now everything shown has its text in Spanish and in English, and the technical ones are hidden. A test (src/rich-text/amset/prop-labels.test.ts) walks every schema and fails if a new property or value comes out unnamed.
  • SchemaForm: an optional group nobody has filled in no longer asks for its required fields. An optional object with required fields (the minimum date of a date picker: year, month and day) was treated as an object missing three fields, so the form could not be submitted without making up a date. Now an optional group with all its fields blank (or made only of empty groups and lists without rows, like the advanced filter of a table) is "no value" and does not show up in the result; a half-filled one still asks for what is missing and an empty required one still says its fields are missing. That is why "Apply" now works in the properties panel of DatePicker, Calendar, DateRangePicker, TimeField, Rating, Steps, Timeline, QuickRange and Table.
  • A required function the document does not bring no longer makes the component fail. ColorPanel calls its onChange when it mounts and in a document nobody listens: it threw t is not a function. A required function with no chosen action is now one that does nothing, and the panel does not require it either (when painting it already did not count as missing). VisuallyHidden leaves the component catalog (157): it cannot be seen, so in a document it could not even be chosen.
  • The editor's "/" menu (and the mentions and spelling menus) no longer ends up off screen. With the cursor near the edge of the window, or outside it (the page scrolled while the list was open), the list was placed below the edge: the active option moved down with the arrow keys, but inside a part of the list you could not see, and at the end nothing was visible. Now the whole list always fits in the window: it sticks to the nearest edge and, if the window is shorter than the list, it shrinks. Measured on the other lists with virtual focus (select, combo box, multi-select, search field, phone prefix, tree select, cascader, command palette): they follow the active option correctly. e2e/rich-text-ux.spec.ts pins it, with a short window and with the cursor outside.
  • CookieBanner no longer steals focus from someone who is typing. When it appeared it moved focus into the banner even if focus was in a field: in a shop that renders it after hydration, someone already typing in the search box lost the next keystrokes (typed "zzz" and got "z"). Now, if focus is in a text field, a textarea, a select or editable content, it stays there and the banner is announced with aria-live; in any other case it takes focus as before. isTyping() in utils/focus is what decides it.

0.25.0, released October 25, 2026

  • New modes: corner radius, elevation and spacious density. data-radius (none, sm, md or lg) moves the four radii at once, including the pills of a switch, a progress bar or a chip, which had 999px written by hand and no theme could reach: they now come from a new token, --amset-radius-full. data-elevation="flat" removes the resting shadow from cards, fields and buttons and keeps the one on what floats. data-density="spacious" raises controls to 36, 44 and 52 px for screens used with a finger, and the table, the data list and the history open their gaps with it. All three go on the root or on a container, AmsetProvider takes them as radius, elevation and density, and the Storybook toolbar has the two new pickers. What is round because of what it means (the dot of a radio button, an avatar, a spinner) does not change, and a shape="pill" asked for in the prop stays a pill in every mode.
  • The 693 component variables, tested one by one in a browser. Each one was set the way a project would set it and checked for any change: all of them do something. 674 showed up on their own across the 1,052 stories; the rest only act in a given state (dragging, pressing, closing a panel, on a narrow screen) and were checked by building that state. A new test fails if a sheet declares a variable nobody reads.
  • Tabs and AppliedFilters aligned with the rest. Tabs measured 30, 38 and 46 px and did not follow density; they now measure what a control of their size does (30, 36 and 44 px, and whatever the density asks for), with --amset-tabs-height, and --amset-tabs-padding-block goes to 0. The applied filter chips came out at 23.2, 26.8 and 32.4 px and now measure 24, 28 and 36, like ToggleChip, with --amset-filters-height. Measured across the 119 components with a size, in their three sizes and three densities: the other controls already matched. e2e/modes.spec.ts pins it.

0.24.0, released October 25, 2026

  • Mobile and keyboard pass over every story. A joined ButtonGroup that does not fit in one row now stacks at full width (before it wrapped to a misaligned second row); the component decides by measuring, not with a fixed width. The DonutChart legend no longer lets the name run into the value, Pagination hides the middle page numbers below 30 rem and the RichTextEditor toolbar becomes a single scrolling row. In the editor, Escape on a toolbar menu closes it and leaves focus on its button instead of jumping to the text. The Affix and TableOfContents demo pages no longer overflow on a phone. Measured with a keyboard probe over the 208 stories; the remaining warnings came from the probe itself (native radios, accordions that do not close with Escape).
  • WeekAgenda: the slot for creating an appointment can also be chosen with the keyboard. amset-ui a11y lint found it when run over the library itself: with onSlotSelect the slot could only be chosen with the mouse (WCAG 2.1.1). The grid is now a tab stop with a slot box that moves with the arrow keys (Page Up and Page Down an hour at a time, Home and End to the first and the last slot of the day), announces the day and the time and is chosen with Enter or Space, just like a click. The box is only drawn with keyboard focus, not when you arrive with a click, and in a right-to-left language the sideways arrows are reversed. Without onSlotSelect the grid still stays out of the tab order. Browser test in e2e/week-agenda-keyboard.spec.ts.
  • Menubar and ButtonGroup no longer push the page sideways at 320 px wide (WCAG 1.4.10). Measured by amset-ui a11y audit: the menu bar reached 338 px and the button group 323 px out of 320, and the cards in the AppShell example 325 px because the grid asked for a 14 rem minimum. The bar entries and the group buttons wrap onto the next line when they do not fit (they do not scroll inside their box: a button's focus ring sticks out 4 px and would be clipped), and the example grid shrinks. e2e/reflow-320.spec.ts pins it.
  • amset-ui a11y audit does not count as unreachable what disappears while tabbing. A button that only exists after scrolling down (like BackToTop) unmounts when focus goes up to the first link, and it was counted as a control the tab key never reached. The page is now looked at again when the walk ends and whatever is no longer there does not count. The clean page of e2e/a11y-cli.spec.ts carries a button that removes itself when something else is focused, so it does not come back.
  • amset-ui a11y: run against the 208 sheets of its own Storybook, it no longer gives false failures. The first pass gave 112 findings of its own and almost all were the tool's mistakes, not the library's. a11y audit no longer counts as unreachable the controls of a composite widget (toolbar, tablist, listbox...), which has one tab stop and is walked with the arrow keys; it does not tab controls with tabindex="-1"; it measures a hidden <input> by the label that is shown; it compares focus by element and not by selector (two table rows gave the same one); it does not close the lap at the first exit from the document, because a top layer (a banner) crosses it before the end; it measures the size of a target with its whole box even if its list's scroll clips it; and it releases the previous focus (an autofocus) before measuring, or a field that already had its ring on looked as if it had none. a11y lint recognises focus drawn with [data-focus-visible] and treats infinite motion as switched off if any sheet respects prefers-reduced-motion. Result: 202 of 208 sheets with no findings of their own; the rest are candidates to review, not confirmed failures.

0.23.0, released October 24, 2026

  • amset-ui a11y: the accessibility of your project, outside the library. Three new commands that share one table of rules, each with its WCAG criterion. a11y audit <url> opens the pages in a browser with the project's Playwright and measures what a static scanner cannot see because it needs key presses or changing the page: visible focus when tabbing, keyboard traps, controls the keyboard never reaches, focus covered by a fixed header (2.4.11), targets smaller than 24 px (2.5.8), the page at 320 px wide (1.4.10), text spacing (1.4.12) and reduced motion. If the project has axe-core, it also runs its WCAG 2.2 rules without counting them twice; if it does not, the report says so and does not count it as passed. It only reads: it tabs and measures, it never clicks. It writes a11y-report/index.html and report.json, and exits with code 1 if there are errors. a11y lint reads the code with the project's TypeScript and warns about controls built from a div, images without alt, IconButton without a name, fields without a label, a removed focus outline, colours without contrast and hand-made components where the library already has one; a single finding is silenced with // amset-a11y-ignore image-without-alt. a11y statement drafts the accessibility statement (Law 11/2023, Annex V of the European Directive) from what was measured and the sheets of the components the project uses: without an audit the status is "not evaluated", with a clean audit it is "no failures detected" and never "compliant", whatever nobody has checked is listed as pending, and what it cannot know (company, contact, complaints route) stays as a marked gap to fill in. It is a draft, not legal advice. The browser measures are tested against one clean page and one with every failure (e2e/a11y-cli.spec.ts), and npm run consumers runs the three commands against each freshly created and built starter.

0.22.0, released October 24, 2026

  • @amset/ui/rich-text: a full pass over the editor (AMS-2072). An Insert button in the toolbar with everything that can go in, by section and with a search box (components used to appear only by typing "/"). A freshly inserted component comes with its properties open, and they open again with a double click, with the right button or from the toolbar; the properties sheet is in Spanish, ordered and without the technical props (the select went from 86 English fields to its own). The right button gives a menu for what is underneath: component (properties, duplicate, move up, move down, delete), image, link, table, plus cut, copy, paste and formatting. The 158 components come in with real content taken from their stories (about 80 used to come in with "Etiqueta" and empty lists). Full screen is the browser one or the top layer: with an ancestor with transform (the Storybook docs) it stayed as the toolbar with no text. The "/" menu lives in the top layer and opens upwards when there is no room below. Pasting, dropping or choosing an image from this device works without onImageUpload (it is embedded as base64; also in Firefox, which leaves it in items). The link panel asks for the text, the address and a tooltip (title, which is stored and exported), and warns about texts like "here".
  • CommandPalette: with the arrow keys, the active option stays in view. The palette keeps focus in the search field (the active row is announced through aria-activedescendant), so the browser scrolled nothing and, as soon as the list no longer fit, you went down blind past the edge and back up without the list following. The active row is now brought into view when the keyboard moves it (not with the mouse, so the list does not jerk under the pointer) and the first one scrolls up to its group title. Selects already did this correctly; e2e/keyboard-scroll.spec.ts pins both and measured the palette before and after the fix (22 rows out of view going down, 0 now).

0.21.0, released October 23, 2026

  • The recipes for third-party editors are removed. With the own editor of @amset/ui/rich-text they are no longer needed: the stories with recipes for other editors in RichTextToolbar, their recipes and demos, the e2e test that exercised them and the eleven development dependencies of those editors are gone (package-lock.json shrinks by about 900 lines). RichTextToolbar is still public API and can be used with any engine: the docs/rich-text-editor.md guide now focuses on the own editor and on the toolbar contract, and keeps the screen reader checklist, which is now run on RichTextEditor.
  • llms.txt and AGENTS.md tell an AI assistant what it needs to know: the code can be copied, the MCP server and Figma. npx amset-ui add already copied the source of the components, @amset/mcp already gave a coding assistant eight tools and the tokens already came out as Figma Variables, but those files, which are what an assistant reads, said nothing about it and the library looked install-only and without integrations. They now have the “Install or copy the code” and “MCP server and Figma” sections, and llms.txt links to the guides. The rich text editor (RichTextEditor, RichTextView) and the HTML viewer (HtmlViewer) belong to the @amset/ui/rich-text subpath and cannot be copied with add: they are installed, and a test checks it.
  • llms.txt now has links. Google flagged that the file had none. It now has a “Documentation” section with Markdown links to llms-full.txt, the component catalogue, “Getting started”, the agents page and the editor page, and a test checks for an H1 and at least three links.
  • @amset/ui/rich-text: own spell checker, comments and suggestions, version history, columns and formulas (AMS-2055). spellchecker (with createWordSpellchecker and createLanguageToolSpellchecker): mistakes are underlined with a wave, the right button or the menu key gives suggestions, ignore and add to dictionary, and the word being typed is not marked; the editor ships no dictionary, you plug one in. review adds comments (threads you store outside the document, with reply, resolve and delete) and the “Suggest changes” mode (what you type is underlined, what you delete is struck through, and each can be accepted or rejected one by one or all at once); RichTextView gains suggestions="show | accepted | rejected", and Word receives suggestions as revisions and comments as comments, and reads them back. versions adds the history: list, comparison with the current document (additions underlined, removals struck through) and restore with undo; diffDocuments(before, after) gives it outside the editor. New built-in blocks: columns (2 to 4, text flows from one to the next, a single one on a narrow screen) and formulas in TeX drawn as MathML with an own dependency-free converter (stored as $$ ... $$ in Markdown). Also: the search and spelling highlights no longer overwrite each other between two editors on the same page. No API changes to what already existed.

0.20.0, released October 22, 2026

  • Markdown: two spaces at the end of a line are a line break again, and nothing is lost by nesting too deep. fromMarkdown trimmed every line of the paragraph and the CommonMark break disappeared. A link between <> with spaces is now read (spaces become %20), and a quote nested beyond 20 levels stays as text instead of being emptied when saved.
  • @amset/ui/rich-text: a safe HTML viewer and the AMSET UI components inside a document (AMS-2028). HtmlViewer draws foreign HTML: in document mode it writes it again from the tree with an allowlist (tags, attributes, URL schemes and CSS properties), and in page mode it isolates it in an <iframe sandbox> without allow-same-origin and with a CSP that allows no network, forms or foreign scripts. amsetBlocks({ actions, data }) registers 159 library components as blocks: the catalog comes from the TypeScript compiler, each one is downloaded the first time it is used, the props are checked against their schema before drawing, and actions and data are identifiers supplied by the application (a function never travels). Card, Alert, Grid, Stack and the like hold document blocks inside (component), with a frame and name in the editor, a properties form and "Remove and keep content". toTsx(doc) turns it into a React component, and listComponents, describeComponent and createComponent let an agent build validated screens. Also, in Firefox the first click in an unfocused editor put the cursor at the start of the document instead of where it was clicked, and npm run e2e:engines did not enable Firefox or WebKit (the variable name did not match the one in playwright.config.ts). No API changes to what already existed.
  • @amset/ui/rich-text: Markdown and Word, faithful pasting, new blocks, accessibility warnings and an AI circuit (AMS-2011). toMarkdown/fromMarkdown (CommonMark and GFM; what Markdown cannot say is written as HTML and read back) and toDocx/fromDocx (a .docx with headings, formatting, multi-level lists, tables with merged cells, links and images); pasting from Word, Google Docs, Excel and LibreOffice is tested with a corpus. Merged cells, @ mentions, callouts (> [!NOTE]), collapsible sections and custom blocks (blocks: your component inside the text, with its render and edit). A floating bar over selected text, a handle to drag blocks and Alt+Shift+arrows. Accessibility warnings while typing (checkAccessibility). An AI circuit with no provider: aiCommands, createRichTextAgent (read, search, propose changes that a person accepts or rejects, write live, tools for a model).
  • @amset/ui/rich-text: an own rich text editor, with no dependencies (AMS-2011). RichTextEditor is a complete field (label, hint, error, counter, read-only) with a toolbar -formatting, headings, lists and tasks, quotes, code, alignment, colour and highlight, scripts, links, images with their properties, tables, horizontal rule, find and replace, HTML source and full screen-, Markdown shortcuts as you type, pasting from Word and Google Docs without their junk, drag and drop, and grouped undo. The value is the document as JSON (format="json", the exact thing) or sanitised HTML (format="html"). Inside there is an immutable model with invertible steps, a schema that discards everything it does not know (the exported HTML is generated from the model: no <script>, no handlers, no javascript:) and a view over contenteditable that cancels beforeinput, and when the browser does not allow it (an IME composition, an Android keyboard) reads the DOM and applies the difference. RichTextView renders what was saved on the server, without dangerouslySetInnerHTML. RichTextToolbar accepts children for own groups. It lives in its own subpath and the index does not import it: it weighs nothing for whoever does not use it.

0.19.0, released October 22, 2026

  • Form: the click on Save is no longer lost when fixing the last error (AMS-2036). A field that validates when you leave it (the number commits its value on blur) fixed its error right as Save was pressed: the summary and the field message vanished, the form moved up to 111 px between pointerdown and pointerup and the click landed outside the button, so it had to be pressed twice. Now what was fixed stays crossed out in the summary until the next submit (with "All fixed: you can submit now" and the success tone when nothing is left), and while you press inside the form no error changes how it looks: it catches up as soon as the click arrives. Also, submitting with errors now moves the focus to the number itself and not to its minus button. New test e2e/form-summary.spec.ts in Chromium, Firefox and WebKit, with axe in light and dark.
  • The chat message separates what it holds. A reasoning block, a tool call and the answer inside the same ChatMessage bubble were stuck together with no air at all (measured: 0 px between the tool and the text). Now every block that is not the last one leaves --amset-chat-message-block-gap below it (--amset-space-2 by default), also with an inline margin: 0 paragraph. New story "With reasoning and tool".
  • Dialog, Drawer and the command palette no longer close on a click that started inside (AMS-2027). Clicking outside was detected from the click target, and the browser sends it to the common ancestor of where the press and the release happened. If the content changes size in between (an error message that disappears when leaving the field) the button moves, the release lands on the backdrop and the dialog closed without saving: measured in Invoicing when logging hours after a first error. Now it only closes if the gesture also started on the backdrop. The same applies when dragging to select the text of a field and releasing outside.
  • npm run figma does it all, and on every push to pre it is generated on its own. It builds Storybook if missing, serves it locally and measures the stories with several pages at once, and leaves figma-export/ with the components, the tokens and the plugin. A new CI job (figma, which does not block the web) runs it on every push to pre, and ui.amset.tech publishes it at /figma/, like everything else that is generated with each version.
  • A management app now lays out properly without its own CSS (AMS-1972). Five gaps measured in a real management app with getComputedStyle, which the app used to cover with rules of its own: a Table inside a Card with withoutPadding no longer draws a second frame and aligns its outer cells and the pagination with the card title; numeric cells do not split an amount over two lines; PageHeader centres media with the title and gains mediaTile for the section icon in its tile; a SideNav inside a Drawer fills the whole drawer, without the side line or the double scrollbar; AppShell removes the 8 px body margin, a SiteFooter with only the legal line does not repeat the rule, and a pressable Card without content no longer cuts off the subtitle.
  • A text field no longer brings the CSS for color, date, time, select or phone (AMS-2014). The per-component CSS generator imported any sheet that had a rule starting with a class the module mentioned, and almost every field sheet has a few .amset-field rules (how a field that contains a color or a date is drawn). TextField and TextArea therefore dragged in 64 KB of uncompressed CSS when they used 29. Now a sheet is imported only for the classes that belong to it: the ones that dominate in it, or the sheet where the class has the most rules. TextField drops to 29 KB, Select from 71 to 40, and the imports across the whole package from 710 to 489. A test pins the components that must not lose their own sheet.
  • MultiSelect and TagsInput: the selection lives inside the box (AMS-1971). The chips used to sit in a separate block above the field, and every new value pushed the whole form down. Now they flow inside the box, before the text entry: with one line it is as tall as any other field and only grows when they do not fit. The label always stays in place. Pressing the empty part of the box focuses the field, and limitTags keeps the box on one line however many are chosen: at rest it shows that many chips and a “+N”, and entering the field shows them all.
  • The Figma export now also captures the pressed and disabled states, and the Storybook cover page explains it. Each component's "Interactive" story measures pressed (a real pointer held down on the first interactive element) and disabled (the attribute is set on the live element, drawing the library's :disabled rules). The cover page gains an "In Figma" section, in Spanish and in English.
  • The Figma export now also brings hover and focus. Every component with an "Interactive" story adds the Interactivo · hover and Interactivo · foco variants, measured with a real pointer and a real Tab; the focus ring is drawn as an outside stroke. They only count if they change the resting appearance.
  • doctor no longer errors on data-theme={variable} (AMS-1962). The value of an expression cannot be read without running the code, and the variable itself was taken as if it were the theme, so data-theme={tema} came out as an error. Now it is a warning (data-theme-dynamic) asking you to check that it only returns "light" or "dark". Literals ({'oscuro'}) are still an error, and what a comment (/* */, //, <!-- -->) says no longer counts as code.
  • The Notifications documentation page no longer repeats the demo four times. With inline stories, each one mounted its own fixed-position queue and the four stacked in the same corner; now each story runs in its own frame, as in AmsetProvider. Also, the errors of useNotifications and useConfirmation outside their provider named useNotificaciones and ProveedorNotificaciones, which no longer exist: they now say the real name, in English.
  • @amset/ui/icons.json: the geometry of each icon as data (AMS-1373). To draw the icons without importing the components (the icons page on amset.tech would carry 900 KB of JS if it imported them). It comes from calling each icon and walking the element it returns, not from reading the generated code, so it does not break when the build format changes. Each entry says whether it is one of ours, whether it brings its own <svg> (the filled star), what happens to its wrapper (the own strokeWidth of 35 icons) and its elements with their attributes.

0.18.0, released October 20, 2026

  • VatField: the EU VAT number, validated country by country. DocumentField only understands NIF, NIE and CIF, and a shop selling to EU businesses could not validate a German or French VAT number without a regular expression that lets a changed digit through, which is the real error. The field covers the 27 EU member states and Northern Ireland (XI): each with its shape and, in the 22 where the algorithm is public, its check digit (AT, BE, CZ, DE, DK, EE, EL, ES, FI, FR, HR, HU, IE, IT, LU, NL, PL, PT, RO, SE, SI, SK); Bulgaria, Cyprus, Lithuania, Latvia, Malta and Northern Ireland are validated by shape only and the field says so. It is typed with spaces, dots or hyphens and comes out of onChange clean, in capitals and with EL for Greece (GR is accepted). The hint says which country it is and whether the check digit was verified; countries limits the accepted states. It does not query VIES: it says whether the number can exist, not whether it is registered (the query needs a server). New utilities validateVat, vatProblem, vatIsChecked, vatCountry, clearVat and VAT_COUNTRIES, and vatSchema for schemas. 88 tests, with real numbers from each country published as valid and the same number with one digit changed, which must fail in every country with an algorithm.
  • Fieldset: a group of fields with its label, a real <fieldset>. The library had no way to group controls that go together so a screen reader announces the group before each one. legend is the <legend>; disabled turns off the fields inside at once; columns lays them out in 2, 3 or 4 columns (one below 40 rem); hint puts help under the label and hiddenLegend hides it from view. It removes the browser's min-width: min-content, which made a narrow page overflow. It can be used from a Server Component.

0.17.2, released October 20, 2026

  • Table: hierarchical rows, cell range and group totals. tree (getChildren, expanded, onExpandedChange) turns the grid into a treegrid with each row's level, position and state and tree keyboard handling; cellSelection selects a range with the mouse or with Mayús and arrows, copies it as TSV and HTML and pastes it; groupTotals adds a totals row per group. Everything is announced in a live region. Types TableTree, CellRange and CellRef.
  • SchemaForm (@amset/ui/form): a whole form from a JSON Schema. Fields by type and format, objects as fieldset, lists you can add to and remove from, steps with Wizard and conditions (if/then/else, dependentRequired, dependentSchemas, oneOf by const). Its own validator, with no dependencies, which says what it does not understand instead of ignoring it; fields switched off by a condition are not sent.
  • WeekAgenda: repeating appointments, time zones and one column per resource. repeat with the iCalendar rules (freq, interval, weekdays, until, count, except), timeZone per appointment and per agenda with each date's daylight saving (what crosses midnight comes out split and locked) and resources to split the day view by room or person. onEventChange also delivers series, occurrence, timeZone and resource. occurrencesBetween, occurrenceKey and parseOccurrenceKey come out.
  • Accessibility conformance sheet per component (ACCESSIBILITY-CONFORMANCE.md, npm run a11y:sheet). Generated from the component pages: what each component documents, whether it passes axe in the browser, whether it has a screen reader test script and the result for each reader. The scripts and manual results live in docs/accessibility/protocol.json; whatever nobody has run shows as "pending", not as "passes". A test requires the accessibility section on every page.
  • npx amset-ui generate: a whole app from its JSON blueprint. It validates the whole blueprint (bin/blueprint.mjs) before writing, starts from the create starter, adds one page per entry (empty, or with dashboard, customer-list, settings or sign-in), rebuilds the menu, copies the blocks and runs doctor with a single repair attempt; --check adds the compiler. Screens come out with empty data and the service unconnected, failing loudly. The amset-ui skill explains the blueprint to an assistant.
  • Coverage of the reference screenshots (npm run visual:coverage). It says which review and system stories lack their four screenshots, which have only some and which screenshots are left over; a test fails on orphans and partial sets. The missing ones are generated by the e2e:capturas job. Weight limits for the table and the agenda raised (44.6 and 20.8 KB) because of the above.
  • What amset.tech used to copy by hand now ships in the package (AMS-1922). The English CHANGELOG ships as @amset/ui/changelog.en.json (the normalised Spanish text as the key, like props.en.json) and a test fails if a new entry does not bring it; SECURITY.md, benchmark/RESULTADOS.md, docs/rich-text-editor.md and docs/adr/ are now in the package; api.json gives the types in full (it used to cut with an ellipsis the ones of messages of AmsetProvider and trigger of Tooltip); EditableColor and ColorFormat are exported, and ToneIcon counts as a component and not among the icons.
  • The field label knobs now also reach the rating, the file upload and the radio group (AMS-737). components.field['label-weight'] and the rest of label-* were written only on .amset-field, so those three pieces, which reuse the field label, kept another typography. amset-ui theme now emits them on them too (not the size: each one adjusts it with its size).
  • The theme command tests run again: they had been silently skipped since the switch to English because they looked for dist/styles/paleta.js.
  • amset-ui theme --format hex: the theme with colours in hexadecimal. By default the CSS still comes out in oklch(...), which is how the theme is derived; with --format hex it is converted to #rrggbb (#rrggbbaa with transparency). A format that does not exist gives a clear error.
  • ImageViewer: "fit to window" now fits (AMS-1947). The viewer stage was a grid with an auto row, which grew to the natural height of the image, and the image's max-height: 100% resolved against nothing: a 1440 × 900 screenshot in a 1280 × 900 window came out 41 px taller than its stage, and a 600 × 1600 portrait on a phone, 225 px. Measured in a real browser. Now the grid has a defined row and column (minmax(0, 1fr)) and at actual size it grows with the image again, so scrolling reaches the end. New test e2e/image-viewer.spec.ts: landscape, square and portrait at three window sizes, on all four sides of the stage.
  • Components export to Figma (npm run export:figma-components). Every story under Componentes/ is rendered in a real browser and what it measures is dumped (boxes, padding, borders, radii, shadows, text, SVG); the plugin in figma/ draws one component set per component, with one variant per story, and binds to the AMSET · Color variables the colours that match exactly one. Boxes are fixed (no auto layout) and states only appear if a story shows them.
  • knobs.json brings back the knobs of the code field and the page heading (AMS-1903). When their classes were renamed (AMS-814) the catalog stopped seeing them, and with them the theme tester, the MCP and the variables table of their pages. The groups are still code and heading, and the selector is the new one. A new test (scripts/knobs-groups.test.ts) fails if they are lost.
  • The data table of the series charts is now styled (AMS-1902). With tableVisible, LineChart, AreaChart and StackedBarChart showed it without a single rule: figures stuck together and no borders. It now carries the same ones as the BarChart one, with the headers aligned to their figures.
  • The first two series of a chart no longer come out the same color with a blue brand (AMS-1909). The default tone order was accent, information, success...; with a blue brand, accent and information were two blues. It is now accent, warning, success, error, information, neutral, in LineChart, AreaChart, StackedBarChart and RadarChart. Whoever did not pass tone will see the second series in amber instead of blue.
  • The blocks show fully in English in Storybook, and the manifest carries their English (AMS-1913, AMS-1922). The texts a block builds with a value inside (the customer count, the invoice title, the question before issuing it, the invoices and the history of a customer) have their translation, and the demos test now sees the fixed pieces of those templates, so the next one does not slip through. The length message of the new customer form puts the number at the end. dist/sources/manifest.json publishes the en of each block.

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 any editing engine, 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 complete recipes, 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.
  • 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; 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 the previous library 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/: the reference screen, measured in a real browser (AMS-1784). One screen (form with validation, table with sorting and pagination, confirmation dialog) rendered on the server and hydrated in Chromium: it measures what the page downloads, the JS that is parsed and the hydration, and it goes through the same walk by roles and axe with no 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.
  • 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 of the most used pieces (P1.6). npm run benchmark bundles each one with Vite (Button; Button, Select and DatePicker; Table) and counts JS and CSS in gzip, with pinned versions 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 a library that 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 an 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. 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. 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 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 are the expandable row detail and the summary footer of 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. 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.