v0.157.0
Minor Changes
-
#1296
4fc58aecThanks @tenphi! - Keep the caret in place inTextInput,TextArea,PasswordInputandSearchInputwhen the
controlled value comes back late.A controlled
<input>renders whatever string its parent hands it. If the parent hands back the
pre-keystroke string — because its state arrives through a store that publishes a render late, a
debounce, or a deferred update — React writes that stale string into the DOM node, and a native
valueassignment collapses the selection. Typing in the middle of a field threw the caret to the
end. The text still landed a render later, so it read as a caret bug rather than a data-flow one.These four fields now hold the typed text locally until the parent catches up.
onChangestill
fires once per keystroke with the full value, so nothing downstream changes: no debouncing, no
commit-on-blur, no coalesced calls. An incomingvalueis adopted whenever it is a genuine change
from the parent — an undo, a reset, a transformed string, another record — and on blur the parent's
value takes over again.Components that already own their typed text are untouched:
NumberInput,ComboBox,
SearchComboBox,FilterListBox,CommandMenu,CommandTextArea,ColorPickerand
InlineInput.Two additions to the public API:
useBufferedValue(value, onChange, options)— the hook behind it, exported for controls that
own their own input. It is generic, andoptions.getKeylets non-string values (an array of
colour stops rebuilt on every emit) be matched by signature rather than identity.isBufferedon the four fields — set it tofalsefor a caller that must see the field snap
back to its ownvaluethe instant it declines a keystroke.
// A parent whose state arrives a render late no longer fights the caret. <TextInput value={spec.title} onChange={(title) => updateSpec({ title })} />
-
#1293
6696d7feThanks @tenphi! - Breaking: renameColorPickertoColorInput, and give theColorPickername to a new trigger-only component.The component released in 0.156.0 as
ColorPickeris a text field — a swatch, an editable color string, and a popover trigger. That is an input, and it is now calledColorInput. Its API is unchanged: rename the import and theCubeColorPickerPropstype (nowCubeColorInputProps), and everything else works as before.-import { ColorPicker } from '@cube-dev/ui-kit'; -<ColorPicker label="Brand" format="oklch" formatMode="derive" /> +import { ColorInput } from '@cube-dev/ui-kit'; +<ColorInput label="Brand" format="oklch" formatMode="derive" />
Note that this is a silent break rather than a type error:
<ColorPicker value onChange label />still compiles, but now renders a button instead of a text field.ColorPickeris now a swatch button that opens the same popover, matching whatPickermeans elsewhere in the kit — a trigger plus an overlay, with no text entry. It shows the color as a swatch and spells it out beside it;childrenreplaces that label, andchildren={null}leaves the swatch on its own for toolbars and dense tables. It has noformatMode, since there is no text to reconcile.<ColorPicker label="Series color" value={color} onChange={setColor} /> <ColorPicker aria-label="Series color" value={color} onChange={setColor}>{null}</ColorPicker>
The color model, the three editing spaces and the popover are shared by both, so
format,defaultSpace, and every notation they read behave identically.COLOR_FORMATS,COLOR_SPACES,ColorPickerFormatandColorPickerSpacekeep their names. -
#1293
6696d7feThanks @tenphi! - AddColorSwatchGroup— a grid of color swatches, one of which can be selected. It is the palette half of choosing a color, whereColorPickeris the freeform half.Colors go in as data rather than as children, so the API reads like
Pickerrather thanRadioGroup, though a radio group is what it is underneath: one tab stop, arrow keys between swatches.<ColorSwatchGroup label="Brand color" colors={["#7a4dbf", "#26fcb2", { color: "#ff0000", label: "Danger" }]} columns={4} value={color} onChange={setColor} />
Swatches are keyed by their canonical hex, so the same color written two ways collapses into one entry — equivalent colors would otherwise make selection ambiguous. That same matching decides which swatch a value selects, so
valueneed not use the notation the swatch was written in.allowCustomappends aColorPickerfor colors outside the set, showing the current color whenever it is not one of the swatches.ColorSwatchis exported too, for showing a color without a control around it. It takes direct style props, so<ColorSwatch color="#7a4dbf" radius="round" />works.ColorSwatchGroupandColorPickerboth accept the outer and block style props, soradius,border,paddingandshadowapply directly.The selected swatch is marked the way React Aria marks it: two rings drawn inside the swatch, one in
#surface-textand one in#surface. A single ring in one color vanishes against a swatch of that color — an accent ring on an accent-colored swatch — while two tones that flip with the color scheme contrast against anything, and drawing them inset keeps the swatch's footprint fixed.ColorPickergainsswatchesandswatchColumns, which put a palette under the editor in its popover. The group dropsallowCustomthere — the escape hatch is itself aColorPicker, so offering it inside one would nest popovers without end. That is enforced through context rather than documented, so the recursion cannot be written by hand either. -
#1289
c7c85799Thanks @tenphi! - RemoveFileTabs. UseTabsinstead.FileTabswas an editor-style tab bar with close buttons and dirty-state dots.
ItsFileTabPropshas carried@deprecated consider using <Tabs /> instead
since before the Glaze migration. Cube Cloud — the component's only known
consumer — movedFilesEditorontoTabsin October 2025 and has had no
reference to it since.Removed from the public API: the
FileTabscomponent (with its
FileTabs.TabPanesubcomponent) and theCubeFileTabPropstype.-<FileTabs defaultActiveKey="1" onTabClose={(key) => removeTab(key)}> - <FileTabs.TabPane id="1" title="index.ts" /> - <FileTabs.TabPane id="2" title="styles.css" /> -</FileTabs> +<Tabs defaultActiveKey="1" onDelete={(key) => removeTab(key)}> + <Tabs.Panel key="1" title="index.ts" /> + <Tabs.Panel key="2" title="styles.css" /> +</Tabs>
The close button is the main behaviour to port: on
Tabsit isonDelete, and
passing it is what makes the buttons appear (onTabCloseonFileTabs).
Tabshas no built-in equivalent ofisDirty— render the unsaved indicator
into the tab'stitleoractions, which is what Cube Cloud'sFilesEditor
does. -
#1295
0bc4113dThanks @tenphi! - Add thec3typography preset.c3extends the uppercase caption scale one step belowc2: 11px / 16px line
height, 600 weight,0.02emtracking, uppercase. It is the size used for table
column headers, wherec2is a touch heavy against 14px body text.The caption scale now reads
c1(14px) →c2(12px) →c3(11px). -
#1295
0bc4113dThanks @tenphi! - AddItemTable,DataTableandPagination.Two tables over one shared engine, replacing the ag-grid wrappers Cube Cloud
carries today. They keep the same names, so migration is mechanical, and drop
theOmit<AgGridReactProps, …>intersection that made every ag-grid option
public API.ItemTable— lists of records that get acted on. Sorting, row selection
with a bulk action bar, row links and a row menu, a toolbar with client or
server search, client/server pagination or infinite scroll, column resize and
pinning, row reordering and drop-onto-row, virtualization, and per-row visuals
throughgetRowProps.DataTable— query results. The same engine with an analytical grid's
defaults rather than a list's:t4type,smalldensity, banded rows,
resizable columns and column rules, all on by default. What is genuinely
different is multi-column sorting whose array order is the precedence, pinned
totals that sit outside sorting and paging, continuous row numbers, and
rectangular cell selection with⌘/Ctrl+Ccopying the block as TSV and as an
HTML table so spreadsheets keep the cell boundaries.PaginationandusePagination, which the kit did not have.type="numbers"
is the default deliberately: Cloud's builds aSelectof every page, which is a
thousand collection items per render at 100k rows.Neither table knows anything about Cube. Measures, dimensions, pivots and
drill-downs arrive as ordinary columns,renderoutput and
column.header.menucontent, which is what keeps Cloud's column-header menu in
Cloud.Built on a native
<table>: sticky pinned columns need a cell's containing
block to be the scrollport, and a grid item's is its own grid area — which is
why a div-and-CSS-grid design cannot have them without ag-grid-style flex lanes.
Header and body then agree through<colgroup>with nothing to sync.Some behaviour worth knowing without reading the source:
- A refresh fades the table, header included, and sweeps a band of lower
opacity across it rather than covering the rows with a spinner — the previous
result stays readable, which is the point of keeping it on screen. - A sort slides rows to their new positions over 120ms, so a row can be followed
to where it went. Only a reorder animates: if the rows keep their relative
order the table never moves, however much the layout shifts underneath them —
so mounting, resizing and filtering are all silent. - Infinite scroll starts fetching a screen before the end and holds the scroll
height with a batch-sized run of skeleton rows, so scrolling is not
interrupted and nothing lurches when the rows land. - Both animations respect
prefers-reduced-motion. - Selection survives sorting and paging: it is keyed, and a cell range is stored
as two corners re-resolved against the current order.
DraggableCollectiongainsonItemDrop,shouldAcceptItemDropand
renderPreview, so a drag preview can be a React node instead of markup written
intoinnerHTML. - A refresh fades the table, header included, and sweeps a band of lower
-
#1291
90e86d48Thanks @tenphi! - ExportIconSwitch(and itsCubeIconSwitchPropstype) from the package root.IconSwitchcross-fades between icons when its children change — the animated
icon swap used inside buttons and items. It already had a stories file and a
published docs page underHelpers/IconSwitch, andsrc/components/helpers/index.ts
exported it, but that barrel is not re-exported fromsrc/index.ts— only
DisplayTransitionwas pulled through. So the component was publicly documented
while being impossible to import.import { IconSwitch } from "@cube-dev/ui-kit"; <IconSwitch>{isLoading ? <LoaderIcon /> : <CheckIcon />}</IconSwitch>;
Patch Changes
-
#1297
39c49438Thanks @tenphi! - DeclareisBufferedon the four fields that implement it rather than on the shared
CubeTextInputBaseProps.TextInput,TextArea,PasswordInputandSearchInputbuffer their value;NumberInputand
CommandTextAreaare built on the same base type but keep their own text and never read the prop.
Declaring it on the base put it in their declared surface too. It now comes from a
CubeBufferedValuePropsmixin, exported alongside the base props, so a field built on that base in
future doesn't inherit a flag it ignores.Type-level only — no runtime change, and no behaviour change for the four fields.
-
#1298
e2c583dcThanks @solarrust! - FixedPickerandFilterPickerwithdisallowEmptySelectionin single selection mode: re-selecting the already-selected item (by click or Enter) now closes the popover without firingonSelectionChange, matching the react-aria Select behavior. Previously the popover stayed open and no event fired at all.Without
disallowEmptySelectionthe behavior is unchanged: re-selecting the current item still deselects it and firesonSelectionChange(null).ListBoxandFilterListBoxnow expose theallowDuplicateSelectionEventsprop (React Stately pass-through) that the fix is built on. -
#1295
0bc4113dThanks @tenphi! - FixuseContextMenuopening its popover far from the pointer.The hook positions an invisible anchor at the click coordinates, but rendered it
wherever the consumer placedrendered— so those coordinates resolved against
whichever positioned ancestor happened to enclose it, and the menu opened one
ancestor-origin away from the click.Treeshowed this too.The anchor now sits in a zero-size
position: fixedhost portalled tobody.
fixedmakes the containing block's origin the viewport, and the portal keeps
it clear of transformed ancestors, which capturefixed— a virtualized row is
usually translated, so the fixed host alone would still be anchored to the row.
Coordinates are now computed in viewport space to match.